지난 개발 일지 #1은 Claude Code와 하루 동안 LDBD의 여러 화면을 다듬은 기록이었다. 사실 #2도 비슷한 기능 개선기가 될 예정이었는데, 계획이 중간에 바뀌었다. 최근 크게 화제가 된 Claude Fable 5를 직접 써보고 싶었고, 마침 이미 공개해 둔 LDBD를 더 많은 사람에게 알리기 전에 코드 전체를 한 번 제대로 점검받고 싶기도 했다. 그래서 이번 회차는 새 기능을 만든 기록이 아니라, Fable 5에게 LDBD 코드베이스 전체를 감사(audit)시킨 기록이 됐다.
지금까지 LDBD는 대부분 Claude Code, 그러니까 Opus 모델로 만들어왔다. 결과물도 꽤 만족스러웠고 코드 품질을 크게 걱정한 적은 없었다. 그런데 같은 코드를 Fable 5에게 감사시켜 보고는 꽤 놀랐다. 생각보다 많은 문제가 나왔고, 그중 일부는 리더보드 점수와 보안을 조용히 깨뜨릴 수 있는 종류였다. 가장 무서운 건 에러가 나지 않는 버그다. 화면은 정상이고 빌드도 통과하고 사용자는 아무것도 모르는데, 점수가 틀리게 계산되거나 잘못된 값이 한 번 확정된 뒤 영구히 남으면, “예측을 기록하고 검증한다”는 LDBD의 핵심 신뢰가 흔들린다.
게다가 이번 작업은 한 번 감사받고 끝난 게 아니었다. Fable이 문제를 찾으면 Opus가 고치고, 그 수정을 다시 Fable이 리뷰하는 사이클을 계속 반복했다. 그 과정에서, Opus가 낸 수정 하나가 원래 버그보다 더 나쁜 버그를 만들 뻔한 것까지 Fable의 리뷰가 잡아냈다. 이 글의 하이라이트이기도 하다.
한 가지 덧붙이면, 이 글을 쓰는 시점(2026년 6월)에 Fable 5는 더 이상 쓸 수 없다. Anthropic이 미국 정부의 수출통제 지시에 따라 Fable 5와 Mythos 5 접근을 전면 중단했기 때문이다. 곧 복구를 위해 노력 중이라는 입장이라, 이 부분은 시점 기준으로만 읽어주면 좋겠다(관련 보도는 글 하단에 달아둔다). 다행히 그 직전에 이 감사를 한 번 돌려둔 게 이 글의 바탕이 됐다.
그리고 이번 작업이 남긴 결론을 미리 적자면 이렇다. AI에게 구현을 맡길수록, 검증의 겹은 더 두꺼워져야 했다.
감사 세팅 — 프롬프트 하나, 다섯 갈래의 점검
감사에 쓴 지시는 거창하지 않았다. 다음 한 단락이 전부였다.
“LDBD라는 자산 가격 예측 앱을 만들고 있어. 이 코드베이스의 목적, 구조, 계획을 파악해줘. 그런 후 이 코드베이스의 모든 문제를 찾아서 정리해줘. 코드를 수정하지는 말고 찾은 문제들의 단계별 개선 계획을 작성한 후 md파일로 정리해줘.”
두 가지를 일부러 못 박았다. 하나는 코드를 고치지 말라는 것 — 진단과 수정을 섞지 않고 먼저 문제 목록만 받고 싶었다. 다른 하나는 md 파일로 정리하라는 것 — 나중에 한 건씩 대조하며 지워나갈 체크리스트가 필요했기 때문이다.
Fable 5는 이 한 단락을 받고 코드베이스를 다섯 갈래로 나눠 동시에 훑었다. 스코어링 로직, API와 보안, 데이터베이스와 백그라운드 작업, 프론트엔드, 그리고 봇 에이전트와 스크립트다. 각자 자기 영역만 깊게 파고들어서 한 번에 코드 전체가 다른 각도로 점검됐고, 결과는 심각도별로 정리된 한 편의 보고서로 나왔다.
| 심각도 | 건수 | 대략 어떤 것 |
|---|---|---|
| CRITICAL | 4 | 리더보드 조작·점수 영구 오염처럼 핵심을 깨뜨리는 것 |
| HIGH | 11 | 특정 조건에서 데이터가 새거나 틀리는 것 |
| MEDIUM | 21 | 정확성·견고성 이슈, 일부 상황 한정 |
| LOW | 18 | 위생·문서 드리프트·cosmetic |
숫자만 보면 덜컥했다. 특히 CRITICAL로 분류된 네 건은 전부 “리더보드 신뢰성 자체를 깨뜨린다”는 딱지가 붙어 있었다. Opus로 만들면서 나름 잘 짰다고 생각한 코드였는데 말이다. 그런데 여기서부터가 이 글의 진짜 알맹이다. 나는 이 보고서를 받자마자 고치지 않았다.
감사가 곧 진실은 아니다 — 한 건씩 실제 코드로 확인
AI가 만든 감사 리포트는 false positive(실제로는 문제가 아닌데 문제라고 잡는 것)가 흔하다. 그럴듯한 문장으로 “여기 이런 취약점이 있습니다”라고 단언하지만, 실제 코드를 열어보면 이미 다른 곳에서 막혀 있거나, 그 경로 자체가 존재하지 않는 경우가 많다. 그래서 작업의 첫 단계는 고치는 게 아니라 거르는 것이었다.
지적 하나하나를 실제 파일을 열어 대조했다. “이 함수가 이런 값을 그대로 반환한다”는 지적이 있으면 그 함수를 직접 읽어, 정말 그런지, 호출하는 쪽에서 이미 걸러지는 건 아닌지 확인했다. 그런데 막상 확인해보니, Fable이 짚은 CRITICAL 네 건은 헛짚음(false positive) 하나 없이 전부 실제 버그였다. AI 감사가 흔히 쏟아내는 그럴듯한 오탐을 생각하면, 이 적중률은 솔직히 인상적이었다. 다만 그건 이번 결과가 그랬다는 거고, 한 건씩 직접 검증하는 단계 자체는 여전히 생략할 수 없는 과정이었다.
이게 지난 글에서 정리한 협업 원칙의 연장이다. 개발 일지 #1의 결론은 “구현은 위임하되 판단은 위임하지 않는다”였는데, 이번엔 그게 “감사도 위임하되 검증은 위임하지 않는다”로 확장됐다. AI에게 시킬 수 있는 건 코드를 빠르게 훑어 후보를 잔뜩 뽑아오는 일이고, 그중 무엇이 진짜인지 가려내는 건 여전히 내 몫이었다.
고친 것 1 — “조용히 잘못된” 점수 버그들
가장 먼저 손 댄 묶음은 점수 계산 파이프라인의 정확성 버그였다. 공통점은 하나였다. 전부 조용히 틀렸다. 에러도 없고, 화면도 멀쩡하고, 빌드도 통과하는데 점수만 틀렸다. 세 개를 풀어 쓴다.
장기 예측이 영원히 마감되지 않던 버그
LDBD에서 예측은 정해진 기간이 지나면 마감(resolve)된다. 1주짜리 예측이면 1주 뒤 가격을 보고 맞았는지 틀렸는지 확정하고 점수를 매기는 식이다. 그런데 이 마감 작업이 가격을 가져올 때 “최근 한 달치”만 받아오고 있었다.
1일·1주 예측은 그 안에 들어오니 괜찮았지만, 1개월·6개월·1년짜리 예측은 시작 시점이 항상 그 한 달 범위 밖이었다. 시작일 가격을 찾으려 해도 받아온 데이터에 없으니 매번 실패했고, 그 예측은 영원히 열린(open) 상태로 남았다. 열린 예측에는 개수 상한이 있어서, 안 닫히는 예측이 쌓이면 그만큼 새 예측을 넣을 자리가 줄어든다. 장기 예측이 통째로 묻혀 있던 것이다.
사용자가 거의 없는 단계라 장기 예측 자체가 적었고, 그래서 이 버그는 화면 어디에도 드러나지 않았다. “데이터가 적어서 안 보이던 버그”의 전형이었다.
날짜가 안 맞으면 “아무 가격으로나 확정”
이게 더 무서웠다. 마감할 때 해당 날짜의 가격이 없으면, 코드가 “없으면 가장 최근 가격을 쓴다”는 식으로 짜여 있었다. 프로그래밍에서 흔한 값 ?? 기본값 패턴인데(앞 값이 비면 뒷값을 쓴다는 뜻), 하필 그 기본값이 엉뚱한 날짜의 가격이었다.
그러니까 6월 10일 종가로 확정해야 할 예측을, 그 데이터가 없으면 그냥 가장 최근에 있던 6월 2일 가격으로 확정해버릴 수 있었다. 게다가 마감된 예측의 점수는 다시 바꾸지 않는 불변 값이라, 한 번 틀리게 확정되면 그대로 영구히 오염된다.
고치는 방향은 단순했다. 정확한 날짜의 가격이 없으면 확정하지 않고 다음 실행으로 미룬다. 교훈도 분명했다. ?? 기본값은 편하지만, 틀린 값으로 조용히 성공하는 것이 그냥 실패하는 것보다 위험할 때가 있다. 특히 그 값을 나중에 못 고칠 때.
한 글자 때문에 모든 종목의 기준 확률이 옛날에 고정
LDBD는 각 종목이 “그동안 얼마나 자주 올랐나”를 기준 확률(base rate)로 잡아둔다. 이게 예측 난이도를 보정하는 데 쓰인다. 늘 오르기만 하는 종목을 “오른다”고 맞히는 건 쉬운 일이니 점수를 덜 주고, 잘 안 오르는 종목을 맞히면 더 쳐주는 식이다.
그런데 이 기준 확률을 계산하는 코드가 가장 오래된 데이터부터 읽고 있었다. 정렬 방향을 정하는 값 하나가 뒤집혀 있었던 것이다(데이터베이스 질의에서 오래된 순으로 정렬한 뒤 앞에서부터 일정 개수만 읽는 구조였다). 십수 년 전 데이터까지 미리 채워둔(백필된) 종목은 기준 확률이 옛날 시장 상황에 고정됐고, 최근 데이터가 아무리 쌓여도 갱신되지 않았다.
이 값이 모든 예측의 점수 보정에 들어가니, 정렬 방향을 정하는 한 글자(오래된 순을 최신 순으로)가 전 종목 점수에 조용히 스며들고 있었던 셈이다. 가장 사소해 보이는 버그가 가장 광범위했다.
이 버그들의 공통점
공통점은 앞에서 말한 그대로다. 전부 에러를 안 내고, 화면도 멀쩡하고, 사용자도 모른다. 측정하거나 감사하지 않았으면 영영 몰랐을 것들이다. 사용자가 거의 없는 단계라 실제 피해는 없었지만, 바로 그래서 데이터가 적을 때 미리 고치는 게 싸다는 걸 다시 확인했다. 나중에 예측이 수만 건 쌓인 뒤에 같은 버그를 발견했다면 되돌릴 방법이 없었을 것이다.
고친 것 2 — 그런데 그 수정이 더 나쁜 버그를 만들었다
여기서부터가 이번 회차에서 가장 배운 게 많았던 대목이다.
위의 첫 번째 버그(장기 예측 미마감)를 고치는 과정에서, Opus가 가격을 가져오는 방식을 바꿨다. 매번 외부에서 새로 받아오는 대신, 이미 저장해 둔 가격 테이블에서 시작일·종료일 가격을 직접 조회하도록 바꾼 것이다. 깔끔해 보였고, 빌드도 통과했고, 타입 에러도 없었다.
그런데 그 수정을 다시 Fable 5에게 리뷰시키자, 이게 원래 버그보다 더 나쁜 버그를 만들었다는 지적이 돌아왔다.앞에서 말한 “고치고 다시 감사받는” 왕복이 진가를 발휘한 순간이었다.
문제는 수정종가(adj_close)라는 개념에 있었다. 주식 가격에는 그냥 종가가 있고, 배당이나 액면분할 같은 이벤트를 반영해 소급 조정한 수정종가가 따로 있다. 예를 들어 어떤 주식이 10:1로 분할되면(주식 1주가 10주로 쪼개지면) 가격도 1/10이 되는데, 과거 가격까지 전부 1/10로 다시 계산해서 일관성을 맞춘다. LDBD의 수익률 계산은 이 수정종가를 기준으로 한다.
수익률 계산에서 중요한 건, 시작일 가격과 종료일 가격이 같은 조정 기준에서 나온 값이어야 한다는 점이다. 둘 다 최신 수정종가 기준이면 괜찮고, 둘 다 당시 원시 종가 기준이어도 일관성은 있다. 문제는 둘이 서로 다른 기준일 때 생긴다. 이 수정종가는 분할·배당이 생길 때마다 과거 값까지 바뀌는데, 이렇게 바꾼 방식은 가격을 저장할 때 “최근 며칠치”만 갱신하므로 오래 전에 저장해 둔 시작일 가격은 그 당시 기준으로 동결돼 있었다. 시작일은 옛날 기준 수정종가인데 종료일은 최신 기준 수정종가가 되니, 둘은 서로 다른 기준의 숫자다. 이걸 섞어 수익률을 내면 그 사이에 분할이 낀 예측은 -90% 같은 엉터리 값으로 확정될 수 있었다.
원래의 방식(매번 외부에서 새로 받아오기)은 시작일과 종료일을 같은 응답에서 한꺼번에 읽었기 때문에 조정 기준이 자동으로 일치했다. “효율적으로 바꾼다”던 그 변경이, 사실은 그 일관성을 깨뜨린 것이다. 다행히 머지하기 전이라 실제 피해는 없었다.
최종 수정은 절충이었다. 외부에서 새로 받아오는 방식은 유지하되, 타임프레임별로 받아오는 범위를 넉넉하게 넓혔다(1일·1주는 1개월, 1개월은 3개월, 6개월은 1년, 1년은 2년치). 이렇게 하면 원래의 첫 번째 버그(범위 부족으로 장기 예측이 안 마감되던 것)도 같이 해결되고, 수정종가 일관성도 깨지지 않는다. 정확한 날짜 매칭과 멱등 가드(같은 작업이 두 번 돌아도 점수가 중복 반영되지 않게 막는 장치) 같은 좋은 부분은 그대로 살렸다. 다만 이 멱등 가드는 이중 반영을 막은 중간 단계고, 완전한 해결(예측 마감과 점수 반영을 단일 트랜잭션으로 묶는 것)은 다음 작업으로 남겨뒀다.
여기서 얻은 교훈이 두 개다.
- AI 협업의 핵심은 여전히 검증이다.개발 일지 #1의 “구현은 위임, 판단은 위임 안 함”이, 여기서는 “AI가 만든 수정도 다른 AI로 한 번 더 리뷰”로 한 겹 더 늘어났다. 코드를 짠 AI도, 그걸 리뷰한 AI도, 그리고 나도, 각자 한 겹씩 다른 문제를 잡았다. 어느 한 겹만 빠졌어도 -90%짜리 오염이 그대로 머지될 뻔했다.
- 빌드 통과가 올바름을 뜻하진 않는다. 빌드 통과는 문법이 맞다는 뜻이지, 도메인 의미가 맞다는 뜻은 아니었다. 타입도 맞고 빌드도 통과한 코드가 수정종가의 소급 조정이라는 도메인 사실에서 틀렸다. 이런 건 컴파일러가 잡아주지 못한다. 결국 이 코드가 현실에서 무엇을 뜻하는지는 사람이 봐야 한다.
고친 것 3 — 보안: 읽기는 막았는데 쓰기 우회를 안 막음
감사에서 나온 가장 심각한 항목은 보안 쪽이었다. 이건 이미 패치해서 운영에 반영했기 때문에 큰 틀만 적는다.
LDBD는 데이터베이스에 행 수준 보안(RLS)이라는 규칙을 걸어둔다. 쉽게 말하면 누가 어떤 데이터를 읽고 쓸 수 있는지를 데이터베이스가 직접 통제하는 장치다. 정상적인 예측 제출은 서버를 거치고, 서버가 시작 시점·점수 배수 같은 값을 전부 직접 계산해서 넣는다. 사용자가 손댈 수 없는 값들이다.
문제는 그 서버 검증을 우회하는 직접 쓰기 경로가 막혀 있지 않았다는 것이다. 이론상 서버가 계산해야 할 값이 사용자 입력으로 들어갈 수 있었고, 그러면 리더보드 점수가 왜곡될 가능성이 있었다. 읽기 권한은 신경 써서 막아뒀는데, 정작 같은 테이블에 대한 쓰기 경로가 충분히 닫혀 있지 않았던 셈이다.
수정은 작았다. 직접 쓰기를 데이터베이스 정책으로 막고(정상 서버 경로는 그대로 작동한다), 예측을 써 넣는 코드가 정말 서버 한 곳뿐인지부터 확인한 다음 적용했다. 가장 심각했던 보안 구멍의 해법이 거창한 재작성이 아니라 정책 한 줄에 가까웠다는 게 인상적이었다. 작고 정확한 수정이 큰 재작성보다 나을 때가 있다.
교훈으로 남길 만한 건 “RLS의 가장 흔한 함정”이다. 읽기 정책은 꼼꼼히 거는데, 같은 테이블에 대한 쓰기 우회 경로는 깜빡하기 쉽다. 읽기와 쓰기를 따로따로 막아야 한다는 걸 머리로는 알아도, 실제로는 한쪽만 챙기게 된다.
고친 것 4 — 시간 계산이 거래소 달력을 너무 순진하게 봤다
시장이 언제 열리고 닫히는지를 판단하는 코드에도 두 가지 문제가 있었다.
하나는 서머타임(DST) 처리였다. 미국 시장 시각을 계산하려고 “3월 둘째 일요일에 시작해서 11월 첫째 일요일에 끝난다”는 서머타임 공식이 코드에 박혀 있었는데, 이게 며칠씩 어긋나서 전환 주간에는 장 마감·개장 판정이 틀렸다. 그 공식을 버리고, 그냥시장 현지 시각으로 변환해서 읽는 방식으로 바꿨다. 서머타임이 언제 바뀌는지는 런타임이 알아서 처리하니, 손으로 계산할 이유가 없었다.
다른 하나는 더 미묘했다. 장이 마감된 뒤부터 그날 가격을 수집하기 전까지 한두 시간 동안, 기준 시점이 “어제”로 남아 있었다. 이 틈에는 이미 공개된 오늘 종가를 보고 예측을 제출하면 유리했다. 결과를 알고 베팅하는 셈이다. 기준 시점을 시장 달력과 시계 기준의 직전 거래 세션으로 정확히 계산하도록 고쳤다.
솔직하게 적자면, 이건 처음 이 시간 계산을 설계할 때 거래소 달력을 너무 순진하게 본 탓이었다. 주말만 알았지 공휴일도, 서머타임도 제대로 못 봤다. 그리고 이 버그를 파고들다 보니 더 큰 미구현이 드러났다. LDBD에는 아직 완전한 휴일 달력(미국 정규 공휴일, 한국 음력 명절)이 없다. 지금은 주말과 수동으로 등록한 예외일만 안다. 이건 데이터 파이프라인 작업이라 따로 떼어 두기로 했다. 한 버그를 파보니 더 큰 미구현이 드러나는 일도 흔하다.
이번에 다시 확인한 협업 패턴
개발 일지 #1에서 정리한 패턴들이 이번엔 다른 모양으로 반복됐다.
감사도 위임하되, 검증은 위임하지 않는다
AI에게 코드 전체를 훑어 후보를 뽑게 하는 건 엄청나게 빠르고 유용했다. 사람 혼자였다면 며칠이 걸렸을 점검을 다섯 갈래로 동시에 끝냈다. 하지만 그 리포트를 진실로 받아들이는 순간 함정에 빠진다. 무엇이 진짜 문제인지 실제 코드로 가려내는 일은 여전히 사람이 해야 했다.
감사와 리뷰는 Fable, 구현은 Opus — 비용이 만든 분업
왜 Fable에게 수정까지 다 시키지 않았을까. 솔직한 이유는 비용이었다. Fable 5는 같은 작업에도 토큰(AI가 글을 읽고 쓰는 양을 재는 단위이자 사용료 단위)을 꽤 많이 쓴다. 그래서 가장 값어치 있는 일, 즉 코드 전체를 읽고 문제를 짚어내는 감사와 그 수정이 맞는지 따지는 리뷰만 Fable에게 맡기고, 실제로 코드를 고치는 일은 Opus에게 넘겼다. Fable한테 처음부터 끝까지 다 맡겼으면 토큰은 더 들어도 결과가 더 좋았을지도 모른다. 다만 지금 단계에서는 이 분업이 비용 대비 가장 합리적이었다.
돌이켜보면 이 구도는 시니어 개발자와 주니어 개발자의 분업과 닮아 있었다. 경험 많은 시니어(Fable)가 코드 전체를 훑어 문제를 짚고 수정이 맞는지 리뷰를 맡고, 주니어 (Opus)는 그 방향대로 실제 코드를 친다. 시니어의 시간이 비싸니 판단과 리뷰처럼 값어치 큰 일에 그 시간을 쓰고, 손이 많이 가는 구현은 주니어에게 넘기는 셈이다. 물론 Opus도 주니어라고 부르기엔 LDBD를 통째로 만들어낸 실력이지만, 둘을 한자리에 놓고 보니 역할이 자연스럽게 그렇게 갈렸다.
그런데 이 분업에는 뜻밖의 부수 효과가 있었다. 나는 Opus로 LDBD를 만들면서 코드가 꽤 탄탄하다고 믿고 있었는데, 같은 코드를 Fable 5에게 감사시키자 그 믿음에 가려져 있던 문제가 한 무더기 나왔다. 코드를 짠 모델 스스로는 잘 안 보이는 사각지대가, 다른 모델의 눈에는 선명하게 보였던 것이다. Opus가 낸 -90% 오염 수정을 Fable의 리뷰가 잡아준 것도 같은 맥락이다. 한 모델만으로 같은 작업을 했다면 그 -90%는 그대로 운영에 올라갔을 것이다.
가장 사소해 보이는 변경이 가장 광범위할 수 있다
정렬 방향 한 글자가 전 종목 점수에 스며들었고, 정책 한 줄이 가장 심각한 보안 구멍을 닫았다. 코드의 영향 범위는 변경의 크기에 비례하지 않는다. 작아 보이는 줄일수록 오히려 더 조심해서 봐야 했다.
마무리 — 측정하지 않으면 모르는 버그들
이번 회차에서 고친 버그들은 대부분 조용한 것들이었다. 에러를 내지도, 화면을 깨뜨리지도 않았다. 사용자가 거의 없는 지금이라 실제 피해는 없었지만, 그래서 더더욱 지금 점검하길 잘했다고 생각한다. 데이터가 적을 때 고치는 비용이 가장 싸다.
이번 작업의 결론은 단순했다. AI를 더 많이 쓸수록 사람의 검증이 덜 중요해지는 게 아니라 오히려 더 중요해졌다. AI에게 구현을 맡길수록, 검증의 겹은 더 두꺼워져야 했다. 다만 이번엔 그 검증의 일부도 다른 AI에게 맡길 수 있었다. 구현은 Opus가 하고, 감사는 Fable이 하고, 나는 둘 사이에서 무엇이 진짜 문제인지 확인했다. 그 삼각형이 이번에는 잘 맞아 돌아갔다.
아쉬운 건 이 삼각형의 한 축이던 Fable 5를 지금은 쓸 수 없다는 점이다. 다행히 가장 위험한 구멍들은 그 전에 닫았다. 다음 모델이 또 어떤 사각지대를 비춰줄지는, 그때 가서 다시 한 편 쓸 일이다.
본인 봇이나 예측을 LDBD에 올려보고 싶다면 /settings에서 identity와 (필요 시) API key를 만들면 된다. 가입은 메인 페이지에서, 사용은 무료다.
Fable 5 접근 중단 관련 보도: Anthropic 공식 입장, CNBC, Bloomberg. (글을 쓰는 시점 기준이며, 상황은 바뀔 수 있다.)