-
JOOQ 테스트 환경 분리하기개발 2024. 9. 25. 16:07
jooq는 DSL을 사용하여 자바 코드를 쿼리로 변환하여 데이터베이스 작업을 한다.
여기서 사용되는 DSL 코드는 실제 데이터베이스의 테이블 정보를 컴파일 시점에 읽고 생성하여 런타임 시에 사용되는 구조이다.
프로젝트를 진행하며 테스트 환경 분리가 필요했다.
기존의 테스트는 데이터베이스를 사용하는 테스트를 진행하기 위해서는 실제 데이터베이스를 실행해야하는 수고스러움이 있었다. 만약 데이터베이스를 실행하지 않고 테스트를 실행하면 DSL 코드를 생성할 테이블 정보를 읽지 못해 에러가 발생한다. 이를 해결하고자 testContainer 를 도입하여 테스트 환경에서 컨테이너를 띄워 테스트의 환경을 격리 시키기로 했다.
문제점
- jooq 는 위 설명처럼 빌드 시 데이터베이스에 접근해 메타데이터를 기반으로 DSL 코드를 생성한다. 따라서 빌드 시점에 testContainer 가 떠있어야 DSL 코드를 생성할 수 있다.
- 하지만 testContainer 는 테스트 실행 시점, 즉 런타임 시점에 컨테이너가 올라간다.
- jooq codegenrate 는 연결된 DB 정보가 없기에 빌드 시점에 연결할 데이터베이스가 없기에 DSL 코드를 생성하지 못해 에러가 발생한다.
따라서 나는 DSL 코드 생성하는 작업 이전에 testContainer 가 떠있도록 실행 시점을 조정하여 문제를 해결하기로 했다.
TestContainer 의존성 추가
implementation 'org.testcontainers:testcontainers' implementation 'org.testcontainers:mysql' testImplementation 'org.springframework.boot:spring-boot-testcontainers' testImplementation 'org.testcontainers:junit-jupiter'
build.gradle에서 buildScript 작성
buildscript { repositories { mavenCentral() } dependencies { classpath 'org.testcontainers:testcontainers:1.19.8' classpath 'org.testcontainers:mysql:1.19.8' } }
gradle 내에서 테스트 컨테이너를 띄우기 위해서 해당 설정이 필요하다.
BuildService 구현
abstract class MySqlService implements BuildService<BuildServiceParameters.None>, AutoCloseable { private final MySQLContainer container; MySqlService() { container = new MySQLContainer("mysql:8.0.33") .withDatabaseName("ulma") .withUsername("root") .withPassword("1234"); container.start(); // SQL 파일을 컨테이너로 복사 String schemaFilePath = "C:\\\\Users\\\\SSAFY\\\\workspace\\\\S11P21E204\\\\Backend\\\\module-common\\\\src\\\\main\\\\resources\\\\schema.sql" print(schemaFilePath) File schemaFile = new File(schemaFilePath) if (schemaFile.exists()) { // 컨테이너 내부의 경로로 파일 복사 container.copyFileToContainer( org.testcontainers.utility.MountableFile.forHostPath(schemaFilePath), "/schema.sql" ); // 복사된 schema.sql 파일을 MySQL에 적용 container.execInContainer("mysql", "-u", "root", "-p1234", "ulma", "-e", "source /schema.sql"); } } @Override void close() { container.stop(); } MySQLContainer getContainer() { return container; } }
- BuildService 는 상태나 리소스를 여러 작업에서 사용할 수 있도록 도와주는 클래스이다.
- jooq 에서 테스트 컨테이너를 사용해야하기에 BuildService 에서 컨테이너를 실행하고 컨테이너를 반환하도록 했다.
- DSL 코드 생성을 위해 컨테이너를 띄우고, DDL 쿼리를 실행해서 테이블을 생성해주었다.
이제 jooq DSL 코드 생성하는 부분에서 프로필이 test 일 경우에는 컨테이너를 먼저 생성하고, 데이터베이스 정보를 읽도록 설정해야 한다.
Provider<MySqlService> dbContainerProvider = project.getGradle() // mySqlService 호출 .getSharedServices() .registerIfAbsent("mysql", MySqlService.class, {}) jooq { def activeProfile = System.getProperty('spring.profiles.active', 'prod') // 프로필 정보 가져오기 configurations { main { generationTool { logging = org.jooq.meta.jaxb.Logging.WARN if (activeProfile.contains('test')) { // 프로필이 test 라면 컨테이너 실행 def dbContainer = dbContainerProvider.get().getContainer() jdbc { driver = 'com.mysql.cj.jdbc.Driver' url = dbContainer.jdbcUrl user = dbContainer.username password = dbContainer.password } } else { // 아니라면 서버의 데이터베이스에 연결 jdbc { driver = 'com.mysql.cj.jdbc.Driver' url = 'jdbc:mysql://j11e204.p.ssafy.io:3306/ulma' user = 'user' password = 'password' } } generator { name = 'org.jooq.codegen.DefaultGenerator' database { name = 'org.jooq.meta.mysql.MySQLDatabase' inputSchema = 'ulma' } generate { deprecated = false records = true immutablePojos = true fluentSetters = true } target { packageName = 'com.ssafy11.ulma.generated' directory = 'build/generated-src/jooq/main' } } } } } }
gradle 설정을 해본 적이 별로 없어서 실행 시점을 조정하는 것에 많은 시간이 걸렸다.
빌드 시점에 프로필이 test 라면 컨테이너를 실행하고 DDL 쿼리를 실행하여 테이블을 생성하고, 컨테이너에 연결해 DSL 코드를 만들도록 설정하였다.
트러블 슈팅
@ActiveProfile("test") @SpringBootTest public class UserDaoImplTest { @Test void test () { } }
간단한 테스트를 작성 후 실행을 해보았다.
여기서 주의해야 할 점은 테스트 클래스에 프로필을 적용하기 위해 위와 같이 @ActiveProfile 을 적용해도 build.gradle 설정엔 적용되지 않는다.
분명 프로필은 test 로 실행이 되었는데 testContainer 를 띄우지 않는 걸 보고 곰곰히 되짚어보며 문제점을 발견했다.
💡적용이 안되는 이유
빌드는 컴파일을 통해 실행 가능한 파일을 만드는 것이다. 빌드를 하는 시점에 프로필이 test 로 활성화가 되어있어야 조건문을 통해 testContainer 를 실행하고 DSL 코드를 생성한다. 전제 조건은 빌드시점에 프로필이 test 로 활성화 되어있어야 한다는 것이다 ! 하지만 위의 코드는 빌드가 완료되고 테스트를 실행할 때 프로필을 test 로 활성화 하는 것이기에 빌드 시점에 영향을 끼치지 못한다. 즉 프로필이 활성화 되지 않아 testContainer를 띄우지 않는 것이었다.
위와 같이 Intellij 실행 설정에서 프로필을 test 로 설정해주고 실행을 해보았다.
의도한 대로 DSL 코드도 생성되고 실행도 되었다.
문제점
테스트 환경은 격리가 되었지만 문제점이 있다. testContainer 를 띄우고 종료하는 것이 문제인건지 아무런 코드도 없는 테스트의 실행시간이 20초 가까이 걸린다.
- 서버의 DB 에 접속해서 DSL 코드를 만들고 테스트를 하는 것.
- 로컬에서 testContainer를 실행하고 DSL 코드를 만들고 테스트를 하는 것.
1번의 경우 테스트 실행 시간은 빠르지만 서버의 환경에 따라 테스트가 실패하거나 성공하는 경우가 생길 수 있다.
즉 테스트의 멱등성이 보장되지 않는다.2번의 경우는 매번 컨테이너를 실행하고 종료하기에 테스트 환경이 완벽히 격리되고 멱등성이 보장된다.
하지만 테스트의 시간이 많이 걸린다.
테스트의 시간이 걸리더라도 서버의 환경에 따라 실패되거나 성공하는 경우가 없도록 하는 것이 적합하다고 생각하여 2번의 방법을 채택하였다.
참고 자료
'개발' 카테고리의 다른 글
findAll vs Stream vs Batch처리 비교 (0) 2024.11.07 JPA N+1 감지 기능 구현기 (0) 2024.10.30 메인 기능, 부가 기능 분리 그리고 Transactional outbox pattern (2) 2024.09.04 테스트하기 안 좋은 코드 리팩토링하기 (0) 2024.08.20 부하테스트를 위한 더미데이터 준비 (0) 2024.08.08