섀도우 런(Shadow Run): 사이드 이펙트 없이 라이브 트래픽에서 새로운 워크플로우 버전을 테스트하는 방법
요약
섀도우 런은 실제 트래픽을 대상으로 새로운 워크플로우 버전을 테스트하는 방법론입니다. 라이브 버전(v1)과 새 버전(v2)을 동시에 실행하되, v2의 사이드 이펙트는 로그로 기록하여 실제 시스템에 영향을 주지 않으면서 두 결과를 비교할 수 있습니다.
핵심 포인트
- 섀도우 런은 프로덕션 환경에서 안전하게 신규 워크플로우를 테스트합니다.
- v2의 모든 사이드 이펙트 노드는 더미(stub)로 대체되어 로그만 남깁니다.
- 두 버전의 결과를 비교하여 변경된 동작이나 버그를 정확히 찾아낼 수 있습니다.
Fixture는 메커니즘이 작동함을 증명할 뿐입니다. 실제 트래픽에서 새 버전이 동일하게 동작한다는 것을 증명하지는 못합니다. 섀도우 런(shadow run)은 다음과 같이 작동합니다. 모든 사이드 이펙트를 더미(stubbed out) 처리한 채 라이브 이벤트를 새로운 워크플로우 버전을 통해 재실행하고, 'v1이 무엇을 했는지'와 'v2가 무엇을 했을지'를 비교(diffs)합니다. 이것이 제가 어떤 버전이 프로덕션에 적용되기 전에 실행하는 정확한 설정입니다.
지난 글에서 다룬 픽스처 테스트는 메커니즘, 즉 게이트가 따옴표 없는 항목을 제거하고, 폴백(fallback)이 작동하며, 크래시 재실행 시 중복이 발생하지 않는지 등을 검증합니다. 모두 결정론적이고 필수적인 과정입니다. 하지만 지난주에는 프롬프트 변경 사항이 모든 픽스처를 통과했지만, 실제 프로덕션 환경에서는 여전히 열두 명의 고객에게 이메일을 두 번 보낼 수 있는 상황이었습니다. 픽스처는 그 대화 내용을 볼 기회가 없었는데, 그것들은 거의 깨끗했기 때문에—통과하기에 충분히 깨끗했지만, 새로운 문구에 걸리기에는 너무 이상했습니다.
바로 이것이 섀도우 런이 메우는 간극입니다. 실제 무언가에 영향을 주지 않으면서 라이브 트래픽을 대상으로 새 버전을 테스트하는 것입니다.
포크(The fork)
저의 모든 프로덕션 워크플로우는 Webhook 또는 Schedule 트리거로 시작합니다. 섀도우 버전 역시 같은 트리거로 시작하지만, 추가적인 첫 번째 단계가 하나 있습니다. 바로 이벤트가 복제되는 것입니다.
- 브랜치 A (라이브, v1): 현재의 프로덕션 워크플로우이며 변경되지 않았습니다. 실제 사이드 이펙트를 작성합니다—이메일을 보내고, 작업을 생성하고, 추적기를 업데이트합니다.
- 브랜치 B (섀도우, v2): 새로운 워크플로우 버전이며, 모든 효과를 발생시키는 노드는 더미(stub)로 대체됩니다.
포크 자체는 트리거 바로 다음에 오는 하나의 Code 노드입니다:
// Fan-out: same event to live and shadow
const event = $input.first().json;
return [
...
브랜치 A는 워크플로우를 그대로 실행합니다. 브랜치 B는 새로운 버전을 실행합니다. 동일한 입력, 두 개의 버전, 하나의 실제 세계입니다.
더미(The stubs): 효과가 로그 행이 되다
이것이 핵심 트릭입니다. 섀도우 브랜치에서는 사이드 이펙트를 발생시킬 모든 노드—Gmail 전송, Slack 게시, Notion 삽입, 타사 API로의 HTTP POST—가 실제 동작을 수행하는 대신, 의도된 효과를 설명하는 _로그 행(log row)_을 작성하는 더미 Code 노드로 대체됩니다:
// Shadow stub: 의도를 기록하고 아무것도 발생시키지 않음
const intent = {
event_id: $input.first().json.event_id, // branch A와 동일한 키
...
Branch A는 실제로 발생하는 모든 효과에 대해 같은 형태의 행을 live_log에 기록합니다. 이제 저는 스키마가 동일한 두 개의 테이블을 갖게 되었습니다: v1이 무엇을 했는지, v2가 무엇을 했을지.
섀도우 스텁은 단순히 설정되지 않은 것을 넘어, 효과를 발생시킬 수 없어야 합니다. 저는 실행 전에 섀도우 워크플로우에 대해 태그 linter(이전 게시물)를 실행하고 다음을 단언합니다: 효과적이라고 태그된 노드는 제로. 자격 증명이 붙은 스텁은 나쁜 날을 기다리는 버그입니다.
차이점 쿼리 (The divergence query)
N개의 라이브 이벤트가 흐른 후(저는 50개를 사용합니다. 이상한 경우를 보기엔 충분하고, 감당하기엔 저렴합니다), 하나의 쿼리를 실행합니다:
-- 이벤트별: live에만 있는 효과, shadow에만 있는 효과, 또는 변경된 효과
SELECT event_id,
SUM(CASE WHEN l.event_id IS NULL THEN 1 ELSE 0 END) AS extra, // v2가 새로운 것을 할 경우
...
세 가지 클래스, 세 가지 규칙:
- 누락됨 (Missing) (v2가 v1이 했던 것을 누락): 항상 버그입니다. 새 버전은 적어도 이전 버전이 했던 모든 것을 해야 하며, 누락된 것이 의도적인 검토된 변경 사항이 아니라면 안 됩니다. 저는 의도적으로 누락된 사례를 단 한 번도 본 적이 없습니다. 그것은 항상 버그입니다.
- 추가됨 (Extra) (v2가 새로운 것을 할 경우): 가장 위험도가 높은 클래스입니다. 모든 추가 효과는 버전 배포 전에 수동 검토를 거쳐야 합니다. 즉, v2가 보냈을 실제 페이로드를 읽어봐야 합니다. 위에서 언급된 더블 이메일 사고는 '추가됨' 사례였습니다. 피처 팩은 트리거하는 스크립트 패턴(
섀도우 런(Shadow run)은 실행되는 동안 LLM 지출 비용을 두 배로 만듭니다. 모든 이벤트가 두 개의 모델 스택을 거치기 때문입니다. 따라서 이 실행은 설계상 제한됩니다. 즉, 포크 노드(fork node)가 이벤트를 계산하고 50개를 초과하면 중복 생성을 멈춥니다. 제 트래픽 볼륨 기준으로 이는 버전당 약 $2의 추가 모델 비용을 의미합니다. 고객에게 두 번 이메일을 보내는 버전은 환불 및 사과하는 데 드는 시간으로 그보다 더 많은 비용이 발생할 수 있습니다. 또한, 이 제한은 메모리가 아닌 기계적으로 강제됩니다. 카운터가 포크 노드에 존재하며, 섀도우 워크플로우(shadow workflow)는 상한선에 도달하면 스스로 비활성화됩니다.
섀도우 런이 아닌 것들
이는 피처스(fixtures)를 대체하는 것이 아닙니다. 피처스는 저렴하고 빠르며 결정론적입니다. 모든 편집마다 실행되기 때문입니다. 반면, 섀도우 런은 비용이 많이 들고 느리하며 비결정론적입니다. 버전당 한 번만 실행되며, 피처스가 가짜로 만들 수 없는 유일한 것, 즉 프로덕션 현실(production reality)을 대상으로 합니다. 피처스는 메커니즘을 테스트합니다. 섀도우는 _버전_을 테스트합니다. 이 둘은 함께 두 축을 다루지만, 어느 하나도 두 가지를 모두 다루지는 못합니다.
또한 A/B 테스트도 아닙니다. 섀도우 브랜치(shadow branch)의 어떤 것도 인간에게 도달하지 않습니다. 오픈율이나 답장 품질에 대한 측정 지표가 없습니다. 디버전스 쿼리(divergence query)는 단지
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기