07 테스트, 실패, 복구
모듈 07 강사 노트 — 타이밍, 토크 트랙, 흔한 실패 사례, 초기화 절차.
학습자 레슨 07 테스트, 실패, 복구에 대응하는 진행자용 안내서입니다.
타이밍
약 20분. 이 모듈이 장애 실습입니다. 모듈 08과 마무리 전에 진단이 마무리될 수 있도록 충분한
활주로를 확보하세요. 실습은 결함 01(진단 515분)을 다룹니다. 결함 02(510분)와
03(10~20분, 그리고 약 3천만 행 전체 데이터셋이 있을 때만)은 진행이 빠른 강의실을 위한 선택
추가 장애로 남아 있습니다. 마무리가 밀리지 않도록 리허설에서 하드 스톱 시각을 정하세요.
토크 트랙
- 여기가 성과가 드러나는 지점입니다. 지금까지 만든 모든 것을 동원해 실제 장애를 처리합니다.
- 원인이 아니라 증상을 설명하고, 에이전트가 텔레메트리로부터 스스로 수렴하게 하세요.
- 여러 참가자의 진단을 프로젝터에 나란히 띄워 보는 것도 고려하세요.
- 실습은 결함 01을 다룹니다. 시간이 남아 한 라운드를 더 할 수 있다면 결함 02를 추가하세요 (결함 03은 전체 데이터셋이 미리 시드된 경우에만). 결함 사이에는 아래의 공통 stash 후 브랜치 전환 초기화 절차를 사용하세요.
정답지
이 섹션을 학습자와 공유하지 마세요. 플레이북에만 존재하며 앱 저장소에는 절대 커밋되지
않습니다. 각 결함은 build-workshop-v1에서 갈라진 자체 브랜치의 작은 변경 하나이며,
복구 방법은 그것을 되돌리는 것입니다. 한 번에 결함 하나만 실행하세요. 실습의 장애는
결함 01(515분, 변칙구 — 백엔드는 무죄)입니다. 진행이 빠른 강의실을 위한 선택 추가:
결함 02(510분, 워밍업, 트레이스가 전부를 말해줌), 그리고 전체 데이터셋이 있을 때만
결함 03(10~20분, 더 깊은 ClickHouse 추론).
모든 결함에 공통인 초기화 절차(stash는 참가자의 편집을 보존하고 브랜치 전환이 막히는 것을 방지합니다):
git stash push --include-untracked -m "module-07-fix"
git switch build-workshop-v1
docker compose --env-file .env.workshop \
-f docker-compose.workshop.yml -f docker-compose.otel.yml up -d --build결함 02 — zone stats 500 (fault/02-zone-stats-500)
참가자에게 보이는 커밋 메시지: "backend: align zone stats column names with API params"
(backend/app/query_builders.py의 zone_stats_sql만 건드립니다). 이 변경은
pickup_location_id 대신 taxi_trips에 존재하지 않는 컬럼인 pickup_zone_id로
그룹화합니다.
- 증상: 지도 단계 구분도가 비어 있고(폴리곤은 렌더링되지만 모든 zone이 가장 밝은 구간에
들어갑니다) "Query Nms" 캡션이 사라집니다.
GET /api/metrics/zone_stats에서 500이 연달아 발생합니다(React Query가 세 번 재시도). 다른 카드는 모두 정상입니다. - 신호가 있는 곳:
error=True,error.category="query_failed"이고db.statement에pickup_zone_id AS zone_id가 담긴 ClickStackclickhouse.query스팬. 기록된 예외는 ClickHouse Code 47UNKNOWN_IDENTIFIER입니다. 대응되는 ERROR 로그: "ClickHouse query failed (category=query_failed, elapsed_ms=...): ... | sql=...". - 진단 경로: 500 스팬을 찾음 ->
db.statement를 읽음 ->pickup_zone_id에 대한UNKNOWN_IDENTIFIER->DESCRIBE taxi_trips(또는 형제 쿼리 빌더와 비교) -> 실제 컬럼은pickup_location_id. - 복구:
zone_stats_sql에서pickup_zone_id를 다시pickup_location_id로 바꾸거나 (결함 커밋을git revert), 백엔드를 다시 빌드합니다. - 초기화: 위의 공통 초기화 절차.
결함 03 — 느린 대시보드 (fault/03-slow-dashboard)
커밋 메시지: "backend: window the trend series by bucket in HAVING"
(timeseries_sql을 건드립니다). 시간 범위 조건을 WHERE에서 버킷 별칭에 대한 HAVING으로
옮겨서 WHERE가 1이 됩니다. ClickHouse가 더 이상
(car_type, pickup_datetime) 기본 키로 프루닝할 수 없어 quantileTDigest 상태 두 개와 함께
taxi_trips를 전체 스캔하고, 5초 max_execution_time을 넘깁니다.
- 증상: "What's happening now?" 추세 카드만 실패합니다 — 로딩만 돌다가 "504 Query timed out..."을 표시합니다. 형제 카드는 모두 빠르게 유지됩니다.
- 신호가 있는 곳:
error.category="timeout",db.elapsed_ms가 5000 근처이고db.statement가WHERE 1 ... GROUP BY ts HAVING ts >= ...를 보여주는clickhouse.query스팬. 예외는 Code 159TIMEOUT_EXCEEDED입니다. 다른 엔드포인트의 빠른 200 스팬과의 대비가 결정적 단서입니다. - 진단 경로: 빠른 형제들 사이에서 느린 카드 하나 -> 타임아웃 스팬을 열어봄 -> SQL을 읽음
-> 시간 필터가
HAVING에 있고WHERE에는pickup_datetime범위가 없음 -> 기본 키 프루닝 / 조건 푸시다운 안티패턴(형제들은 범위 조건을WHERE에 둡니다). - 복구: 시간 범위 조건을
WHERE로 되돌리고(결함 커밋을 revert) 백엔드를 다시 빌드합니다. - 초기화: 위의 공통 초기화 절차.
- 강사 주의 사항: 심각도는 데이터 양과 서비스 크기에 비례합니다. 전체 시드 데이터셋
(약 3천만 행)과 워크숍 등급 서비스(유휴 상태에서 깨어나는 중일 수 있음)에서는 5초 타임아웃이
안정적으로 발생하지만, 작은 샘플 시드에서는 단지 조금 느릴 뿐이며 확실한 504가 아닙니다.
클라이언트의
send_receive_timeout과 서버의max_execution_time이 모두 5초이므로, 소켓 타임아웃 경쟁 때문에 깔끔한 504 대신 500이 간간이 나타날 수 있습니다 — 결함이 아니라 기본 설정의 성질입니다.
결함 01 — 지도가 로딩되지 않음 (fault/01-map-not-loading)
커밋 메시지: "frontend: serve map geojson from /static asset path"
(frontend/src/ui/ZoneMap.tsx의 fetch 경로를 건드립니다). 존재하지 않는
/static/taxi_zones.geojson을 가져옵니다. 핵심 미묘함: nginx의 SPA 폴백
(try_files ... /index.html)이 404가 아니라 HTML 문서와 함께 HTTP 200을 반환하므로
r.ok는 통과하고 r.json()이 Unexpected token '<' 같은
SyntaxError를 던집니다.
- 증상: 지도 카드가 죽습니다 — 베이스 타일은 렌더링되지만 NYC 폴리곤이나 단계 구분도가 없고, 빨간 인라인 오류가 표시됩니다. 나머지는 모두 정상입니다.
- 신호가 있는 곳: 백엔드 ClickStack에는 아무것도 없습니다 — 그 asset 요청은 FastAPI에
도달하지 않습니다. 브라우저 콘솔에는
/static/taxi_zones.geojson을 지목하는 JSON 파싱 오류가 보이고, Network 탭에는 그 요청이 상태 200과text/html로 반환되는 것이 보이며, nginx 액세스 로그에는 폴백이 기록됩니다. - 진단 경로: 백엔드 트레이스는 깨끗함 -> 브라우저 콘솔 / Network 탭으로 전환
->
.geojson요청이 200text/html을 반환 -> SPA 폴백이라는 함정을 알아차림 -> 파일은 실제로 웹 루트, 즉/taxi_zones.geojson에 있음. - 복구: fetch 경로를 되돌리거나(두 줄) 결함 커밋을
git revert한 뒤 프런트엔드를 다시 빌드합니다. - 초기화: 위의 공통 초기화 절차.
- 교육 포인트: 모든 실패가 백엔드 트레이스에 나타나는 것은 아니며, SPA 폴백 뒤에서 404가 200으로 위장할 수 있습니다 — 그러므로 상태 코드만 보지 말고 실제 응답 본문과 content-type을 읽어야 합니다.
- 참고(브라우저 SDK): HyperDX 브라우저 SDK가 활성화되어 있으면(모듈 05 오버레이),
이제 프런트엔드가 이 문제를 ClickStack 자체에 드러냅니다 —
ZoneMap의 파싱 실패가ServiceName=nyc-taxi-frontend아래의console.error스팬으로 캡처되고,text/html200에 대한 리소스 스팬과 짝지어집니다. 따라서 에이전트는 ClickStack을 벗어나지 않고 텔레메트리만으로 문제를 특정할 수 있습니다. 브라우저 콘솔 / Network 탭은 유일한 경로가 아니라 대안입니다.
에이전트 응답 예시 (결함 01, ClickStack MCP 경유):

예상 진단: 에이전트는 실패를 200 text/html을 반환하는 geojson 요청(SPA 폴백)과 그로 인해
nyc-taxi-frontend 아래에 캡처된 JSON.parse 오류에 연결하고, asset을 /static/으로
배포하거나 .json() 파싱을 방어하라고 권고합니다.
흔한 실패 사례
- 에이전트가 문제를 특정할 만한 텔레메트리 기준선이 부족함. 모듈 05가 일찍 실행되었어야 합니다.
- 참가자가 증거로 근본 원인을 확인하기 전에 복구로 뛰어듦.
- 스택을 방금 올린 탓에 — 또는 결함 03의 경우 과거 데이터가 너무 적게 로드된 탓에 — 결함 증상이 아직 보이지 않음. 리허설에서 측정됨: 기본 한 달치 시드(약 317만 행)에서는 WHERE 대 HAVING 결함이 관측되지 않습니다 — 전체 스캔이 약 300~560ms에 끝나 기준선과 구별되지 않습니다. 결함 03을 데모하려면 여러 달을 시드하거나, 즉시 재현되는 결함 01을 고수하고 결함 03은 대규모에서의 예시로 다루세요.
- 결함 01은 백엔드 트레이스를 전혀 만들지 않으며, 잘못된 요청은 404가 아니라 SPA 폴백을 통해
200
text/html을 반환합니다. 참가자가 트레이스를 끝없이 찾을 수 있습니다. JSON 파싱 오류와text/html응답이 보이는 브라우저 콘솔 / Network 탭으로 유도하세요. - 결함 브랜치를 체크아웃한 뒤
--build를 잊어서 예전 이미지가 계속 실행됨.
초기화 절차
- 공통 stash 후 브랜치 전환 초기화 절차는 참가자가 시도한 복구를 버리지 않고 앱 전체를 복원합니다.
- 결함을 다시 주입하려면
fault/01-map-not-loading/fault/02-zone-stats-500/fault/03-slow-dashboard중 하나를 체크아웃하고 워크숍과 otel compose 파일로 다시 빌드하세요. - 결함별 증상, 신호, 복구 방법은 위의 정답지 섹션에 있습니다. 정답지는 이 플레이북에만 두세요. 앱 저장소에는 절대 커밋되지 않습니다.