마이크로서비스 아키텍처의 출현은 전통적인 ACID 보장이 불가능한 서비스 경계를 넘는 분산 트랜잭션을 관리하기 위해 사가 패턴이 필요하게 했습니다. 역사적으로 테스트는 즉각적인 일관성을 가진 모놀리틱 데이터베이스에 의존했지만, 현대의 다중 데이터 저장소 시스템은 비동기 워크플로 및 보상 로직의 검증이 필요합니다. 핵심 문제는 기존의 통합 테스트가 동기 응답을 가정하고 있으며, 이는 경합 조건, 네트워크 분할 및 일부 사가 참가자가 커밋할 때 다른 참가자가 실패하는 모호한 상태를 포착하지 못한다는 것입니다.
해결책은 테스트 하네스에 통합된 Chaos Engineering 접근 방식이 필요합니다. Testcontainers를 사용하여 격리된 Docker 네트워크 내에서 실제 PostgreSQL, MongoDB 및 Redis 인스턴스를 조정하는 프레임워크를 설계합니다. Toxiproxy를 서비스 간 프로그래밍 가능한 TCP 프록시로 도입하여 정확한 사가 단계에서 지연, 대역폭 제약 및 네트워크 분할을 주입합니다. 정적 대기 대신 폴링 기반 비동기 단언을 위해 Awaitility를 사용하고, 분산 추적을 위해 Jaeger를 통합하여 정확한 실행 경로를 재구성합니다. 보상의 정확히 한 번만 수행되도록 검증하기 위해 UUID 기반의 멱등성 키 추적을 구현하고, 모든 지속성 계층의 상태를 스냅샷하여 불변성을 검증하는 GlobalConsistencyValidator를 구축합니다.
맥락: 다국적 전자상거래 플랫폼은 재고 서비스(PostgreSQL), 결제 서비스(MongoDB, 트랜잭션 로그 용) 및 배송 서비스(Elasticsearch)를 포함하는 이벤트 기반 사가를 통해 주문을 처리했습니다. 이 아키텍처는 Java 기반 마이크로서비스 간의 조화를 위해 Apache Kafka를 사용했습니다.
문제 설명: 피크 트래픽 동안 네트워크의 간헐성으로 인해 결제 처리가 성공하였으나 재고 예약이 실패하여 보상이 촉발되었습니다. 그러나 보상 로직은 초기 환불 요청이 타임아웃될 경우 중복 환불 요청이 발생하는 중요한 경쟁 조건을 포함하고 있어 멱등성 계약을 위반했습니다. 또한, 다중 데이터 저장소 간의 최종 일관성 지연으로 인해 즉각적인 재고 복원을 단언하는 기존 테스트에서 잘못된 양성 결과가 발생하여 불안정한 CI/CD 파이프라인과 함께 고객이 사용 불가능한 상품에 대해 과금당하는 결함을 초래했습니다.
접근 방법 1: 고정 지연을 가진 UI 기반의 종단 간 테스트
우리는 초기에는 Selenium WebDriver를 사용하여 사용자 체크아웃 흐름을 시뮬레이션하고 비동기 처리를 기다리기 위해 Thread.sleep(5000)을 삽입하는 것을 고려했습니다.
장점: 구현이 간단하고 전체 사용자 여정을 포함하며 서비스 코드 변경이 필요 없습니다.
단점: 매우 취약하며; 5초는 부하 하에서는 충분하지 않고 유휴 기간에는 과도합니다. 네트워크 실패를 정확한 사가 단계에서 주입할 수 없어 특정 경쟁 조건을 재현하기 불가능합니다. 이 접근 방식은 서비스 간 HTTP 통신 패턴이나 데이터베이스 상태 전환에 대한 가시성을 제공하지 않습니다.
접근 방법 2: 인메모리 데이터베이스를 갖춘 모킹된 단위 테스트 두 번째 옵션은 Mockito를 사용하여 모든 외부 서비스 호출을 모킹하고 각 서비스의 단위 테스트에 H2 인메모리 데이터베이스를 사용하는 것이 었습니다. 장점: 실행 시간이 10초 이내이고 인프라 종속성이 없으며 격리된 상태에서 결정론적 결과를 제공합니다. 단점: 실제 환경에서 직렬화 문제, TCP 소켓 타임아웃 동작 또는 PostgreSQL에 존재하지만 H2에는 없는 데이터베이스 특정 잠금 메커니즘을 감지하지 못했습니다. 멱등성 경쟁 조건은 실제 네트워크 패킷 동작과 연결 풀 소모에서만 나타났으며 모킹으로는 복제할 수 없습니다.
접근 방법 3: 실제 인프라를 통한 혼돈 오케스트레이션 (선택됨) 우리는 JUnit 5와 Testcontainers를 사용하여 전용 테스트 하네스를 구현했습니다. 각 서비스는 Toxiproxy가 관리하는 격리된 Docker 컨테이너에서 실행되었습니다. API 진입점에 대해 RestAssured를 사용하고, 외부 결제 처리기의 멱등성 동작을 시뮬레이션하기 위해 WireMock을 사용했습니다. 장점: 특정 사가 단계에서 정확한 오류 주입을 가능하게 했습니다 (예: 결제 커밋 후 재고 확인 전 연결을 차단). Awaitility는 고정 지연 없이 최종 일관성을 동적으로 기다릴 수 있게 해주었습니다. Jaeger 추적은 보상 경로를 검증하기 위한 실행 경로의 포렌식 분석을 제공했습니다. 단점: 초기 설정 복잡성 및 리소스 요구 사항이 높으며 (로컬 실행을 위한 최소 8GB RAM 필요), 단위 테스트에 비해 초기 부트스트랩 시간이 길어졌습니다.
결과: 프레임워크는 보상 재시도가 중복 키에 대한 적절한 HTTP 409 Conflict 처리가 부족한 멱등성 버그를 감지했습니다. 환불 요청 제출 전에 Redis 멱등성 키를 확인하도록 로직을 수정한 후, 생산 현장에서의 중복 요금은 0으로 감소했습니다. 테스트 실행 시간은 8분 (불안정한 UI 테스트)에서 45초 (표적 통합 테스트)로 단축되었으며 실패 시나리오에 대한 커버리지가 300% 향상되었습니다.
네트워크 실패가 모호한 요청 결과를 유발할 때, 보상 트랜잭션이 멱등성을 유지한다는 것을 어떻게 검증합니까?
후보자들은 보통 최종 계좌 잔고만 단언하고, 하류 시스템이 정확히 하나의 요청을 수신했다는 중요한 검증을 놓치곤 합니다. 올바른 구현은 혼돈 주입 전에 UUID 멱등성 키를 캡처한 다음 WireMock의 verify(exactly(1), postRequestedFor()) 메서드를 사용하여 결제 게이트웨이에 정확히 하나의 일치하는 요청이 도달했는지 확인하는 것을 포함합니다. 또한 Saga Orchestrator의 상태 기계 로그를 검사하여 전이가 COMPENSATING -> COMPENSATED로 진행되고 중간 FAILED 상태가 없음을 확인하여 불필요한 알림을 유발하지 않도록 해야 합니다. 이것은 요청 바이트가 전송된 후 응답 바이트가 도착하기 전에 연결을 끊는 TCP 수준의 프록시 제어가 필요하며, 이는 멱등성 처리를 테스트하는 정확한 모호한 타임아웃 조건을 생성합니다.
서로 다른 복제 지연을 가진 이기종 데이터 저장소에서 최종 일관성을 단언할 때 테스트의 불안정성을 방지하는 전략은 무엇입니까?
대부분의 후보자들은 고정 타임아웃으로 폴링을 제안합니다. 강력한 해결책은 Awaitility와 함께 100ms에서 시작하여 99번째 백분위수 생산 지연(예: 3초)으로 제한된 지수 백오프를 사용하는 것입니다. 중요한 것은 테스트에서 Global Clock 또는 Vector Clock 메커니즘을 구현하여 사가 시작 전에 PostgreSQL, MongoDB 및 Redis의 논리적 타임스탬프를 스냅샷하여 읽기 작업이 사가 시작 시간과 같거나 그보다 큰 타임스탬프를 반환하도록하는 단언을 검증하는 것입니다. CQRS 시나리오에서는 데이터베이스를 폴링하는 대신 테스트에 내장된 Debezium을 사용하여 CDC 이벤트에 구독하여 대기 시간을 초 단위에서 밀리초 단위로 단축하고 테스트 단언과 데이터 복제 사이의 경합 조건을 제거합니다.
일부 사가 참가자가 커밋되고 다른 참가자는 보류 상태에서 부분 실행 상태를 어떻게 탐지합니까? 생산 관측 도구에 접근하지 않고?
후보자들은 종종 테스트 하네스에서 접근 가능한 In-Process Saga 추적 또는 Saga Audit Logs의 필요성을 놓칩니다. 솔루션은 gRPC 또는 HTTP 호출을 참가 서비스에 가로채는 Sidecar 패턴을 테스트 컨테이너에 주입하는 것을 요구합니다. 각 참가자의 상태(PENDING, COMMITTED, ABORTED)를 추적하는 Saga State Matrix를 테스트 하네스에 유지합니다. Toxiproxy가 분할을 주입할 때 이 매트릭스를 쿼리하여 커밋된 참가자가 예상된 실패 전 상태와 일치하는지, 그리고 중단된 참가자가 어떤 부작용도 보이지 않는지 확인합니다. 보상이 커밋된 참가자에 대해서만 실행되도록 하여 리소스가 실제로 예약되지 않은 트랜잭션에 대해 해제되지 않도록 하기 위해 Jaeger 범위 태그에서 JSONPath 단언을 사용합니다.