Replay Testing
가장 값진 테스트 케이스는 이미 로그·웨어하우스·데이터레이크·이벤트 스트림 안에 있다
핵심 개념
Replay testing은 프로덕션 히스토리를 실행 가능한 테스트 자산으로 만드는 것이다. 서비스의 결정 로직을 요청 경로에서 분리해 라이브러리로 만든 뒤, 수개월~수년치 히스토리 입력을 그 로직에 대량으로 통과시킨다.
출력은 더 이상 단일 응답이 아니라 쿼리·조인·diff·집계·리뷰가 가능한 데이터셋이다. 그래서 대화가 “테스트가 통과했다”에서 “프로덕션 형태의 트래픽 전체에 후보 구현을 replay했고 정확히 어디서 행동이 바뀌었는지 여기 있다”로 바뀐다.
어떤 서비스에 필요한가
입력의 롱테일이 행동을 결정하는 서비스. 하나의 룰이 아니라 여러 조건의 상호작용에서 행동이 나오는 시스템이다. 대표적으로 결제 비용 추정, 가격·과금, 자격 판정, 고객 대면 설명.
단위 테스트는 개별 룰을 검증하고 통합 테스트는 대표 흐름 몇 개를 검증한다. 하지만 팀이 가장 알고 싶은 질문에는 답하지 못한다:
룰 하나, rate table 하나, 의존성 하나, 자격 조건 하나를 바꾸면 실제 히스토리 트래픽 분포 전체에서 무슨 일이 일어나는가?
AI 시대에 더 중요해지는 이유
AI 도구가 리팩터·마이그레이션·후보 구현을 그 어느 때보다 빠르게 만들어준다. 하지만 금전 이동·과금·가격·자격에 영향을 주는 시스템에서는 더 빠른 코드 변경일수록 더 강한 평가가 필요하다. 국소적으로 맞아 보이는 변경이 입력 분포의 좁지만 중요한 부분에서 행동을 바꿀 수 있기 때문이다.
replay harness가 있으면 질문이 주관적이지 않게 된다: 행동이 바뀌었는가, 어디서 바뀌었는가, 그 변화는 수용 가능한가.
세 가지 활용
1. 회귀 테스트
현재 구현과 후보 구현을 같은 히스토리 입력에 돌려 비교. 완벽 일치가 목표가 아니다 — 일부 diff는 의도된 변경이고, 일부 diff는 기존 구현의 버그를 드러낸다. 가치는 차이가 프로덕션 도달 전에 보인다는 것.
2. What-if (미래 룰 변경 모델링)
많은 중요한 변경은 코드 리팩터가 아니라 외부 룰 변경이다. 어려운 문제는 새 룰을 인코딩하는 게 아니라 실제 히스토리 트래픽에서 어떻게 행동할지 추정하는 것이다.
일반 분석과의 결정적 차이: 과거에 무슨 일이 있었는지를 묻는 게 아니라, 의사결정 환경이 달랐다면 서비스가 무엇을 반환했을까를 묻는다.
3. Golden dataset (PR용 경량 버전)
공통 흐름, 자격 경계, 과거 인시던트, 작은 입력 변화가 큰 결과 차이를 내는 케이스를 담은 입력/기대출력 쌍. CI에서 매 PR마다 실행하고 예기치 않은 차이가 있으면 빌드 실패. 인시던트가 놓친 엣지 케이스를 드러낼 때마다 보존되므로 살아있는 스펙이 된다.
테스팅 스택에서의 위치
단위 테스트 의도적으로 구성한 작은 케이스
통합 테스트 시스템 간 상호작용
golden dataset 알려진 서비스 레벨 예시 (현실적 입력)
히스토리 replay 실제 프로덕션 분포 — 팀이 설계하지 않은 입력까지
replay는 아래 레이어를 대체하지 않고 위에 얹힌다. 성숙한 시스템일수록 히스토리 입력의 롱테일이 어떤 손으로 쓴 테스트 스위트보다 더 정보량이 많아진다.
구현 — 두 번째 구현을 만들지 않기
의존성을 데이터로 표현
프로덕션 서비스는 요청 페이로드만으로 결정하지 않는다. replay에서는:
| 프로덕션 | replay |
|---|---|
| 라이브 DB read | 히스토리 스냅샷 조인 |
| 버전 룰 | replay 입력으로 로드 |
| 머천트 설정 | 머천트 + 유효일자로 조인 |
| 프로그램 자격 | time-aware 상태 |
| 타 서비스 호출 | 로깅된 request/response 쌍 |
| 부수효과 | 실행 대신 출력 row로 기록 |
필요한 fidelity는 테스트하려는 결정에 달렸다. harness는 프로덕션 환경 전체를 시뮬레이션할 필요가 없고 그 결정에 영향을 주는 상태만 포착하면 된다.
코드 구조가 관건
코어 로직이 명시적 입력·의존성을 받아야 하고, 요청 스코프 프레임워크 객체·전역 상태·라이브 DB·외부 서비스에 직접 손을 뻗으면 안 된다.
// Before: 결정과 부수효과가 뒤섞임
CostResponse call(CostRequest request) {
Context context = loadContext(request);
CostResult result = evaluateCost(request, context);
storage.write(request, result);
publisher.publish(result);
return CostResponse.from(result);
}
// After: decisionEngine이 온라인/오프라인 양쪽에서 호출 가능한 라이브러리
CostResult result = decisionEngine.evaluate(input);Spark wrapper는 얇아야 한다 — 데이터 로드, 조인, 객체 구성, 분산 실행, 출력 쓰기만. Spark 잡이 룰을 재구현하면 replay 시스템이 프로덕션과 drift해 거짓 확신을 생산한다.
출력 스키마는 비교가 아니라 디버깅을 위해
레코드마다 원본 입력 추적 식별자, 현재 출력, 후보 출력, 선택된 중간값, 취한 룰 경로의 설명 필드. “값이 바뀌었다”만 말하는 diff는 부족하고, 왜 바뀌었는지 설명하는 diff가 유용하다.
온라인 변형 — shadow validation
배치 replay의 실시간 사촌. 라이브 트래픽을 샘플링해 신규 구현에 replay하고 응답을 비동기·요청 경로 밖에서 비교한다. Affirm의 모놀리스 탈출에서 이 파이프라인이:
- 어떤 합성 테스트로도 못 잡을, 설정 롱테일에서만 나타나는 실제 버그를 사용자가 보기 전에 잡았고
- 어떤 테스트 스위트보다 정직하게 롤아웃 결정을 주도한 머천트별 parity 신호를 줬다
전제 조건이 하나 있다. 레거시 시스템이 먼저 정직해져야 한다 — 렌더 결과가 저장된 설정의 순수 함수여야 한다. 인라인 오버라이드의 롱테일이 남아 있으면 재작성에 고정된 타겟이 없고, shadow 파이프라인이 설명할 수 없는 diff를 쏟아낸다. 앞단의 지루한 정리 스프린트가 프로젝트에서 가장 싼 부분이다.
프라이버시 경계
유용한 테스트 케이스는 프로덕션에 나타난 모든 원본 필드가 아니라 의사결정에 관련된 요청의 형태다.
- 테스트 대상 행동에 필요한 필드로 입력 최소화
- 민감 식별자는 토큰화하거나 안정적 테스트 ID로 치환
- 불필요한 필드는 삭제·집계·제외
- 기대출력·diff·로그·golden dataset 모두 원본 PII 노출 회피
- 소스 데이터 민감도에 맞는 접근 통제와 보존 정책
한계
- 재구성 가능한 결정 상태만큼만 좋다. 컨텍스트가 불완전하면 결과가 정밀해 보이면서 틀릴 수 있다
- 분산 상태·이벤트 구동 의존성·비동기 워크플로·시간에 따라 변하는 라이브 의존성이 있으면 미래 입력을 바꾸는 부수효과를 깔끔히 격리할 수 없다
- 미래가 외부 행위자에 달린 경우(발급사 행동, 레이턴시, 승인률, 고객 믹스 변화) replay는 전체 검증 전략이 아니라 증거의 한 레이어
- 출력이 이 한계를 가시화해야 한다. 완벽한 시뮬레이션이 목표가 아니라, 고위험 변경을 프로덕션 전에 더 안전하게 만들 만큼의 의사결정 환경을 재구성하는 것이 목표
부수 효과 — 더 나은 서비스 경계
harness를 만드는 과정이 제거할 만한 결합을 드러낸다. replay 가능하게 만드는 리팩터링은 곧 단위 테스트·마이그레이션·리뷰·추론을 쉽게 만드는 리팩터링이다. 그래서 마이그레이션이 harness를 짓기에 최적의 시점이다 — 어차피 새 구현을 만들고 새 경계를 정의하는 중이고, harness는 그 마이그레이션의 리스크를 즉시 줄여준다.
연관 개념
- Data Pipeline Testing
- Large-Scale Data Migration
- Spark at Scale
- Data Quality and Validation
- Idempotency
- Data Governance
- A-B Testing and Experimentation
- Accelerating Spark Queries with Apache DataFusion Comet — 기존 1만 개 테스트를 differential testing harness로 재활용
Source: Scaling up your microservice testing with Apache Spark (Part 1), Scaling up your microservice testing with Apache Spark (Part 2), Re-architecting Affirms Upfunnel Platform