pile·
백엔드·flexflex·

DDL이 코드 밖에서 온다면, 테스트 DB 구성을 빌드 안에 선언한다

flex 백엔드팀이 스키마의 진실이 애플리케이션 코드가 아니라 Liquibase changelog에 있는 조직에서 테스트 DB 구성을 Gradle 빌드 선언으로 끌어올린 과정이다. Hibernate가 엔티티에서 테이블을 파생시키지 않으므로 ddl-auto로 테스트 스키마를 만드는 길 자체가 막혀 있었고, 더 큰 문제는 어떤 테스트 DB가 어떤 스키마들로 서 있어야 하는지가 빌드 어디에도 없다는 점이었다. v1(changelog 병합)과 v2(테스트 DB variant 선언 + 덤프 캐시)로 이어진 두 번의 전환을 짚는다.

핵심 포인트
  • 모듈 의존은 빌드에 남지만 스키마 의존은 남지 않는다. B 모듈 테스트를 단독 실행하면 relation payment_account does not exist로 깨지고, 그 지식은 위키나 먼저 겪은 사람 머릿속에만 있다.
  • CI가 프로젝트를 병렬로 돌리다 순서가 흔들리면 간헐적으로만 실패하고 flaky 재시도로 덮여 원인이 오래 숨는다.
  • v1은 여러 모듈 changelog를 모아 정적 schema.sql 하나로 합쳤다. 출처는 changelog가 됐지만 실제 마이그레이션 경로를 밟지 않아 중간 상태·제약조건 검사·엔진 고유 반응을 재현하지 못했다.
  • v2는 root 빌드가 테스트용 DB variant를 정의하고 그 variant가 자신을 세울 스키마 프로젝트를 나열한다. 격리는 variant마다 전용 데이터베이스를 쓰는 데서 나온다.
  • 반복 재생 비용은 실제 엔진에 적용한 결과를 덤프로 고정해 Gradle 빌드 캐시에 넣어 줄인다.
상세 정리
  • 전제: 이 조직에서 지금 시점의 전체 스키마가 무엇인지는 코드 어디에도 통째로 적혀 있지 않고 changelog를 다 재생해야 나온다. 엔티티는 그 스키마를 자바에서 바라보는 한쪽 창일 뿐이다.
  • 진짜 문제는 출처가 코드가 아니라는 사실이 아니라, 그 DDL이 세우는 테스트 DB의 구성이 빌드 어디에도 없다는 점이었다. B 모듈의 DDL이 A 모듈 테이블을 전제한다는 사실이 사람의 기억과 실행 순서 관례에만 남는다.
  • 1단계 배경: Liquibase는 원래 운영 마이그레이션 도구로 오래 써서 changelog가 몇 해에 걸쳐 두꺼워진 상태였고, 이 시절 테스트는 그 진실과 거의 무관하게 돌았다.
  • v1 구현: liquibaseChangelog(project(":my-domain"))로 다른 모듈의 changelog를 산출물처럼 끌어오고 FileMergeTask로 이어 붙여 build 디렉터리에 schema.sql 하나를 만든 뒤, Spring 통합 테스트가 그 파일을 읽어 스키마를 세웠다. Testcontainers도 실제 마이그레이션 실행도 없었다.
  • v1의 한계: 테스트가 읽는 것은 마이그레이션 경로의 결과물을 한 번 요약해 옮겨 적은 파일이었다. changelog가 순서대로 적용되며 만드는 중간 상태나 제약조건 검사는 재현되지 않았다.
  • 두 번째 전환: 정적 파일 대신 프로덕션과 같은 엔진을 Testcontainers로 매번 띄우고 그 안 실제 DB에 Liquibase로 DDL을 적용했다. 테스트가 밟는 길이 프로덕션과 같아져 엔진 고유 예약어, 컬럼 기본값, 순차 적용 시 걸리는 제약조건까지 검증할 수 있게 됐다.
  • 비용이 드러난 방식: 방식이 정확해서 그 위에 쌓는 DB 테스트가 계속 늘었고, 수백 개가 되자 참을 만하던 비용이 병목이 됐다. 컨테이너 기동만 테스트당 몇 초씩 들고 changelog가 수백 개면 매번 전체를 재생하는 것이 가장 큰 병목이었다.
  • 전환점 해석: 비용이 문제가 된 것은 방식이 틀렸다는 신호가 아니라 옳아서 널리 쓰이게 됐다는 신호로 봤다. 그래서 방향을 바꾸는 대신 감당 가능한 비용으로 계속 굴리는 쪽으로 구조를 다시 짰다.
  • v2 선언 형태: root 빌드에 testDatabase("testdb")를 정의하고 그 안에 schemas로 project(":payment")·project(":order")를, engine과 DB 생성·계정·권한 초기화를 함께 적는다. DSL은 아직 다듬는 중이라 의사코드로 제시했다.
  • v1과 v2의 층위 차이: v1은 소비 모듈이 다른 모듈 changelog를 자기 의존성으로 가져오는 선언이고, v2는 root에 정의된 테스트 DB variant가 자신을 구성할 스키마 프로젝트를 나열하는 선언이다. "모듈이 무엇을 가져오나"가 "이 테스트 DB를 무엇으로 세우나"로 바뀐다.
  • 조립: 통합 테스트가 여러 모듈을 함께 검증해야 하면 플러그인이 variant에 나열된 각 스키마 프로젝트의 changelog를 하나의 master changelog로 엮어 전용 DB에 적용한다. 헥사고날에서 여러 모듈 스키마가 결국 한 물리 DB에 합쳐지는 현실을 반영한다.
  • 격리: variant마다 전용 데이터베이스를 쓰고 이름도 variant가 정한다. 한 컨테이너를 공유해도 DB가 다르니 DDL 이력·테이블·테스트 데이터가 섞이지 않아, 병목이던 Gradle 프로젝트 단위 병렬 실행이 안전해진다.
  • 덤프 캐시: changelog 적용 결과를 엔진 덤프 형식으로 고정해 Gradle 빌드 캐시에 넣는다. changelog·베이스 이미지·초기화 SQL처럼 스키마에 영향을 주는 입력이 바뀌면 다시 적용하고 안 바뀌면 검증된 덤프를 복원해, 느린 경로와 빠른 경로가 같은 스키마로 수렴한다.
  • 결정성 확보: 같은 스키마라도 덤프를 두 번 뜨면 헤더 생성 시각, 시퀀스 현재값, 오브젝트 출력 순서가 달라져 캐시가 빗나간다. 이 비결정 요소를 정규화하는 처리는 편의 기능이 아니라 수렴을 지키기 위한 설계 결정이다.
  • 우회로 비교 1: 공유 DB에 트랜잭션을 열고 끝에 롤백하는 방식은 재시딩이 없어 빠르지만 롤백이 데이터 변경만 되돌리고 DDL은 대개 범위 밖이라, 새 DDL 자체를 검증해야 하는 상황에서는 애초에 대상이 아니었다.
  • 우회로 비교 2: 테스트에서만 ddl-auto를 켜거나 인메모리 DB로 바꿔 끼우는 방식은 스키마가 코드에서 나오지 않는다는 전제와 정면으로 부딪혀 선택지에 오르지 못했다.
  • 우회로 비교 3: 테스트마다 Testcontainers로 실제 마이그레이션을 돌리는 방식은 실제로 거쳐온 단계이며 방향은 옳았다. 다만 전체 재생 비용이 드러나 v2로 넘어왔다.
  • 공통점 지적: 세 우회로 모두 테스트 DB의 구성을 빌드에 남기지 못한다. Liquibase 멀티모듈 표준 안내조차 의존이 있으면 master changelog에서 선행 모듈을 먼저 include하도록 사람이 손으로 맞추라고 정리한다. 그 구성을 root 빌드 variant 선언에 맡긴 것이 차이점이다.
왜 읽나마이그레이션 도구가 스키마의 진실인 멀티모듈 백엔드에서 통합 테스트 DB 구성을 관례가 아니라 빌드 산출물로 관리하려는 팀에게 단계별 트레이드오프가 정리돼 있다.
flex
flex 블로그
원문은 여기서 이어서 읽을 수 있어요
원문 읽기
읽음 (0)

이 글과 비슷한

  1. 백엔드·twilio-engTwilio Engineering·

    Programmable Messaging에서 Verify API로 마이그레이션하기

    Twilio의 Programmable Messaging API로 자체 OTP 솔루션을 운영하던 서비스가 Verify API로 전환하는 방법을 코드 예시와 함께 설명한다. Verify는 OTP 전송·검증을 위한 전용 API로, 전화번호 구매, 토큰 생성, DB 저장·만료 관리를 내부에서 처리해 개발자가 직접 구현할 코드를 크게 줄인다.

    요약 이어보기
    #authentication#twilio#sms+2
  2. 백엔드·포스타입포스타입·

    포스타입이 개인화 추천을 하는 방법 2부

    포스타입 백엔드 엔지니어가 벡터 기반 개인화 추천 시스템을 실제 운영하며 맞닥뜨린 성능 장애와 용량 문제를 해결한 과정을 담은 2부다. 수백만 개의 벡터 KNN 검색이 피크 시간대에 전체 Elasticsearch 검색 성능을 흔드는 문제부터 클러스터 OOM 사태까지, 쿼리 최적화와 인프라 분리 두 가지 경로로 근본 해결에 이른다.

    요약 이어보기
    #elasticsearch#vector-search#recommendation-system+2
  3. 백엔드·포스타입포스타입·

    포스타입이 개인화 추천을 하는 방법 1부

    포스타입이 태그 기반 추천의 한계를 극복하고 벡터 임베딩 기반 개인화 추천 시스템을 구축한 과정을 담은 1부다. 유사한 콘텐츠가 다른 용어를 쓰거나 동일한 태그가 전혀 다른 톤의 콘텐츠를 가리키는 문제를 임베딩 벡터로 해결하고, OpenSearch의 HNSW ANN 검색으로 수백만 벡터를 실시간 검색하는 시스템을 구축해 구매율 15% 향상을 달성했다.

    요약 이어보기
    #opensearch#vector-search#recommendation-system+2