[Test] 통합 Test에 in-memory DB 를 쓰는게 더 좋을까?
블로그 포스팅은 업데이트 X. 상세 내용은 위 pdf 참조.
Spring Batch는 통합TC 수행하고 나서 자동 rollback이 불가하다.
-> 직접 rollback 코드를 작성해주어야 하는데 잡이 복잡한 경우 까다롭다. 매번 롤백 코드 신경써서 작성하는 것도 리소스 낭비다.
-> 그러면 TC 수행 후 삭제되는 H2를 쓰면 어떨까? 생각하게 되는데, 이는 아래와 같은 문제점이 있다.
운영에서는 다른 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 초기화 방식
- 각 TestMethod 마다 매번 새로운 testcontainer 사용하기
- 각 TestClass 마다 매번 새로운 testcontainer 사용하기
- testcontainer는 1개 띄우고 계속 유지하는데, 각 Test 마다 flyway로 DB를 특정 상태로 되돌리기
- 그냥 전체 테스트 suite에서 하나의 testcontainer를 초기화 없이 연속해서 사용 (pick)
4번은 TC를 실행하는 순서에 따라서 상태가 일정하지 않으므로 TC가 의도치 않게 성공/실패할 가능성이 있지만, DB 띄우는 시간을 줄일 수 있음.
docker DB 띄우고 flyway로 초기화하는데 약 20s가 소모됨. 고립 수준과 TC 수행 시간 사이의 trade-off를 고려하여 상황에 맞게 선택할 것.
참고 ) DataSource와 docker container의 생명 주기
- DB와 연결되어 있는 DataSource 수가 0이 된다면 docker container도 종료된다.
- DB와 연결되어 있는 DataSource가 생긴다면, docker container가 다시 실행된다.
- 따라서, 한 test에서 DataSource를 close 하더라도, 다른 ApplicationContext의 DataSource가 docker DB와 연결되어 있다면 docker가 종료되지 않는다.
- @DirtiesContext 붙여 DataSource Bean이 삭제되는 경우도 마찬가지다. 연결되어 있는 DataSource가 있다면 docker가 종료되지 않는다.
(운영해보니) 결국 나중에 이전 TC의 실행 결과가 다음 TC에 영향을 주어 PK violation 등이 발생함.
1. 기본적으로 공통 docker DB 사용하고, 지정한 테스트만 개별 docker DB를 사용하는 방식
2. TestClass 마다 개별 docker DB 사용하는 방법
3. 이전 TC의 실행 결과에 영향을 받지 않도록 Test를 수정하는 방법
세 가지 방법이 가능함.
1,2번은 testcontainer 설정이 TestClass 내부에 들어가야 해서 로컬DB 쓰려면 TestClass를 수정해주어야 한다는 단점이 있음.
** 참고) TC의 수행 순서를 고정하는 방법은 가능은 하지만 나쁨. 이런 테스트가 또 생기면 그 때 마다 순서를 고정할 것인가?
현재 상황에서의 추가적인 요구사항으로는...
- 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.setBaselineVersionAsString("0");
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.setBaselineVersionAsString("0");
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 등을 사용해야 한다.
```
트러블 슈팅
- 오라클 컨테이너(hub.docker.com/r/oracleinanutshell/oracle-xe-11g)에 flyway 적용 시 DB가 비어있지 않아 이슈가 있었음.
- 테스트 클래스를 일정 수 이상 묶어서 실행하면, 어느 정도 실행하다가 갑자기 JDCB Connection을 무한히 대기하는 현상
관련 링크
- test container는 각 테스트 클래스에서 생성하는게 좋을 수 있다.
- stackoverflow.com/questions/62425598/how-to-reuse-testcontainers-between-multiple-springboottests
- www.baeldung.com/spring-boot-testcontainers-integration-test
- 정리가 잘 되어 있으나, 중간에 'we can modify the JDBC URL and instruct the Testcontainers to create a database instance per test class.' 라고 되어 있는데, 실제로는 `` spring.datasource.url=jdbc:tc:...`` 명시하게 되면 각 테스트 클래스 마다 새로운 DB 사용하는게 아니라, 전체 test suite에서 하나의 DB를 사용하게 된다. (Context가 달라지면 HikariPool은 새로 만드는게 맞지만, docker DB는 계속 사용한다)
'Coding Note' 카테고리의 다른 글
나는 풀스택이 아닌데, 풀스택이란 뭘까 (0) | 2022.02.18 |
---|---|
[Thread-safety] 동시성 문제와 shared mutable state 관리 (0) | 2021.08.30 |
Serverless computing platform의 장점 (0) | 2020.12.04 |
Promise / Future에 대한 개념 정리 (0) | 2020.03.20 |
의존성 주입(DI, Dependency Injection)이란? (4) | 2019.05.10 |