정리 크론이 과거를 지웠다

2026-07-16 · 개발 로그

가장 위험한 작업일수록 이름이 무해합니다. "오래된 데이터 정리"보다 안전하게 들리는 작업 이름도 드뭅니다. 이 작업이 5년치 가격 기록을 지웠습니다.

그리고 이 글이 앞선 과 같은 계열인 이유가 여기 있습니다 — 데이터가 사라졌는데도 화면에는 에러가 뜨지 않았습니다. 그럴듯한 숫자가 떠 있었습니다.

왜 지우려 했나

발단은 DB 용량이었습니다. 일별 가격 테이블이 커지자, 백테스트 기능을 손보면서 이런 판단을 했습니다 — "백테스트는 오늘 가격만 있으면 되니까, 오래된 가격은 안 갖고 있어도 된다."

그래서 매일 오래된 가격을 지우는 정리 작업을 만들었습니다. 보관 기간은 35일. 당시 주석은 이렇게 적혀 있었습니다.

# 백테스트는 오늘(최신) 주가만 조회하므로 과거분은 불필요.
# 기본 35일: 연휴 연속 공백(최대 ~10거래일) + 배치 실패 여유 포함.

35라는 숫자에는 나름의 근거가 있었습니다. 최악의 연휴로 거래일이 열흘쯤 연속으로 비어도 가장 최근 거래일 하나는 안전하게 찾을 수 있는 여유, 거기에 배치가 하루이틀 실패해도 되는 여유. 즉 35일은 "오늘 가격 하나를 찾기 위한" 여유였습니다.

이 판단에는 빠진 게 하나 있었습니다. 백테스트만 생각하고 랭킹을 잊은 것입니다. 백테스트는 오늘 가격만 필요하지만, 랭킹은 다릅니다.

랭킹은 5년치를 매일 읽는다

FomoBot의 5년 상승률 랭킹을 계산하려면, 당연히 5년 전 가격이 필요합니다. 1년 랭킹은 1년 전 가격이, 90일 랭킹은 90일 전 가격이 필요합니다. 랭킹 배치는 매일 최장 1,825일(5년)치 과거 가격을 읽어 순위를 만듭니다.

그런데 정리 작업은 35일보다 오래된 것을 전부 지웠습니다. 시장 구분도 없이, 예외도 없이.

DELETE FROM price_daily WHERE date < :cutoff

매일 이 쿼리가 돌면서, 랭킹이 필요로 하는 과거가 매일 사라졌습니다. 남은 것은 최근 35일뿐. 백테스트를 위한 여유가, 랭킹의 전 재료를 파괴했습니다.

가장 고약한 부분 — 에러가 안 났다

5년치 데이터가 사라졌으니 5년 랭킹이 텅 비었을 거라고 생각하기 쉽습니다. 그랬으면 차라리 나았습니다. 빈 화면은 눈에 띄니까요.

실제로는 숫자가 떠 있었습니다. 90일 랭킹에도, 1년 랭킹에도, 5년 랭킹에도. 다만 그 숫자들이 전부 똑같았습니다.

이유는 이렇습니다. 90일·1년·5년 랭킹을 계산하려 해도, 실제로 존재하는 데이터가 최근 35일뿐이면 세 계산 모두 그 35일 안으로 수렴합니다. 5년 전 가격을 찾지 못하면 있는 것 중 가장 오래된 값을 쓰게 되고, 그 값은 세 기간 모두 같습니다. 그래서 기간만 다른 세 랭킹이 서로 완전히 같은 결과를 냈습니다.

화면의 "5년 상승률 1위"는 사실 며칠치 데이터로 계산된 숫자였습니다. 텅 빈 게 아니라 가짜로 채워진 겁니다. 그리고 가짜로 채워진 숫자는 빈칸보다 훨씬 늦게 발각됩니다. 그럴듯하게 생겼으니까요.

발각의 단서는 "90일과 1년과 5년 랭킹이 똑같다"는 이상함 하나였습니다. 세 개가 같을 수는 없습니다. 그 이상함을 잡아당기자 실이 풀렸습니다.

되돌릴 수 없는 삭제

이 사고가 특히 무거운 건, DELETE에는 되돌리기 버튼이 없다는 점입니다. 계산이 틀린 버그는 코드를 고쳐 다시 계산하면 됩니다. 하지만 지워진 데이터는 코드를 고친다고 돌아오지 않습니다. 원본이 어딘가에 남아 있어야 합니다.

다행히 원본을 다시 구할 수 있었습니다. 가격 데이터는 원래 외부 소스에서 수집한 것이라, 삭제된 기간(2020년부터 현재까지)을 통째로 재수집했습니다. 지금은 두 시장 모두 2020년 6월부터 빈 구간 없이 복구된 상태입니다.

하지만 재수집에는 생존 편향과 맞닿는 구조적 허점이 있습니다. 재수집은 "오늘 기준으로 상장된" 종목 목록을 순회합니다. 그렇다면 2020년 이후 이 사고 이전에 이미 상장폐지된 종목은, 오늘의 목록에 없으니 재수집 대상에서도 빠졌을 수 있습니다. 원래 있던 데이터를 완벽히 복원한 게 아니라, 오늘 살아있는 종목의 과거만 복원했을 가능성이 있습니다. 확정된 사실은 아니고 구조상 남는 구멍이지만, 밝혀 둡니다. 삭제는 이렇게, 복구한 뒤에도 흔적을 남길 수 있습니다.

어떻게 막았나

정리 작업의 보관 기간을 늘리는 것으로는 부족했습니다. 처음엔 35일을 2,000일로 바꿨지만, 한 시간여 뒤 정리 작업 자체를 통째로 삭제했습니다. 예약 실행 목록에서도 제거했습니다.

보관 기간을 늘리는 건 "이번엔 안전한 값"을 고르는 일입니다. 하지만 다음에 누군가 그 값을 다시 건드릴 수 있고, 그러면 같은 사고가 반복됩니다. 삭제하는 작업 자체를 없애면, 되돌릴 수 없는 DELETE가 애초에 실행될 경로가 사라집니다. 용량 문제는 다른 방법으로 풀기로 했습니다 — 데이터를 지우는 것 말고.

남는 교훈

1. 삭제 작업의 이름을 의심하십시오. "정리", "청소", "오래된 것 제거"는 전부 무해하게 들립니다. 하지만 되돌릴 수 없는 DELETE를 매일 자동으로 실행하는 작업은, 이름이 무엇이든 시스템에서 가장 위험한 코드입니다.

2. "이 데이터는 누가 쓰는가"를 전부 세십시오. 이 사고의 뿌리는 "백테스트는 오늘 가격만 쓴다"까지만 확인하고 멈춘 것입니다. 같은 테이블을 랭킹도 읽고 있었습니다. 데이터를 지우기 전에 그 데이터의 모든 소비자를 확인해야 합니다. 하나라도 빠뜨리면 그쪽이 조용히 깨집니다.

3. 빈칸보다 가짜 값을 두려워하십시오. 데이터가 사라졌을 때 시스템이 에러를 냈다면 즉시 알았을 겁니다. 대신 시스템은 있는 것 중 가장 오래된 값으로 빈칸을 메워 그럴듯한 숫자를 만들었습니다. LAG, 거래일 라벨, 박제된 종가와 똑같습니다 — 금융 데이터에서 가장 무서운 실패는 멈추는 실패가 아니라 계속 돌아가는 실패입니다.

4. 삭제는 늘리지 말고 없애십시오. 위험한 삭제를 "안전한 값으로 조정"하면 위험은 잠들 뿐입니다. 가능하면 삭제 경로 자체를 제거하는 편이, 안전한 값을 고르는 것보다 안전합니다.

이것으로 이 사이트를 만들며 겪은 "조용히 틀리는 데이터" 사고를 네 편에 걸쳐 기록했습니다. 넷 다 공통점이 하나입니다. 크래시하지 않았고, 그럴듯한 숫자를 뱉었으며, 그 숫자가 사용자에게 그대로 보일 뻔했습니다. FomoBot이 파는 것은 결국 숫자에 대한 신뢰이고, 그 숫자가 어떻게 조용히 틀릴 수 있는지를 아는 것이 그 신뢰를 지키는 유일한 방법입니다.

이 글에 관하여 — 여기 적은 것은 FomoBot 데이터 파이프라인에서 실제로 있었던 사고입니다. 일별 가격 테이블에 대해 최근 35일만 남기고 삭제하는 정리 작업이 랭킹 계산에 필요한 장기 데이터를 제거했고, 이로 인해 서로 다른 기간의 랭킹이 동일한 결과를 내는 증상이 나타났습니다. 데이터는 외부 소스 재수집으로 복구했으며(현재 2020년 6월부터 빈 구간 없음), 정리 작업은 완전히 제거됐습니다. 재수집이 오늘 기준 상장 종목 목록에 의존하는 점에서 과거 상장폐지 종목 일부가 복원되지 않았을 가능성은 확정되지 않은 구조적 여지입니다. 내부 인프라 세부는 이 글의 논지와 무관해 생략했습니다.

투자 판단에 대하여 — FomoBot은 이미 지나간 가격을 정리해 보여주는 도구입니다. 이 글은 개발 기록이며, 어떤 내용도 투자 자문이 아닙니다.