오사카 프로그래머의 생존기

[Spring Test / DbUnit] 로컬에선 빠르던 테스트가 CI에서 느려진 진짜 이유 본문

기술 탐구

[Spring Test / DbUnit] 로컬에선 빠르던 테스트가 CI에서 느려진 진짜 이유

푸름군 2026. 4. 24. 01:02

[Spring Test / DbUnit] 로컬에선 괜찮던 통합 테스트가 CI에서 느려진 이유: 편리한 어노테이션의 숨겨진 비용

1. 이 기술을 파고든 배경 (Motivation)

5년 전, 한창 바쁘게 돌아가던 물류 시스템 프로젝트 현장이었다. 개발자 로컬 PC에서 쌩쌩하게 돌리던 테스트 코드들을 젠킨스(Jenkins) CI 환경에 올리면 전체적으로 수행 시간이 늘어나는 것은 어느 정도 피할 수 없는 상식으로 받아들이고 있었다. CI 서버의 자원은 한정적이고, 여러 파이프라인이 동시에 맞물려 돌아가니까 말이다.

하지만 젠킨스 콘솔 로그가 흘러가는 것을 무심히 지켜보던 중, 이상한 패턴을 하나 발견했다. 모든 테스트가 균일하게 느려지는 것이 아니었다. 유독 @DatabaseSetup이나 @ExpectedDatabase 같은 어노테이션이 붙은 데이터베이스 통합 테스트 구간에만 진입하면, 파이프라인의 진행이 턱턱 막히는 현상이 뚜렷하게 관찰되었다.

로컬 환경에서는 그저 '조금 무거운 테스트' 정도로 치부했던 녀석들이, CI 파이프라인 전체를 하염없이 지연시키는 주범이 되어 있었다. 결국 팀 내에서 이 원인 모를 병목 현상에 대한 조사를 의뢰받게 되었다.

2. 실무에서의 트러블슈팅: 5년 전의 헛발질 (Practical Use)

문제를 파악하기 위해 제일 먼저 테스트 코드를 까보았다. 수만 건의 데이터를 밀어 넣는 로직 자체는 1,000건 단위로 분할하여 처리되도록 짜여 있었기에, 코드의 겉모습만 봐서는 특별히 모난 곳이 없었다.

당시의 나는 엉뚱하게도 '데이터를 읽어오는 파일 포맷' 쪽에 화살을 돌렸다. VBA 매크로까지 동원해가며 기존의 CSV 형식 테스트 데이터를 XML로, 다시 TXT로 변환해가며 실행 속도를 측정하는 삽질을 반복했다. 당연하게도 실행 시간은 전혀 달라지지 않았다.

그때의 나는 이 마법처럼 편리한 어노테이션들이 정확히 테스트 사이클의 어느 시점에 동작하며, 어떤 트랜잭션 경계에서 쿼리를 날리는지까지 깊게 파고들지 못했다. 당장 라이브 환경에 배포되는 비즈니스 로직의 치명적인 버그는 아니었기에, 결국 업무 영향도가 크지 않다는 핑계로 'CI 환경의 자원 부족 문제'라는 찜찜한 결론을 내린 채 덮어두고 말았다.

3. 핵심 원리 파헤치기: 5년 뒤 맞춰진 퍼즐 (Deep Dive)

최근 AI 도구들을 활용해 당시의 아키텍처와 상황을 복기하며 대화를 나누던 중, 그때는 보이지 않았던 구조적 문제들이 선명하게 드러났다.

로컬과 젠킨스에서 체감 속도가 극심하게 달랐던 진짜 원인은 파일 포맷 따위가 아니었다. 정답은 '네트워크 Latency'와 프레임워크가 감춰둔 '숨겨진 I/O 비용'의 결합이었다.

① Setup 비용: 단건 JDBC 왕복의 누적
@DatabaseSetup은 테스트 실행 전 XML이나 CSV 데이터를 읽어 DB에 적재한다. 문제는 DbUnit 기반의 이 기능이 대량의 데이터를 꽂아 넣을 때, 내부적으로 JDBC 단건 Insert를 반복한다는 점에 있었다.

// 5년 전 내 머릿속의 이상적인 동작 (Batch Insert)
insert into target_table (col1, col2) values (1, 'A'), (2, 'B'), ... (10000, 'Z');

// 실제 프레임워크 이면의 동작 (Single Insert 낭비)
for (Row row : dataset) {
    // 1만 번의 네트워크 핑퐁(Round-trip) 발생
    insert into target_table (col1, col2) values (row.val1, row.val2); 
}

로컬 PC에 띄워둔 로컬 DB에서는 이 네트워크 왕복 비용이 제로에 가깝다. 건건이 쿼리를 날려도 눈에 띄게 느리지 않다. 하지만 젠킨스 서버에서 원격 물리 DB로 1만 건의 쿼리를 하나씩 날리는 순간, 그 미세한 네트워크 지연(Latency)이 1만 번 누적되며 끔찍한 병목으로 둔갑하는 것이다.

② Expected 비용: 무자비한 메모리 풀스캔
결과 검증을 위해 사용했던 @ExpectedDatabase의 작동 방식은 더 치명적이었다. 이 어노테이션은 DB에 적재된 결과물과 기대 데이터를 비교하기 위해, 대상 테이블의 데이터를 DB에서 풀스캔(Full Scan)하여 애플리케이션(CI 워커) 메모리로 전부 퍼 올린 뒤 비교를 수행한다. 젠킨스처럼 여러 테스트가 병렬로 자원을 쪼개 쓰는 환경에서 이 방식은 CPU와 메모리 리소스를 극한으로 소모하게 만든다.

4. 회고 (Retrospective)

5년 전의 나는 데이터가 출발하는 '파일(CSV/XML)'만 뚫어져라 쳐다보며 원인을 찾으려 했다. 하지만 진짜 문제는 단 몇 줄만으로 복잡한 데이터베이스 상태를 제어해 주던 편리한 어노테이션 뒤에 거대한 네트워크 통신과 I/O 비용이 숨어 있었다는 사실이다.

결국 이 문제는 단순히 '어쩌다 한 번 느려진 테스트'가 아니었다. '실행 환경이 달라졌을 때(Local vs Remote) 비용이 기하급수적으로 폭발하는 구조'를 당시의 내가 객관적으로 구분해서 보지 못하고 있었다.

강력한 추상화 도구는 개발의 생산성을 높여주지만, 그 마법이 실제로 물리적인 인프라 위에서 어떻게 동작하는지 꿰뚫어 보지 못하면 결국 엉뚱한 곳에서 삽질을 하게 된다. 그때의 찜찜했던 원인 불명의 리포트를 5년이 지난 지금에야 비로소 제대로 마침표 찍게 되었다.