테스트 커버리지 80%
내부 품질 게이트가 요구한 코드 커버리지 80% 기준을 KakaoPay v2에서 어떻게 달성했는지 기록한다. 단순히 커버리지 수치를 올린 이야기가 아니라, 커버리지 목표가 코드 구조 자체를 어떻게 바꾸게 했는지, 그 과정에서 배운 테스트 가능한 설계(testable design)에 대한 교훈을 남긴다.
왜 커버리지 숫자보다 설계가 중요했나
결론부터 말하면, 구현을 먼저 하고 마지막에 커버리지를 맞추려 하면 리팩토링이 불가피하다. 이 경험 이후 test-first 또는 최소한 test-aware 설계를 초기 아키텍처에 반영하는 습관이 생겼다.
무엇이 테스트를 어렵게 만들었나
내부 품질 게이트는 새로 작성하거나 변경한 코드에 대해 80% 이상의 테스트 커버리지를 요구했다. 이 기준을 충족하지 못하면 빌드가 실패하고, 배포할 수 없다.
KakaoPay v2는 3개월(2023.07~10) 만에 49건의 커밋으로 완성한 서비스다. 핵심 기능 구현에 집중하느라 테스트 코드 작성이 뒤로 밀렸고, 2023년 8월 말 커버리지를 측정하니 80%에 미달했다. 단순히 테스트를 추가하는 것으로는 해결되지 않았다. 코드 구조 자체가 테스트하기 어렵게 되어 있었기 때문이다.
핵심 체크포인트
- Communicator: 외부 API(KakaoPay API) 호출을 담당. HTTP 요청/응답만 처리하고, 비즈니스 로직은 없다.
- Processor: 비즈니스 로직을 담당. Communicator에서 받은 응답을 가공하고, 상태 전이를 판단한다.
- Service: 전체 흐름을 조율(orchestrate). Communicator 호출 → Processor 처리 → DynamoDB 저장의 순서를 관리한다.
- KakaoPay API 타임아웃 — Communicator에서 타임아웃 발생 시 적절한 에러 응답 반환
품질 게이트를 통과할 구조로 어떻게 바꿨나
가장 먼저 발견한 문제는 JUnit 테스트 리포트 경로가 내부 품질 게이트가 기대하는 위치와 달랐다는 것이다. Gradle의 기본 테스트 리포트 경로는 build/test-results/test/이지만, 품질 게이트가 읽는 경로가 프로젝트 설정에 따라 달랐다. 이 불일치 때문에 테스트가 통과해도 커버리지가 0%로 표시되는 상황이 발생했다.
경로를 맞추는 것은 빌드 설정 한 줄 수정이었지만, 이 문제를 발견하기까지 커버리지가 왜 올라가지 않는지 원인을 추적하느라 시간이 걸렸다. 품질 게이트가 어떤 산출물을 읽는지 이해하지 못하면 쉽게 놓치는 부분이었다.
테스트 커버리지가 낮았던 근본 원인은 외부 호출, 상태 처리, 저장 로직이 테스트하기 쉬운 경계로 충분히 정리되지 않았기 때문이다. 구현을 빠르게 밀어붙이던 초기에는 정상 흐름을 완성하는 데 집중하다 보니, 외부 API 호출과 상태 전이, DynamoDB 저장이 한 번에 이어지는 구간이 많았고 실패 시나리오를 독립적으로 검증하기가 쉽지 않았다. 특히 외부 결제 서비스를 호출하는 구조이다 보니, 직접 연동을 확인하며 쌓인 샘플 요청/응답을 바탕으로 테스트 코드를 조금씩 다듬어 가는 과정이 필요했다. 이런 구조에서는 단위 테스트를 추가하더라도 모킹 범위가 넓어지고, 테스트가 세부 구현에 쉽게 끌려가게 된다.
이를 해결하기 위해 세 계층으로 분리했다:
리포트 경로부터 맞췄다
테스트 결과가 품질 게이트에 제대로 집계되도록 입력 경로를 먼저 정리했다.
의존성이 강한 코드를 계층으로 분리했다
외부 호출, 상태 전이, 저장 로직을 나눠 단위 테스트 가능한 구조로 바꿨다.
실패 시나리오 테스트를 보강했다
타임아웃과 예외 경로를 포함해 실제로 깨지기 쉬운 케이스를 테스트로 묶었다.
품질 게이트 예시 코드
커버리지 목표는 테스트 케이스 확장과 품질 게이트 설정을 같이 둘 때 안정적으로 유지된다.
jacocoTestCoverageVerification {
violationRules {
rule {
limit {
minimum = 0.80
}
}
}
}@Test
void shouldReturnFailedWhenVendorTimeout() {
when(vendor.approve(any())).thenThrow(new TimeoutException());
assertThat(processor.approve(command).status()).isEqualTo(Status.FAILED);
}80퍼센트 숫자보다 중요했던 것
이 작업이 남긴 기준은 커버리지를 채웠다는 사실보다, 테스트 가능성이 설계의 일부여야 한다는 점이었다. 외부 통신과 상태 전이가 많은 서비스일수록 나중에 테스트를 덧붙이는 비용이 커지기 때문에, 품질 기준은 구현 마지막이 아니라 구조를 정할 때부터 들어와야 했다.
다음 글에서는 이런 품질 기준을 지키는 과정에서도 실제 운영에서는 작은 문자열 처리 버그가 어떻게 큰 장애로 이어질 수 있는지 살펴본다.