색인할 때 계산해 저장한 값은, 규칙을 고쳐도 옛 데이터에 안 따라옵니다
사내 문서 지식그래프 검색 도구를 만들고 있습니다. 같은 문서가 여러 판본으로 들어오는 환경이라, 색인할 때 판본을 하나로 묶고 그중 하나에 '최신 표시'를 붙입니다. 채팅이 근거를 고를 때는 최신으로 표시된 것만 봅니다. 옛 판본이 근거로 섞여 나오면 답이 틀리기 때문입니다.
열흘 전에 그 묶는 규칙을 한 줄 고쳤습니다. 주마다 같은 이름으로 나오는 문서 종류가 있는데, 이건 판본이 아니라 각자 다른 문서입니다. 그래서 '이 종류는 판본으로 묶지 않는다'로 바꿨습니다. 코드를 고쳤고 테스트는 초록불이었고 그걸로 끝인 줄 알았습니다. 열흘 뒤에 확인한 사실은, 그 규칙이 닿은 범위가 그날 이후 새로 색인된 문서뿐이었다는 것입니다.
분명히 있는 문서가 검색 근거에 안 들어왔습니다
문서 하나를 찾다가 이상한 걸 짚었습니다. 그 문서가 색인에 있는 건 확인했는데 채팅이 근거로 쓰지 않았습니다. 행을 열어 보니 최신 표시가 꺼져 있었습니다. 주마다 나오는 같은 이름의 문서 다섯 건이 옛 규칙대로 한 묶음으로 묶여 있었고, 그중 하나만 최신으로 남아 나머지 네 건이 근거 후보에서 빠진 상태였습니다.
열흘 동안 아무 신호도 없었습니다. 이 실패에는 걸 자리가 없기 때문입니다.
에러가 나지 않습니다 — 묶음 키와 최신 표시는 색인할 때 정상적으로 계산돼 저장된 값입니다. 다만 옛 규칙으로 계산된 값입니다
로그에 안 남습니다 — 검색 시점에는 저장된 표시를 읽기만 하므로 판단하는 코드가 돌지 않습니다
검색 결과가 비지 않습니다 — 남은 한 건으로 채팅은 그럴듯하게 답했습니다. 빠진 네 건을 아무도 못 셉니다
없어진 것을 감지하려면 없어졌다는 걸 이미 알아야 합니다. 그래서 알림을 만들 수 없고, 방법은 하나뿐입니다. 한 건이 이상해 보이면 같은 모양을 전체로 세는 것입니다. 이번에도 한 건에서 출발해 전체를 세니 같은 모양이 139건이었습니다.

전체를 다시 색인할까, 그 값만 다시 계산할까
고칠 방법은 둘이었습니다. 둘의 차이는 '원문을 다시 읽어야 하나'였습니다.
방법 | 비용 | 고쳐지는 범위 |
|---|---|---|
전체 재색인 | 문서 수만큼 파싱과 임베딩을 다시 지불 | 모든 파생값이 새 규칙을 받는다 |
저장된 행만 재계산 | 저장된 메타를 읽어 다시 계산하는 비용만 | 묶음 키와 최신 표시만 고쳐진다 |
재계산을 골랐습니다. 판본 묶음은 파일 이름과 메타에서 정해지는 값이라 원문을 다시 읽을 필요가 없었습니다. 저장된 행을 읽어 새 규칙으로 묶음과 최신 표시만 다시 계산해 덮는 도구를 하나 만들어 한 번 돌렸고, 288건이 다시 근거 후보로 돌아왔습니다.
도구보다 목록이 더 중요했습니다
재계산 도구는 이번 한 번의 문제를 지웠을 뿐입니다. 다음에 같은 일이 안 생기게 하는 것은 다른 산출물이었습니다. '색인할 때 굳는 값'을 세어 적어 뒀고 다섯 가지였습니다.
판본 묶음 키 — 같은 문서의 여러 판본을 묶는 기준
최신 표시 — 묶음 안에서 어느 판본을 근거로 쓸지
청크 본문 — 파서가 자른 결과 그 자체
작성자 편 구분 — 우리 쪽 문서인지 남의 쪽 문서인지
외부 기관 표시 — 문서가 어느 밖의 조직에 붙는지
목록이 있으면 규칙을 고칠 때 물을 것이 하나로 정해집니다. '내가 고친 규칙이 이 다섯 중 무엇을 만드나'입니다. 하나라도 걸리면 재계산이 코드 수정과 한 벌로 따라와야 합니다. 걸리는 게 없으면 그냥 배포하면 됩니다. 이 질문은 목록이 있어야 1분이고, 없으면 매번 코드를 뒤져야 해서 결국 안 하게 됩니다.
같은 날 '고쳤다'와 '닿았다'가 갈린 자리가 둘 더 있었습니다
공교롭게도 같은 날 같은 모양의 실수를 두 번 더 했습니다. 파서를 고치고 그 파서를 직접 불러 숫자를 쟀는데, 실제 색인 경로는 그 함수를 타고 있지 않았습니다. 그리고 고친 코드를 서버에 배포하지 않은 채로 서버에서 값을 재고 있었습니다. 셋 다 테스트는 통과였고 실측 숫자도 나왔습니다.
공통점은 하나입니다. 코드를 고치는 것과 그 변경이 데이터·실행 경로에 닿는 것은 서로 다른 두 개의 일입니다. 앞의 일만 하고 '됐다'고 보면 뒤의 일이 통째로 비는데, 그 상태에서도 화면은 정상이고 테스트는 초록불입니다.
조용히 틀린 결과는 겉으로 드러나지 않습니다. 파서가 낱말을 가른 탓에 검색만 빗나갔던 사례도 같은 모양이었습니다.

굳히는 설계와 굳히지 않는 설계
비슷한 시기에 다른 제품에서 같은 기능을 반대로 만든 것을 봤습니다. 그쪽은 판본 묶음을 저장하지 않고 검색할 때 계산합니다. 그래서 규칙을 고치면 그 순간 옛 문서에도 적용됩니다. 재계산 도구도, 굳는 값 목록도, '이 값이 굳어 있나'라는 질문도 필요하지 않습니다.
파생값을 어디서 계산하나는 성능 선택처럼 보입니다. 실은 '규칙을 고쳤을 때 누가 따라오나'를 정하는 선택입니다. 색인할 때 계산하면 조회가 싸지는 대신 규칙을 고칠 때마다 재계산이라는 숙제가 붙습니다. 검색할 때 계산하면 그 숙제가 없고 대신 질의마다 계산 비용을 냅니다.
그렇다고 굳히는 쪽이 틀린 것은 아닙니다. 문서가 아주 많고 규칙이 거의 안 바뀌면 굳히는 쪽이 분명히 싸고, 저도 그래서 그렇게 뒀습니다. 갈림선은 문서 수가 아니라 '이 규칙을 앞으로 몇 번 고칠 것 같나'입니다. 저는 한 번도 안 고칠 것처럼 설계해 두고 열흘 전에 고쳤습니다.
같은 모양을 쓰고 있다면 점검할 것
비정규화 컬럼, 검색 색인, 캐시, 구체화한 뷰, 이벤트가 일어난 시점에 박아 둔 스냅샷. 전부 '만들 때의 규칙'을 값 안에 담고 있어서 같은 자리에 섭니다.
쓸 때 계산해 저장하는 파생값이 무엇인지 적힌 목록이 있는지 — 없으면 규칙을 고칠 때 확인을 건너뛰게 됩니다
규칙을 바꾸는 변경에 재계산 작업이 한 벌로 붙는지 — 코드 수정만으로 끝나면 옛 데이터는 그대로 남습니다
그 파생값이 틀렸을 때 에러가 나는지 — 안 난다면 세는 것 말고 발견 수단이 없습니다
원문을 다시 읽지 않고 재계산할 수 있는 값인지 — 파싱·임베딩을 다시 해야 하는 값은 목록에 있어도 미루게 됩니다
그 규칙을 앞으로 몇 번 고칠 것 같은지 — 자주 고칠 값이면 애초에 저장하지 않는 쪽이 쌉니다
정답률 숫자를 올리는 작업도 대부분 프롬프트가 아니라 이런 데이터 쪽 커밋에서 나왔습니다.
마무리
고친 것은 규칙 한 줄이었고, 놓친 것은 그 규칙으로 이미 만들어진 값 139건이었습니다. 재계산 도구를 돌려 되살렸지만 열흘은 되돌리지 못했습니다. 그 열흘 동안 채팅은 남은 한 건으로 계속 답했고, 답이 비지 않았으므로 아무도 이상하게 여기지 않았습니다.
남은 것은 다섯 줄짜리 목록 하나입니다. 다음에 규칙을 고칠 때 저는 '고쳤나'를 묻지 않고 '이 값이 굳어 있나'를 묻습니다. 목록이 짧아서 1분이면 끝나고, 짧게 유지하는 것이 이 목록을 계속 보게 하는 조건입니다.
