한눈에 보기
- 문제: 수동 백필 실행 한 번으로 KOSPI/KOSDAQ 수년치 이력이 30일치로 줄었다.
- 원인:
mode=overwrite가 “요청 범위만 교체”가 아니라 “파일 전체 교체”로 동작했다. 파티션 크기의 문제가 아니라 쓰기 함수의 문제였다. - 선택: append/overwrite 두 함수를 없애고, 범위만 교체하고 범위 밖은 불가침인
upsert_bronze_range하나로 통합했다.mode라는 선택지 자체를 제거했다. - 결과: 재수집으로 복구(2,690개 심볼, 실패 0). 복구 과정에서 데이터 소스의 숨은 한계(3,000행 캡)까지 발견했다.
- 핵심 교훈: 사고 유형을 없애는 가장 확실한 방법은 조심하는 것이 아니라, 잘못 고를 수 있는 선택지를 구조적으로 제거하는 것이다.
사고
Dagster Launch Pad에서 백필 asset을 수동 실행했다. 설정은 mode=overwrite, start_date=2026-06-05. 의도는 “최근 구간을 다시 받아서 덮어쓰기”였다.
결과는 의도와 달랐다. 이 실행은 각 종목의 parquet 파일 전체를 최근 30일치 데이터로 교체했다. KOSPI/KOSDAQ 수년치 이력이 사라졌다.
코드 관점에서 보면 버그도 아니었다. overwrite_bronze()는 문자 그대로 “받은 DataFrame으로 파일을 덮어쓰는” 함수였고, 정확히 그렇게 동작했다. 문제는 “오늘부터 받은 30일치”와 “파일에 있던 5년치”의 관계를 아무도 — 함수도, 실행자도 — 조율하지 않았다는 데 있다.
원인 분석: 파티션이 아니라 쓰기 함수
사고 직후 나온 첫 대응안은 “파일을 연 단위로 쪼개자”였다. 그러면 같은 실수를 해도 한 해치만 날아간다. 하지만 이건 blast radius를 줄일 뿐 사고 유형 자체는 남는다 — 쓰기 로직을 안 고치면 같은 사고가 연 단위로 재현된다. 파티션 크기가 아니라 쓰기 함수가 문제다.
당시 쓰기 경로에는 append_to_bronze와 overwrite_bronze 두 함수가 있었고, 호출자가 mode로 골랐다. 이 구조의 본질적 결함은 “잘못 고를 수 있다”는 것이다. append를 골라야 할 곳에 overwrite를 고르는 실수는 언젠가 반드시 다시 일어난다.
대응: 선택지를 제거한다
두 함수를 폐기하고 upsert_bronze_range 하나로 통합했다. 계약은 이렇다.
- 기존 파일을 읽고, 요청된 날짜 범위만 새 데이터로 교체하고, 다시 쓴다.
- 범위 밖 데이터는 불가침이다. 어떤 인자를 줘도 요청 범위 밖을 건드릴 방법이 없다.
mode파라미터는 존재하지 않는다.
같은 실행(start_date=2026-06-05)을 이 함수로 하면 6월 5일 이후 구간만 갱신되고 5년치 이력은 그대로다. 멱등성도 공짜로 얻는다 — 같은 범위를 몇 번 다시 돌려도 결과가 같다. 검증은 사고와 동일한 패턴을 삼성전자에 재현해 범위 밖 보존을 확인하는 것으로 했다.
이후 이 원칙은 다른 안전장치들과 겹겹이 쌓였다. 쓰기 시점에 기존 대비 90% 이상 행 수가 급감하면 쓰기를 거부하는 가드, MinIO 버킷 versioning(비최신 버전 30일 보존) 백스톱. 특히 versioning은 이후 다른 인시던트(쿼리 한 번에 104초 - 작은 파일 8만 개와의 싸움의 live-mount 사건)에서 실제 복구 수단으로 작동했다.
복구가 알려준 것: 3,000행 캡
복구는 KOSPI 919 + KOSDAQ 1,771개 심볼을 FinanceDataReader로 재수집하는 것이었고, 실패 0으로 끝났다. 그런데 재수집 결과를 검증하다가 이상한 패턴이 보였다.
KOSPI 919개 심볼 중 747개(81%)의 행 수가 정확히 3,000행이었다. 삼성전자 같은 대형주도 전부 2014-04-23부터 시작했다. 우연일 수 없는 균일함이다. FDR의 KRX 데이터는 소스(Naver) 특성상 약 3,000거래일에서 잘린다 — 2014년 이전 이력은 이 경로로는 처음부터 받을 수 없었던 것이다.
이 발견이 흥미로운 이유는, 사고가 아니었으면 몰랐을 사실이라는 점이다. 원래 bronze에는 과거에 다른 경로로 들어온 이력이 섞여 있어서 캡의 존재가 가려져 있었다. 전체 재수집이라는 “동일 조건 실험”이 소스의 한계를 드러냈다. 이 한계는 이후 yfinance 기반의 별도 이력 백필 트랙(2000년까지 소급, 분할 수정주가 일치 검증 포함)으로 이어졌다.
부수 정리도 있었다. 아무도 읽지 않는데 매일 쌓이던 좀비 경로(exchange=KRX — KOSPI/KOSDAQ 분리 전의 유물) 파일 2,690개를 삭제했고, 종목이 KOSPI↔KOSDAQ으로 이전하면 이력이 두 경로로 분절되는 잠재 버그는 물리 저장을 통합하고 소속을 메타데이터 join으로 푸는 구조 변경으로 해소했다.
정리
사고 대응은 세 층위로 남았다.
- 복구: 재수집으로 데이터 원상 복구 (실패 0).
- 재발 방지: 사고를 낼 수 있는 API 자체를 제거 (
mode소멸, 범위 밖 불가침). - 부수 이득: 소스의 숨은 캡 발견, 좀비 데이터 정리, 이후 모든 쓰기 경로의 안전장치 표준화.
개인 프로젝트라 다행이었다는 말로 끝내고 싶지 않다. 오히려 반대다 — 리뷰어도 승인 절차도 없는 1인 운영 환경에서는 “조심한다”가 안전장치가 될 수 없다. 실수해도 사고가 안 나는 구조를 만드는 것 외에는 방법이 없고, 이 사고가 그 사실을 배우게 해줬다.