개요
개발을 하면 할 수록, 테스트의 중요함에 대해 깨닫게 된다. 규모가 큰 프로젝트에서 코드 변경으로 인한 예기치 못한 사이드 이펙트를 방지해주고, 의도한 대로 기능이 동작하는지 빠르게 확인할 수 있게 만들어주는 테스트 코드. 작성한 후 돌려보는 것 까지 필수로 해야겠지? 🤔
테스트 컨테이너를 이용해 테스트 멱등성을 유지하며 손쉽게 프로젝트 테스트를 수행해보자!
테스트 환경을 만드는 세 가지 방법
📚 Test용 DB를 세팅하는 여러가지 방법
대부분의 경우 우리는 운영 환경별로 Property (Properties)를 분리해서 사용한다. 각 property 내부엔 active 환경에 맞는 DB, Log level 등 다양한 환경 설정이 존재한다. 개인적인 경험으로는, prod(운영 서버), dev(개발 서버)는 동일한 스펙의 DB를 사용하고 local (로컬 개발 환경)은 프로젝트 상황에 따라 아래 방법들을 사용했다.
1. docker container에 DB를 올려 사용한다
일반적으로는 컨테이너에 DB를 올리지 않는다. 컨테이너를 삭제하면 DB에 저장된 내용도 전부 삭제되며, 컨테이너 대수에 따라 동기화 문제가 발생할 수 있기 때문에 위험하기 때문이다. 하지만 로컬에서 기능의 동작 여부만 확인하기 위해 더미 데이터를 넣고 테스트용으로 사용한다면, 앞서 말한 문제를 크게 신경 쓸 필요가 없어진다.
- 장점 : 한 번 환경 설정을 마친다면, 이후 더 건드릴 필요가 없다.
- 단점 : 이전에 도커를 써 본 적이 없다면 docker 학습, 세팅, dockerFile 작성 등의 추가 공수가 꽤 드는 편. 테스트를 위해 도커 실행하는 과정이 필요하고, 만약 ddl이 수행되어야 한다면 DBMS에 따라 db cache를 비워줘야 한다. 개인적으로 많이 번거로웠다.
2. Local 환경에 DB를 직접 설치해서 사용한다
아주 심플한 방법. Mac 유저라면 homebrew를 이용하거나, dmg 파일을 공식 홈페이지에서 다운받아 아주 쉽게 로컬에 DB를 설치할 수 있다. 또 세팅한 김에, DataGrip 같은 툴을 이용해 직접 쿼리를 실행하고 관찰하는 등 다양한 실습을 해 볼 수도 있다.
- 장점 : 초기 세팅이 간단하다. 한번 설치하고 나면, 이후 동일 DBMS를 사용하는 다른 프로젝트에도 활용할 수 있어 꽤 편리하다.
- 단점
- 개발자마다 DB에 다른 데이터가 저장되어 있을 수 있으니, 멱등성 유지를 위해 sql.init 과정이 필수로 요구된다.
- 프로젝트마다 sql level에서 직접 새로운 계정, schema를 만드는 작업이 필요하다.
3. H2 DB를 사용한다.
인강이나 블로그 포스팅 등을 보면 보통 이 방법을 많이 사용하는 것 같다. H2는 Java 기반의 In-memory RDBMS로 따로 설치할 필요 없이 의존성만 추가해주면 되며, 표준 SQL을 대부분 지원해준다. 용량도 약 2MB로 매우 가벼워 테스트 용도로 사용하기 딱 좋다.
- 장점 : 설치할 필요가 없다, 인메모리 기반이라 빠르다, 브라우저 콘솔을 지원한다.
- 단점 : 운영 DB에 필요한 문법을 100% 지원해주는 것은 아니다. 따라서 특정 DB에 특화된 기능을 테스트하기 어렵다.
🤔 전부 단점이 하나씩은 있는데요?
위에서 언급한 방법들의 단점을 요약하면 이렇다 💁♀️ 러닝 커브가 있고, 귀찮고, 멱등성 유지가 안되며, DB 특화 기능을 소화하지 못한다.
다른건 몰라도 테스트하기 귀찮아진다 & 멱등성 유지가 안된다는 점이 꽤 크리티컬하다고 생각했다. 테스트는 정말 숨 쉬듯 돌려볼 수 있어야하는데, 테스트를 돌리는 과정이 귀찮다면 자연스럽게 테스트하기 싫어지지 않을까? 또 테스트하는 사람마다 다른 테스트 결과를 받는다면, 과연 그 테스트 결과를 신뢰할 수 있을까?
그래서 프로젝트에 적용될 테스트 방법은 적어도 아래는 꼭 충족해야한다고 결론을 지었다.
1. 테스트하기 편해야 한다
: 시간이 적게 들고, 가벼우며, 명령어 하나로도 충분한.. 그런 방법 😄
2. 멱등성을 유지할 수 있어야 한다
: 즉 내가 테스트 방식을 제어할 수 있어야한다! 최대한 외부 의존성이 적은 방법을 찾아보자.
3. DB 특화 기능을 수화할 수 있어야 한다
: 사실 아직까지 특정 DBMS에 종속적인 기능을 써보진 못했지만, 만약 마주친다면 프로덕트에 큰 영향을 미칠 것 같았다. 따라서 지원되지 않을 경우의 사이드 이펙트를 고려하여 여기까지 충족 조건으로 정리했다.
테스트 컨테이너
놀랍게도 위에서 이야기한 조건들을 충족하는 컴팩트한 테스트 환경 구성 방법이 존재한다. 바로 테스트 컨테이너 (TC)! 테스트 컨테이너의 공식 홈페이지의 소개 문구는 다음과 같다.
~ 바쁜 현대인을 위한 3줄 요약 ~
1. 테스트 컨테이너는 JUnit을 지원하는 Java 라이브러리다.
2. 도커 컨테이너 기반의 일회용 인스턴스를 제공한다.
3. 각종 DBMS의 이미지를 이용해 내가 아는 DB 그대로 쓸 수 있고, 통합 테스트, UI 테스트 등 각종 테스트에 활용 가능하다.
대강 쓱 읽어보면 개발자가 직접 DB를 컨테이너화하는게 아니라, 외부 라이브러리를 이용해 테스트용 DB 컨테이너를 올릴 수 있다는 점에서 메리트가 있는 라이브러리구나! 라는 느낌이 빡 온다. 이름은 생소하지만, TC 라이브러리를 쓴다는 점만 제외하면 나머진 Docker를 사용하는 것과 동일하다.
오 🤔 설명을 보니 딱 위에서 내가 원했던 그 스펙과 꽤 비슷하다. 컨테이너 기반의 일회용 인스턴스이니 멱등성을 유지할 수 있으며, DBMS의 이미지를 사용하므로 DB 특화 기능도 사용할 수 있다. 프로젝트 적용 방법과 사용법만 간단하면 아주 완벽할 것 같다.
이 테스트 컨테이너의 사용법이 아~~~~~~주 쉽다. 한번 아래에서 Spring Boot 3을 이용해 확인해보자.
⚙️ Spring Boot 3, Gradle 프로젝트에 테스트 컨테이너 도입하기
공식 홈페이지에서 JUnit4, JUnit5, Spock Quickstart를 제공해주니 참고하세요~
1. Gradle 의존성 추가하기
testImplementation 'org.testcontainers:testcontainers:1.16.0' // TC 의존성
testImplementation 'org.testcontainers:junit-jupiter:1.16.2' // TC 의존성
testImplementation 'org.testcontainers:postgresql:1.17.6' // PostgreSQL 컨테이너 사용
testImplementation 'org.testcontainers:jdbc:1.16.0' // DB와의 JDBC connection
아래와 같은 Exception이 발생했다면, 아마 높은 확률로 testImplementation 'org.testcontainers:jdbc'을 생략했을 것이다.
java.lang.ClassNotFoundException: org.testcontainers.shaded.org.apache.commons.lang3.StringUtils
기능 작동 시, TC datasource를 이용한다고 알려주는 과정이 필요하다. 이를 TC 공식 문서에선 'Using a specially modified JDBC URL' 이라고 표현하는데, 간단히 설명하자면 기존에 사용하던 Jdbc url에 "tc:" 문자열을 추가해서 테스트 컨테이너임을 명시해줄 수 있다는 뜻이다.
ex. 원본 URL : jdbc:mysql://localhost:3306/databasename
ex2. TC URL : jdbc:tc:mysql://localhost:3306/databasename
TC가 붙은 url을 jdbc datasource로 인식하기 위해 필요한 depencency라고 이해하면 될 것 같다..! 혹시 아니라면 댓글 부탁드립니다 🙌
(출처 : https://www.testcontainers.org/modules/databases/jdbc/)
또, 나는 프로젝트에서 PostgreSQL을 사용해서 해당 컨테이너를 사용했다. 만약 다른 DBMS를 사용한다면 해당 DB에 맞는 depencency를 추가해주어야 한다. 자주 사용되는 메이저 DB 모듈을 몇 가지 첨부한다 :)
- MySQL : https://www.testcontainers.org/modules/databases/mysql/
- MariaDB : https://www.testcontainers.org/modules/databases/mariadb/
- MongoDB : https://www.testcontainers.org/modules/databases/mongodb/
( 제공하는 DB Container Module은 Testcontainer 공식문서에 전부 기재되어있으니, 만약 위 첨부 링크에 내가 사용하는 DB가 없다면 여기 링크를 참고하시면 됩니다.)
2. Test profile 작성하기
기존에 사용하던 application.yml (또는 application.properties)를 그대로 쓰되, spring.datasource 부분만 수정하면 된다.
spring:
# jpa 관련 설정
jpa:
output:
ansi:
enabled: always
database-platform: org.hibernate.dialect.PostgreSQLDialect
database: postgresql
# Postgres testcontainer
datasource:
url: jdbc:tc:postgresql:{version}:///{dbname}?TC_INITSCRIPT=file:src/test/resources/schema.sql
username: Username
password: password
driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver
- datasource.url : TC database url. 위에서 설명한 것처럼 jdbc: 뒤에 tc: 를 추가해 테스트 컨테이너를 사용함을 명시한다.
- TC_INITSCRIPT를 이용해 컨테이너 가동시 특정 DB 스크립트를 이용해 DB를 초기화할 수 있다. 위의 예시처럼 script의 경로를 입력해주면 된다.
- datasource.driver-class-name : 테스트 컨테이너에서 사용되는 driver-class이다. tc 키워드를 가진 url을 JDBC 드라이버가 인식할 수 있게 넣어주는 설정인데, 필요한 dependency를 전부 추가해주어도, IntelliJ 위에서 계속 저렇게 Cannot resolve class error가 뜬다. 하지만 실행은 잘 되니 그냥 무시해주자! 😇
3. 테스트코드에 테스트 컨테이너를 이용함을 알려주기
@SpringBootTest
@Testcontainers
@ActiveProfiles("test")
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
- @Testcontainers : 여기서는 테스트 컨테이너를 이용한다는 의미
- @ActiveProfiles("test") : test profile을 사용하겠다고 선언한다. active profile이 local, dev더라도 테스트에선 test profile (= Test Container)을 이용하게 된다.
- @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) : 기본 내장 DB인 H2를 사용하지 않고, test profile에 정의된 DB를 사용하겠다고 선언한다.
TestConfig 등을 만들어 아래처럼 테스트 컨테이너 설정 파일을 직접 작성할 수도 있지만, 이미 test profile에서 필요한 설정을 전부 마쳐두었다. spring boot가 빌드될 때 test profile에서 이미 필요한 테스트 컨테이너가 올라갔을 것이다. TestConfig를 작성하면 동일한 설정의 테스트 컨테이너가 여러번 뜨게 되어 오히려 성능이 저하될 수 있으니, profile에서 한 번만 작성해주어도 충분하다.
(아래 예시는 Spock - Groovy 기반으로 작성된 코드입니다!)
@SpringBootTest
@Testcontainers
@ActiveProfiles("test")
class TestConfig extends Specification {
@Container
private static final PostgreSQLContainer postgreSQLContainer =
new PostgreSQLContainer("postgres:15")
.withDatabaseName("")
.withUsername("")
.withPassword("")
.withInitScript("schema.sql")
def setupSpec() {
postgreSQLContainer.start()
System.setProperty("spring.datasource.url", postgreSQLContainer.jdbcUrl)
System.setProperty("spring.datasource.username", postgreSQLContainer.username)
System.setProperty("spring.datasource.password", postgreSQLContainer.password)
}
def cleanupSpec() {
postgreSQLContainer.stop()
}
}
이렇게 테스트 컨테이너 설정을 마친 후, 테스트를 돌려주면 아래와 같이 테스트 컨테이너가 뜬 모습을 확인할 수 있다.
마무리
오늘은 테스트 컨테이너를 이용해 테스트 환경을 만드는 방법에 대해 알아봤다. 컨테이너 기반으로 정해진 스크립트, 또는 테스트 코드에서 save되는 데이터만 이용하기에 멱등성을 유지할 수 있으며, gradle build/test 명령어 하나로 컨테이너를 올리고 내릴 수 있기에 아주 간편하다.
테스트 양이 많아지면 테스트 속도가 느려진다는 단점이 있다고 하는데, 테스트 컨테이너를 매번 올리고 내리는 방식이 아니라, 맨 처음 한 번만 올라가면 되도록 설정하면 어느정도 해소가 되지 않을까 싶다. 아직은 프로젝트에 테스트 코드가 많지 않지만, 이후 테스트 코드가 많아지면, 또는 실무에서 적용할 일이 생기면 테스트 성능 개선에 대해 탐구해봐야겠다 😎
'Develop > Spring' 카테고리의 다른 글
[Spring Boot] 자바 스프링에서 처리율 제한 기능을 구현하는 4가지 방법 (2) | 2023.06.21 |
---|---|
[Spring Boot] Jsoup으로 OG태그 메타 데이터 크롤링하기 (1) | 2023.06.14 |
[Spring Boot] count를 구현하는 5가지 방법 (2) | 2023.05.24 |
[Spring Boot] ConstraintValidator를 이용해 나만의 validator annotation 만들기 (1) | 2023.05.15 |
[Spring Boot 3] SpringDoc과 Swagger를 이용해 API 문서화 자동화하기 (4) | 2023.03.21 |