통합TC에TestContainer사용하기-dist-외부공개.pdf
2.30MB

블로그 포스팅은 업데이트 X. 상세 내용은 위 pdf 참조.

 

운영에서는 다른 DB 쓰고, TC는 다른 DB(In-Memory) 사용할 때의 문제점

유닛테스트에서는 DB를 안쓰거나, 간단한 쿼리 정도 사용하게 되므로 In-Memory 사용해도 무방하지만
복잡한 쿼리를 포함하여 실행하는 통합테스트는 In-Memory DB를 사용하면 아래와 같은 문제를 마주할 수 있다.

 

운영 DB는 Oracle 쓰고 있었는데, TC나 오프라인 환경에서는 H2를 사용하게끔 변경했다.

그러나... 다음과 같은 문제점이 있음.

 

  • 운영 DB와 In-Memory DB의 문법이 100% 호환 되지 않는다.
    • `` sysdate - ?`` 같은건, 오라클에서는 그냥 사용 가능하지만 H2에서는 타입 추론이 안돼서 ?를 CAST 해주어야 한다.
    • `` SUBSTRB`` 함수는 오라클에만 존재한다. H2에서는 `` CREATE ALIAS``로 직접 자바로 짜주면 되긴 함...
    • 이 밖에도 오라클에서는 되고 H2에서는 안되는 문법을 꽤나 높은 확률로 마주치게 된다. pagination 이라던가? Oracle Compatibility Mode 란게 있긴 한데, 좀 미흡하다
    • 오라클에서만 되는 문법을 다 걷어내고 호환되도록 짜는 것은 어렵고, 넌센스이며, 절대 H2로 바꾸기 어려울 것 같은 코드도 마주칠 수 있다. 그렇다고 쿼리를 이중으로 짜는건 너무 비효율적이다.
  • In-Memory DB에서 잘 넘어갔는데, 운영 DB에서는 안될 수 있다. 이런 케이스를 마주칠 가능성이 분명 존재한다.
    • = TC를 신뢰할 수 없다. 
    • 이건 문법의 문제라기 보단 내장함수 등 내부 동작에서 차이가 있을 수 있기 때문.
  • 오히려 이런 문제들 때문에 H2 썼을 때 공수가 더 드는 것 같다. (잘 돌아가는데... H2를 위해서 운영 쿼리를 바꿔야 한다? 좀 그렇다...)

 

더 좋은 방법은 없나?

더 좋은 방법은, 사실 In Memory DB를 사용하여 얻고자 하는 것이 "고립된 데이터 상태에서의 테스트" 이므로 기존의 Oracle이나 MySql을 이용해 고립된 데이터 상태를 만들 수 있다면 문제가 해결된다.

 

```

테스트 DB 만들고  ->  [DDL.sql, DATA.sql] 로 초기화하고  ->  TC 돌리고  ->  DB 삭제

```

 

이런 사이클로 돌릴 수 있다면 굳이 H2 쓸 필요가 없는거 아닌가.

TestContainers 사용해서 아예 DB 올렸다 내렸다 하는 식으로 가능할거고... 시간은 좀 오래걸리겠지만, 이득이 훨씬 큰 듯.

 

로컬에서만 H2 사용해서 돌린다?
  • 그럼 로컬에서는 통합 TC를 안돌려보겠다는 의미가 됨. PR 올려서 CI 빌드 할 때만 통합 TC를 돌려볼 수 있을텐데... 이건 너무 비효율이다.
  • 빌드 시에도 로컬에도 docker 올려서 리얼DB와 같은 DB로 통합 TC 돌리는게 낫다고 봄.

 

 

암튼 정리해보면.

1. TestContainers를 사용한 방식은 매번 DB 올렸다 내렸다 해야 하니 시간은 좀 오래 걸릴지 몰라도, "고립된 데이터 상태"를 만들 수 있으므로 제일 괜찮아 보인다.

  • TestContainers를 사용한 방법과 Gradle | Maven 빌드 스크립트에 포함하는 방법이 있음.
  • 매번 올렸다 내렸다 하는건 `` -DreuseDatabase`` 플래그 이용해서 해결 가능한 듯.
  • DDL이나 초기 데이터는 flyway로 관리

 

2. 그 다음으로 괜찮은건 docker 이용해서 local에 아예 DB 올려두고 쓰는 것. 이건 롤백이 좀 귀찮을 수 있다. (Spring Batch)

  • 근데 CI 서버에서 돌아가는 TC에 대한 DB도 필요하므로 CI 서버에도 DB를 올려야 한다는게 문제.
  • CI 서버에 DB 안올리고 alpha DB를 사용한다면, alpha QA 동안 적재된 데이터 등등과 섞임. 
  • 따라서 알파 DB를 사용할거라면, CI Test 수행 시 새 테이블스페이스를 생성하고 끝나면 삭제. 하는 식으로 알파 데이터와 분리하면 가능할 듯.
  • 테이블 스키마, 인덱스 등 복사는 (1) 처럼 flyway로 관리해도 되고... 아예 알파DB만 사용한다면 알파DB 스키마를 복사해와도 됨.
    • 오라클의 경우 expdp,impdp 명령어 또는 프로시저  

 

3. H2 사용한 방법은 상기한 문제가 있으므로 논외.

 

참고

과연 In Memory DB가 TC에 더 효과적인가?? 에 대한 의문이 들어서. 좀 찾아봤다.

 

  • github.com/dotnet/efcore/issues/18457  
    • efcore라는 DB mapper에서 `` InMemoryProvider``를 제거하느냐 마느냐에 대한 논쟁으로 이 것에 대해서는 의견이 분분하지만, 대략 필요한 부분을 종합해보면
    • 1. 유닛 테스트에서는 InMemory DB가 충분히 유용하다.
    • 2. 통합 테스트에서는 당연히 리얼 DB 써야한다.
  • jimmybogard.com/avoid-in-memory-databases-for-tests/ 
    • Unit Test에서도 그닥 이점이 없다. 라고 꽤나 강하게 얘기하고 있는데... 이건 efcore에서 In-Memory DB가 리얼DB를 흉내내도록 하는 것에 대한 어려움이 포함되어 있기 때문에 사견이 좀 강하게 들어간 것 같고...
    • 아무튼 이 사람도 유닛 테스트에는 InMemory DB 쓰더라도, integration test에는 무조건 리얼 DB 써야 된다고 있음.
  • 👍   phauer.com/2017/dont-use-in-memory-databases-tests-h2/
    • 내가 했던 고민이랑 거의 비슷한 내용.
    • Solution: Throw H2 and Fongo away and use a dockerized version of your real database instead. Docker simplifies the management of database instances.

 

TestContainer 이용할건데, 설정을 어떻게 구성하면 좋나?

  • 명시 안해도 자동으로 @SpringBootTest 테스트 실행 시 container DB 사용하도록 설정.
    • Query를 날리려면 Mapper를 DI 받아야 하므로 어차피 SpringBootTest여야 함. (Spring과 무관한 Unit Test는 X) 
    • 각각의 테스트클래스에서 DB 설정하지 않아도 되게끔. 최소 annotation이나 interface로 분리.
  • test container 사용 안하고 디버깅 목적으로 로컬DB, 알파DB 사용해서 돌리고 싶다면, 간단하게 변경 가능할 것.

 

testcontainer 초기화 방식

  1. 각 Test 마다, 매번 새로운 testcontainer 사용하기
  2. testcontainer는 1개 띄우고 계속 유지하는데, 각 Test 마다 flyway로 DB를 특정 상태로 되돌리기
  3. 그냥 전체 테스트에서 하나의 testcontainer를 초기화 없이 연속해서 사용

일단은 3번으로 구성. 3번은 TC를 실행하는 순서에 따라서 상태가 일정하지 않으므로 TC가 실패할 가능성이 있어 그리 좋은 방법은 아니지만, DB 띄우는 시간을 줄일 수 있음.

 

현재 상황에서의 추가적인 요구사항으로는...
  • MyBatis가 test container 사용하도록 하려면, test container와 연결된 dataSource bean 필요.
    • [DataSourceAutoConfiguration, DataSourceTransactionManagerAutoConfiguration] 이 비활성화 되어 있으므로, properties 기반 설정을 사용하지 못한다.
    • java config 이용해 [DataSouce, PlatformTransactionManager] Bean을 직접 만들어 줄 것.

 

AutoConfiguration 사용한다면 java config 작성할 필요 없이 properties 기반으로도 가능

 

 

OracleContainer 객체 사용하는 방법

```java

@Profile("test")
@TestConfiguration
public class TestDatabaseConfig {

    @Bean
    public OracleContainer oracleContainer() {
        OracleContainer oracleContainer = new OracleContainer("private.docker.registry/umbum/oracle-xe-11g");
        oracleContainer.start();
        return oracleContainer;
    }

    @Bean
    @DependsOn("oracleContainer")
    public HikariDataSource dataSource(OracleContainer oracleContainer) {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl(oracleContainer.getJdbcUrl());
        HikariDataSource dataSource = new HikariDataSource(config);
        initSchema(dataSource);
        return dataSource;
    }

    private void initSchema(DataSource dataSource) {
        Flyway flyway = new Flyway();
        flyway.setDataSource(dataSource);
        flyway.baseline();
        flyway.migrate();
    }

    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {

        return new DataSourceTransactionManager(dataSource);

    }

}

```

```

ryuk.container.image=private.docker.registry/umbum/testcontainers-ryuk:0.3.1

```

 

JDBC url 사용하는 방법

```java

@Profile("test")
@TestConfiguration
public class TestDatabaseConfig {

    @Bean
    public HikariDataSource dataSource() {
        HikariConfig config = new HikariConfig();

        config.setJdbcUrl("jdbc:tc:oracle:///databasename"

            + "?TC_INITFUNCTION=dev.umbum.batch.config.TestDatabaseConfig::initSchema");

        return new HikariDataSource(config);

    }

 

    public static void initSchema(Connection connection) throws SQLException {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(connection.getMetaData().getURL());
        Flyway flyway = new Flyway();
        flyway.setDataSource(dataSource);
        flyway.baseline();
        flyway.migrate();
    }

    @Bean
    public PlatformTransactionManager transactionManager(@Qualifier("dataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

```

```properties

ryuk.container.image=private.docker.registry/umbum/testcontainers-ryuk:0.3.1
oracle.container.image=private.docker.registry/umbum/oracle-xe-11g

```

 

flyway 안쓰고 할 수는 없나?

```java

// 잘 동작함.

config.setJdbcUrl("jdbc:tc:oracle:///databasename?TC_INITSCRIPT=db/migration/V1__Initial.sql");

// 소스를 대략 보니 파일만 받을 수 있는 것 같다. 아래처럼 디렉토리를 적으면 invalid SQL statement 오류 발생하는데 파일 이름을 sql문으로 인식하는 듯...?

config.setJdbcUrl("jdbc:tc:oracle:///databasename?TC_INITSCRIPT=db/migration");

// init script가 하나라면 jdbc url에 적고 간단히 끝낼 수 있지만, 두개 이상의 init script를 사용하려면 INITFUNCTION + flyway 등을 사용해야 한다.

```

 

flyway를 적용할 때 발생하는 문제

 

 

관련 링크