H2 DB로 테스트 수행 시 문제점
스프링으로 웹개발을 하고 테스트를 작성하고 있다면 H2 데이터베이스를 많이 사용한다. H2는 인메모리 데이터베이스로 스프링부트에 내장되어 있어 별도의 DB 설정 없이 간편하게 이용할 수 있다.
깃허브에서 프로젝트를 클론한 사람 모두가 테스트를 아무런 환경 세팅 없이 쉽게 돌릴 수 있고 테스트 속도도 빠르기에 개발 초기 단계나 토이 프로젝트에서 많이 사용하고 있다.
하지만 운영 환경에서는 MySQL 같은 데이터베이스를 사용하기 때문에 운영 환경과 다른 환경에서 테스트를 한다는 불안감이 있다. 실제로 환경적인 측면에나 문법적인 측면에서 다른 부분이 있어 테스트는 돌아가더라도 운영 환경에서 장애가 날 수도 있다.
Transactional(readOnly = true) 문제
스프링에서 서비스 계층에 트랜잭션을 적용하기 위해 @Transactional을 사용한다.
예전에 프로젝트를 진행하던 중 읽기 전용 메서드가 아니었는데 실수로 readOnly 속성을 true로 해서 문제가 생겼던 적이 있다. H2 DB를 사용하면 readOnly = true여도 데이터 변경을 막지 않기 때문에 테스트가 통과해서 문제가 없는 줄 알았다.
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class XXXService {
그런데 배포 후 운영 환경에서 장애가 발생했다. 에러 로그를 살펴보니 아래와 같았다.
Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed
MySQL을 사용할 때는 read-only 속성이 걸리면 데이터를 변경하지 못하게 막고 있던 것이었다. 이 경험 이후로 H2에 대한 불신이 생겼고 이를 해결할 수 없을지 생각해보게 되었다.
이 문제 이외에도 문법적인 부분에서 차이가 있는 것도 있어 얼마 전 DB 스키마를 flyway로 마이그레이션 하던 도중 문법이 MySQL에서도 통하는 것이 맞나 싶은 마음에 일일이 확인하고 수정했던 적도 있었다. 운영 DB 환경에서의 테스트가 절실한 순간이었다.
로컬에서 MySQL을 직접 설치하거나 도커를 이용해 MySQL을 구동하여 테스트를 진행할 수도 있지만 여간 번거로운 작업이 아니다.
TestContainer란?
테스트 컨테이너는 Docker 컨테이너로 래핑된 실제 서비스와의 통합 테스트를 위한 쉽고 가벼운 API를 제공하는 테스트 라이브러리이다. 테스트 컨테이너를 사용하면 mock 서비스나 인메모리 서비스 없이도 프로덕션에서 사용하는 것과 동일한 유형의 서비스와 대화하는 테스트를 작성할 수 있다.
테스트 컨테이너를 사용하면 자동으로 도커를 시작하고 원하는 컨테이너 서비스를 구동시킬 수 있다. 운영 환경과 같은 서비스를 사용할 수 있고 테스트가 끝나면 컨테이너를 자동으로 제거해준다.
도커를 설치하고 켜두어야 한다는 부분은 있지만 나머지 작업은 테스트 컨테이너가 모두 맡아서 처리한다.
적용 방법
build.gradle에 다음 의존성을 추가한다.
testImplementation "org.testcontainers:testcontainers:1.19.3"
testImplementation "org.testcontainers:junit-jupiter:1.19.3"
testImplementation "org.testcontainers:mysql:1.19.3"
그리고 application.yml에 아래와 같이 DB 설정을 작성하면 테스트 컨테이너로 MySQL을 연결하여 테스트를 진행할 수 있다.
spring:
datasource:
driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver
url: jdbc:tc:mysql:8.0:///test_container_test
username: root
password: password
위 설정을 적용하여 테스트를 돌리자 도커 컨테이너가 자동으로 실행되고 테스트를 MySQL DB와 연결하여 진행할 수 있었다.
@Transactional(readOnly = true)인 경우 데이터 변경을 했을 때 테스트가 실패하는 것도 확인할 수 있었다.
TestContainer 테스트의 문제점
테스트 컨테이너는 정말 좋은 테스트 도구이지만 문제 아닌 문제가 크게 2가지가 있었다.
도커가 없다면 설치해야 하고 테스트 전 도커를 켜야 한다.
일단 도커를 한 번만 설치하면 되고 또 개발을 시작하기 전에 켜두면 되긴 하지만 이 또한 번거로울 수 있다. 게다가 깃허브에서 우리 프로젝트를 클론 받은 사람들이 아무런 사전 작업 없이 우리 서비스를 시작해볼 수가 없고 추가적인 환경 세팅을 해야하는 점도 마음에 걸렸다.
테스트 속도가 느리다.
이 문제가 좀 치명적이었다. 현재 개발하고 있는 서비스는 아직 초기 단계라 테스트가 약 70개 정도 존재한다. 아래 그림을 보면 알 수 있듯이 테스트 컨테이너를 사용하면 테스트가 총 13초가 걸리게 된다.
하지만 H2를 사용하게 되면 아래처럼 3초가 걸려 테스트 속도가 4,5배나 빠르고 이는 테스트 개수가 많아질수록 더 치명적일거라 생각된다.
H2, TestContainer 둘 다 사용하기
결론적으로 H2 DB와 테스트 컨테이너를 둘 다 사용하기로 결정했다. 이유는 다음과 같다.
- 평소 로컬에서 개발할 때는 H2 DB를 사용해서 다른 환경 세팅이 필요없도록 하고 빠른 테스트 속도를 누림
- 운영 환경에 배포하기 전엔 MySQL 테스트가 필요하기 때문에 최소 1번 이상 테스트 컨테이너로 테스트할 수 있도록 실행 환경을 분리
application.yml은 원래 active profile이 개발할 때의 local 설정과 운영 환경에서 실제 DB에 연결하기 위한 prod 설정이 존재한다.
- application-local.yml
- application-prod.yml
여기에 테스트 컨테이너를 사용하는 환경인 설정 파일을 하나 더 추가한다. 그리고 평소에는 H2 테스트를 진행하다가 배포하기 전에 MySQL 테스트를 위해 active profile을 테스트 컨테이너로 변경하면 원할 때에만 테스트 컨테이너 환경의 테스트를 진행할 수 있다.
# application.yml
spring:
profiles:
active: test-container # 기본으로 local로 설정되어 있지만 원할 때 변경
Github Action에서 테스트 컨테이너 환경 CI
하지만 수동으로 환경 변수를 변경해서 테스트 컨테이너 환경으로 테스트하는 것도 번거롭다. 게다가 휴먼 에러로 MySQL 테스트를 하지 않고 배포해 버릴 수도 있다.
이를 방지하기 위해 pull request 시 Github Action에서 CI를 진행하면서 테스트 컨테이너를 사용하도록 구성하기로 했다.
아래처럼 테스트 컨테이너 환경으로 동작하는 application-ci.yml을 추가한다.
그리고 pull request 시 동작하는 Github Action 스크립트를 아래처럼 작성한다. 그럼 기능 개발이 끝나고 PR을 날리면 자동으로 테스트 컨테이너로 MySQL 테스트를 수행하며 빌드를 진행하게 된다. 빌드 실패 시 PR이 머지되는 것을 막는다면 MySQL 테스트 없이 운영 환경에 배포하는 실수를 완벽히 차단할 수 있다.
name: CI Build
on:
pull_request:
branches:
- '**'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: 저장소 Checkout
uses: actions/checkout@v3
- name: 자바 17 설정
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'adopt'
- name: 빌드
run: SPRING_PROFILES_ACTIVE=ci ./gradlew build
결론
H2와 테스트 컨테이너를 모두 사용하고, CI를 통해 테스트 컨테이너 환경의 테스트를 강제하면서 H2 DB와 테스트 컨테이너의 장점을 모두 가져갈 수 있었다.
다만 걱정되는 것은 지금까진 MySQL에서의 문법과 환경이 H2와 함께 맞물리는 것이 가능했기에 H2로도 테스트하고 MySQL로도 테스트 하는 것이 가능했다. 만약 MySQL에서만 가능하고 H2에서는 가능하지 않는 문법이나 환경을 사용해야할 때가 온다면 테스트 컨테이너만 사용해야할 수도 있다.
아래 링크는 작업한 PR 링크이다.
https://github.com/hufscheer/spectator-server/pull/96
참고
'스프링 > 테스트' 카테고리의 다른 글
@Mock vs @MockBean (0) | 2022.05.01 |
---|---|
@SpringBootTest (0) | 2022.04.30 |