Testcontainers를 이용한 테스트 환경 구축하기

Testcontainers를 이용해 Production 환경과 동일한 테스트 환경 구축하기

장중수
2022.01.10

신상마켓Elasticsearch로 상품정보를 제공하고 있습니다.

일반적으로 검색 서비스라고 하면 아래와 같은 구조로 이루어져 있습니다.

  • 수집 : 필요한 정보를 수집
  • 스토리지 : 수집된 정보의 데이터 저장소
  • 색인 : 저장되어 있는 데이터를 검색 가능한 구조로 가공 및 저장
  • 검색 : 색인되어있는 데이터를 찾아 서빙

신상마켓 검색 서비스의 Architecture를 간단하게 살펴보면 아래 그림과 같은 구조로 되어있습니다.

위 그림에서 색인 부분은 다음 두 가지 중요한 기능을 담당하게 됩니다.

  • Database에서 데이터를 가져와 가공한다.
  • 가공된 데이터를 검색엔진에 색인한다.

이렇게 중요한 색인 서버에는 다수의 로직이 구현되어 있으며 장애에 견고하게 구성되어야 하므로, 새로 추가된 로직을 면밀히 검증하고 배포 후 발생 가능한 side-effect을 줄이기 위해 테스트 구성에 더 많은 신경을 쓰고 있습니다.

아래 내용은 작년 12월에 개최한 딜리셔스 테크데이에서 발표한 내용을 기반으로 정리해 보았습니다.

TestCode 작성 시 자주 사용했던 방법들

Database Test

테스트는 Database를 붙여서 하게 되므로 흔히 아래와 같은 방법들을 사용하는데, 모두 다 조금씩 불편한 점을 가지고 있습니다.

  • Local : 로컬에 설치해서 환경 구축 후 테스트에 사용
  • In-Memory : 인 메모리 DB를 활용하여 테스트 구동시 사용
  • Embedded Library : Library를 이용하여 테스트 구동시 사용

문제는?

Local, In-Memory, Embedded Library 모두 간편하게 사용 가능하지만 위 그림처럼 문제점이 보입니다. 특히 테스트에서 중요한 멱등성이 깨지게 되고 데이터를 계속 Clean 해줘야 하는 큰 귀찮음이 기다리고 있습니다.

멱등성(idempotent)은 수학이나 전산학에서 연산의 한성질을 나타내는 것으로, 연산을 여러 번 적용하더라도 결과가 달라지지 않는 성질을 의미합니다.

Docker 활용

자, 이제 Docker를 활용해 봅시다.

  • pull-request.yml : gihub workflows를 이용하여 테스트 코드에 사용
  • docker-compose.yml : docker-compose 파일을 만들어 테스트 코드에 사용

이것도 문제가?

설정 파일과 테스트 수행 전후로 Container를 관리해야 합니다. Container의 포트를 변경할 때마다 테스트 코드의 포트도 바꿔 줘야 하고, 병렬 테스트 시에 포트 충돌 문제가 발생하기도 합니다.

역시 이것도 귀찮죠.

Mocking

DB와의 통신을 건너뛰고 순수한 로직만을 테스트할 수 있도록 Mock을 사용하는 것도 한 방법입니다.

  • SpyBean : given에서 선언한 코드 외에는 전부 실체의 것을 사용
  • MockBean : 껍데기만 있는 객체, 내부의 구현 부분은 사용자에게 위임

뭔가 아쉽죠?

Mock은 테스트의 수행을 쉽게 하고 비즈니스 로직에 집중할 수 있게 하는 장점이 있습니다. 다만 완전한 Mock을 구현하는 것은 불가능하기 때문에, 실제 환경에서 DB와 연동했을 때 문제가 발생할 가능성을 무시할 수 없습니다.

지금까지의 문제들만 모아 보면

귀차니즘, 멱등성, 위험 요소로 묶어 봤습니다. 안전하고 완성도 높은 코드를 위해 여러가지 테스크 환경을 시도했는데 뭔가 개운하지 못한 느낌입니다.

저희는 이런 문제들을 Testcontainers를 이용해 해결해 보았습니다.

Testcontainers

Testcontainers는 Docker를 기반으로 Junit 수행 시 테스트를 도와주는 Java Library입니다.

Testcontainers 특징

  • Java로 Container를 동작시킬 수 있다.
  • Dockerfile, docker-compose, docker hub로 Container를 동작시킬 수 있다.
  • 테스트 실행 전/후로 Container를 start, stop 할 수 있다.
  • Parallel Test를 지원한다.
  • 다양한 module을 제공하고 있다.
  • GitHub 5.5k stars, 313명의 Contributors 및 지속적인 version up이 되고 있다 (2022.01. 기준)

Testcontainers Lifecycle

  • restarted
    • test method 수행이 될 때마다 Container가 새로 시작하는 방식입니다.
    • method마다 Container가 수행되기 때문에 멱등성을 보장한 테스트가 가능합니다.
    • method가 많을수록 Container의 start, stop의 반복이 많아 테스트 수행 시간이 오래 걸릴 수 있습니다.
  • shared
    • test class scope 안에서 method 가 수행될 때 Container가 한 번만 동작해 공유합니다.
    • method마다 수행되지 않고 공유하기 때문에 데이터 관리가 어느 정도 필요합니다.
    • method마다 수행되지 않기 때문에 테스트 수행 시간을 줄일 수 있습니다.

색인 Server Testcontainer 적용

앞서 검색 서비스 설명 중 색인 서버에 적용한다면 어떤 코드가 들어갈까요??

  • Database에서 데이터를 가져와 가공한다.
  • 가공된 데이터를 검색엔진에 색인한다.

Dependency

testImplementation "org.testcontainers:mysql:1.16.2"
testImplementation "org.testcontainers:elasticsearch:1.16.2"

이미 Testcontiners에는 우리가 사용할 modules가 준비되어 있습니다.

Container

@Container
private MySQLContainer mysqlContainer =
		new MySQLContainer(DockerImageName.parse("mysql"))
		.withDatabaseName("db01")
		.withUsername("user")
		.withPassword("password");

@Container
private ElasticsearchContainer elasticsearchContainer =
		new ElasticsearchContainer(DockerImageName.parse("dealicious/opendistro:0.2")
		.asCompatibleSubstituteFor("docker.elastic.co/elasticsearch/elasticsearch"));

Database와 Elasticsearch가 준비되었습니다. Docker 파일도 필요 없고 Java Code로 Container를 구성했습니다.

적용 완료

Dependency 추가와 Container Java Code 만으로 테스트 환경을 구축하였고, mysql, elasticsearch로 테스트할 준비가 완료되었습니다. 이제 마음껏 테스트하면 됩니다.

Github Workflows를 통해 테스트 수행 후 빌드하고 싶다면, 특별한 Docker 설정 필요없이 pull-request.yml을 구성하고 테스트 코드만 잘 동작하면 됩니다.

MockServer Modules가 있어 타 서비스와의 연동 테스트 역시 쉽게 구성할 수 있습니다.

평가

기존의 문제로 지적되었던 귀차니즘, 멱등성, 위험이 어떻게 해결되었는지 보겠습니다.

귀차니즘

멱등성

위험

장점

  • Java Code로 보다 완벽한 테스트 코드를 작성할 수 있다.
  • 환경에 구애받지 않고 멱등성 있는 테스트 품질 향상을 이룰 수 있다.
  • 클라우드에서 동작하고 있는 Production과 거의 동일한 테스트 환경을 구축할 수 있다.

단점

  • Restarted Lifecycle를 이용해 독립적인 테스트 환경이 많아질수록 시간이 오래 걸릴 수도 있다.
  • Parallel Test에 대해서 아직 완벽하게 지원하고 있지는 않다.

결론

테스트를 구성할 때 가장 귀찮은 3rd Party 연동을 Testcontainers를 통해 쉽게 할 수 있음을 알게 되었습니다. 그 외로 MockServer, Localstack 등을 활용해 AWS 환경에 맞는 테스트를 구축하려 하고 있으며, 이는 Robust한 어플리케이션을 구현하는데 중요한 역할을 담당할 것으로 생각합니다.

장중수

딜리셔스 백엔드 개발자

"No Pain, No Gain"