통합 테스트 스캐폴드: 마이크로서비스 아키텍처를 위한 중앙 집중식 테스트 접근법
Source: Dev.to
현대 테스트의 사각지대
일반적인 마이크로서비스 구성에는 이미 다음이 포함됩니다:
- Unit tests → 견고하고 빠름
- Contract tests → 스키마 호환성 보장
- E2E tests → 전체 UI 워크플로우 커버
그럼에도 불구하고 실패는 어디서 발생할까요?
단일 서비스에서 발생하는 것이 아니라 서비스 간에 발생합니다.
예시
- 여러 변환을 거치면서 데이터 정밀도가 손실됨
- 특정 서비스 조합에서 OAuth 토큰이 실패함
- 실제 부하 패턴에서 레이트‑리미터가 다르게 동작함
- 이벤트‑드리븐 흐름이 메시지를 조용히 드롭함
- 개별적으로는 정상 작동하지만 체인으로 연결되면 트랜잭션이 깨짐
- 캐시 무효화가 서비스 경계 너머로 전파되지 않음
Unit test는 이를 볼 수 없고, Contract test도 이를 볼 수 없습니다. 그리고 E2E test? UI를 통해 전체 시스템을 검증하지만 너무 얕고, 너무 느리며, 가장 중요한 통합 지점을 종종 모킹합니다.
이때 통합 API 테스트가 필요합니다.
중앙 집중식 테스트 저장소가 필요한 이유
여기서 제안하는 해결책은 central‑testing‑repository 입니다: 배포 후 서비스 간 동작을 검증하는 통합 테스트 전용 단일 저장소입니다.
장점
- 다년간의 마이그레이션이 끝난 뒤가 아니라 지금 시스템 수준의 신뢰성을 제공합니다.
- 교차 서비스 실패에 대한 조직적 소유자(예: QA 또는 플랫폼 팀)를 지정합니다.
- 시스템이 어떻게 상호 작용해야 하는지를 문서화하고, 실패 테스트가 문제 서비스를 정확히 지목합니다.
트레이드‑오프
- 테스트가 서비스 코드와 별도로 관리됩니다.
- 팀 간 협업이 필요합니다.
- 서비스와 테스트 사이에 어느 정도 결합도가 생깁니다.
이러한 비용에도 불구하고, 혼합된 모놀리식‑마이크로서비스 환경, 다양한 기술 스택, 분산된 소유권 현실에 부합하는 접근 방식입니다.
모노레포 인사이트
central‑testing‑repository는 본질적으로 가벼운 monorepo이지만, 오직 통합 테스트만을 위해 존재합니다.
- 완전한 monorepo를 도입한 기업(Google, Meta 등)은 교차 서비스 테스트를 거의 비용 없이 얻습니다: 모든 서비스가 한 곳에 있기 때문에 여러 서비스를 아우르는 테스트를 작성하는 것이 단순히 테스트가 되기 때문입니다.
- 대부분의 기업은 조직·툴링 비용 때문에 전체 monorepo로 전환할 수 없습니다.
central‑testing‑repository는 전체 마이그레이션 없이도 monorepo의 테스트 이점을 제공합니다:
- 모든 서비스 API를 한 눈에 볼 수 있는 단일 저장소.
- 실제 배포된 서비스에 대해 테스트 실행.
- 분산 시스템을 연결하는 “접착제” 역할을 담당.
예, API를 변경할 때는 두 개의 PR이 필요합니다(하나는 서비스, 하나는 테스트). 이 협업 순간이 바로 파괴적 변경을 프로덕션에 도달하기 전에 잡아내는 시점입니다.
그럼 계약 테스트가 이걸 잡지 않나요?
Contract testing과 integration testing은 서로 다른 문제를 해결합니다.
| Contract testing | Integration testing | |
|---|---|---|
| 스키마가 일치하나요? | ✅ | ✅ |
| 값이 정확한가요? | ❌ | ✅ |
| 부수 효과가 발생하나요? | ❌ | ✅ |
| 서비스 간 일관성이 있나요? | ❌ | ✅ |
| 타이밍/순서가 맞나요? | ❌ | ✅ |
- Contract testing은 구조를 검증합니다: 서비스가 서로 통신할 수 있나요?
price라는 필드가 있나요? 숫자 타입인가요? 응답 스키마가 소비자가 기대하는 것과 일치하나요? - Integration testing은 동작을 검증합니다: 서비스가 올바르게 함께 작동하나요?
19.99라는 값이 여러 변환을 거쳐 유지되나요? Cart와 Invoice 간 세금 계산이 일치하나요? 부수 효과(재고 예약, 이메일 전송)가 실제로 발생했나요?
테스트를 통과하지 못하고 프로덕션에 넘어가는 실제 사례
이러한 버그는 테스트 스위트가 이를 찾지 못하기 때문에 프로덕션에 배포됩니다.
사례 1: 세금 계산 불일치
- Cart Service는
Decimal타입 사용: tax = €3.80 - Invoice Service는
float사용: tax = €3.79 - 3개 아이템 경우: Cart은 €11.40, Invoice는 €11.39
Contract test: 둘 다 {tax: number} 반환 → 통과.
Integration test: 모든 서비스가 동일한 세금 금액을 반환하는지 검증 → 실패.
사례 2: 날짜 포맷 해석 차이
- **Order Service (Java)**는
"01/02/2024"(미국식 1월 2일) 직렬화 - **Shipping Service (Go)**는 2월 1일(EU식)으로 파싱
Contract test: 둘 다 { "orderDate": "string" } 처리 → 통과.
Integration test: 주문을 생성하고 Shipping Service가 올바른 날짜를 파싱했는지 검증 → 실패.
사례 3: 상태 문자열 대소문자 차이
- **Payment Service (C#)**는
status: "COMPLETED"반환 - **Notification Service (Node.js)**는
"completed"를 기대
Contract test: 둘 다 { "status": "string" } 처리 → 통과.
Integration test: 결제를 처리하고 알림이 트리거됐는지 검증 → 실패.
각 경우 모두 계약 테스트는 통과하지만 실제 동작은 깨집니다.
왜 E2E 테스트가 이 빈틈을 메우지 못하는가
팀에 통합 API 테스트가 없을 경우, 모든 백엔드 검증을 E2E 테스트에 집어넣으려 합니다. 이는 여러 문제를 야기합니다:
- 속도: E2E 테스트는 느려서 빈번한 피드백이 비현실적입니다.
- 불안정성: UI 기반 테스트는 타이밍이나 환경 문제로 인한 오탐이 많습니다.
- 얕은 커버리지: 검증이 필요한 바로 그 통합 지점을 종종 모킹합니다.
- 유지 보수 부담: UI 혹은 프론트엔드 변경이 무관한 테스트 실패를 유발합니다.
결과적으로 E2E 테스트는 백엔드 통합 검증을 위한 주요 도구가 되기 어렵고, 전용·중앙 집중식 통합 테스트 레이어의 필요성을 더욱 강조합니다.