매달 다시 재는 판정이 계좌 101건을 사유 없이 지웠습니다
한 B2B 채권 분석 시스템은 계좌마다 변제가 어디까지 진행됐는지를 월 단위 원장으로 만듭니다. 9월 원장을 뽑아 확인하는 자리에서 이상한 것이 보였습니다. 8월에 멀쩡히 상각이 돌던 계좌 101건이 9월 원장에 없었습니다.
없는 것 자체보다 더 곤란한 것은 그 다음이었습니다. 그 계좌들은 상각 행도 없고, 상각이 중단됐다는 행도 없었습니다. 원장을 정리하는 사람 입장에서는 왜 빠졌는지를 물을 대상조차 없는 상태였습니다.
판정에서 탈락한 것이 아니라 기록에서 사라진 상태였습니다
대상에서 빠지는 것과 대상에서 사라지는 것은 다릅니다. 빠지는 것에는 사유가 따라옵니다. 중단 행이 하나 생기고, 거기에 언제 어떤 이유로 멈췄는지가 남습니다. 이 101건에는 그 행이 없었습니다.
빠질 때 계좌 참조 테이블에 중단 표시가 들어가야 중단 행이 생성되는 구조였는데, 판정에서 떨어진 계좌는 그 경로를 아예 타지 않았습니다. 결과적으로 원장에는 '있었다가 없어진 계좌'만 남았습니다. 이렇게 되면 기록만으로 원인을 추적할 수 없어 과거 코드 변경까지 거슬러 올라가야 합니다.
열흘 전 커밋이 주석 처리된 조건 하나가 다시 활성화됐습니다
원인은 열흘 전 커밋이었습니다. 적격 판정 함수 안에 조건 한 덩어리가 주석으로 꺼져 있었습니다. 수익률 상한을 넘으면서 원가가 낮은 계좌를 걸러내는 조건이었습니다. 다른 작업을 하던 중 다른 작업 중 해당 조건의 주석 처리가 해제됐습니다.
그 자리에서는 타당한 변경이었습니다. 조건 자체가 틀린 것도 아니었고, 테스트도 깨지지 않았습니다. 다만 그 판정 함수가 상각 판정 경로에서도 불린다는 것은 확인되지 않았습니다. 함수 하나를 고쳤는데 그 함수를 부르는 다른 경로가 같이 움직였습니다.
주석은 이미 정확히 경고하고 있었습니다. '켜면 지금 통과하던 건이 빠진다, 영향 범위를 세고 정한다'는 문구가 조건 바로 위에 붙어 있었습니다. 경고가 주석으로만 남아 있어 영향 범위 측정을 강제하지 못했습니다.
그리고 달이 넘어갔습니다. 8월 원장은 조건이 꺼진 상태로 이미 생성돼 있었고, 9월 원장만 켜진 상태로 생성됐습니다. 같은 계좌가 달을 넘기면서 판정이 뒤집혔습니다.

조건을 되돌릴 것인가, 판정 시점을 옮길 것인가
고칠 때 두 갈래가 있었습니다.
갈래 | 내용 | 감수하는 것 |
|---|---|---|
조건을 되돌린다 | 주석으로 다시 꺼서 8월과 같은 상태로 되돌린다 | 빠르지만 같은 자리가 다음에 또 열린다. 켠 조건 자체는 틀리지 않았다 |
재는 시점을 옮긴다 | 조건은 그대로 두고 언제 재는가를 바꾼다 | 저장 칸이 하나 늘고, 손댈 자리가 네 곳이다 |
되돌리는 쪽은 이번 달의 숫자는 맞춥니다. 그런데 문제의 원인이 조건에 있지 않았습니다. 원인은 판정을 매달 다시 잰다는 것이었습니다. 조건을 되돌려도 다음에 규칙이 바뀌는 날 같은 일이 그대로 일어납니다.
그래서 판정 시점을 옮기는 방안을 선택했습니다.
생성할 때 한 번만 판정하고 이후에는 그 기록만 읽습니다
계좌 참조 행이 만들어질 때, 즉 개시하거나 재조정할 때 한 번만 판정합니다. 그 결과를 그 행의 판정 메모 칸에 남깁니다. 원장 생성도, 이어받기도, 중단 행 생성도 이제 그 메모만 읽습니다. 판정 함수를 다시 부르는 자리는 없습니다.
판정 메모 칸은 생성 시에만 기록되고 그 뒤로는 누구도 건드리지 않습니다. 그래서 원장 생성을 몇 번 다시 돌려도 같은 값이 나옵니다. 규칙이 다음 달에 또 바뀌어도 이미 생성된 행의 판정은 해당 행에 기록돼 바뀌지 않습니다. 새 규칙은 새로 생성되는 행부터 적용됩니다.
손대야 할 자리는 네 곳이었습니다.
월 원장 생성 — 판정 함수 호출을 빼고 메모 칸을 읽도록 바꿉니다
개시 판정 — 원래 판정을 거치는 경로라 메모가 그대로 찍힙니다
승계 재조정 — 이전 행을 이어받아 메모가 늘 비는데, 비어 있음이 곧 판정 안 함, 즉 통과라 따로 손댈 것이 없었습니다
중단 행 생성 — 같은 메모를 보고 중단 여부를 정합니다
같이 지운 것이 하나 더 있습니다. 코드에 월 초 기준일 하나가 하드코딩돼 있어 그 날짜 전후로 판정이 갈리는 경로가 있었습니다. 판정 시점을 생성 시점으로 고정하자 날짜로 갈래를 나눌 이유가 없어져서 그 하드코딩을 지웠습니다.

켠 조건이 어디까지 걸리는지 세고 나서 배포했습니다
코드 수정 자체보다 영향 범위를 측정하는 데 더 많은 시간이 들었습니다. 이번 사고의 원인이 '세지 않고 켰다'는 것이었으니, 고치면서 같은 것을 반복할 수는 없었습니다.
미생성 계좌 101건이 0건으로 돌아왔습니다
전월에 중단된 계좌 참조 행 31,135건 중 278건이 새로 중단 행을 받았습니다. 반대로 잃는 건은 0이었습니다
9월 이후 새로 개시되는 건도 따로 셌습니다. 수익률 상한 조건 단독으로 떨어지는 건이 38건이고, 그중 13건은 예전처럼 두 조건을 모두 만족해야 했다면 통과할 건이었습니다
저원가 조건 단독으로 떨어지는 건은 7,906건이었습니다
배포 후에는 9월 상각 행이 실제로 물고 있는 계좌 참조 행 278,155행 전량을 조인해, 새 기준으로 사라지는 상각이 0임을 확인했습니다
조건 두 개를 단독으로 가른 것이 원래 의도였는지는 이 자리에서 판단하지 않고 확인 항목으로 올렸습니다. 다시 묶으면 7,906건이 움직이므로 코드 수정 담당자만으로 결정하기에는 영향 범위가 큽니다.
운영 경로에서는 그 적격 판정 함수 호출이 전부 사라졌습니다. 검수 쪽 한 곳이 아직 남아 있어서 후속으로 뺐습니다.
무엇을 기록으로 고정하고 무엇을 다시 판정할지가 기준입니다
이 건을 정리하고 나니 판단 기준이 하나로 보입니다. 사건에 묶인 판정은 굳히고, 현재 상태를 묻는 값은 다시 잽니다.
'이 계좌가 그때 적격이었나'는 그때의 사실입니다. 매달 다시 재면 규칙이 바뀔 때마다 과거가 소급해서 바뀝니다. 반대로 '지금 이 계좌가 연체인가'는 판정을 고정하면 기준이 바뀐 뒤에는 값이 낡을 수 있습니다. 같은 테이블 안에 두 종류가 섞여 있고, 섞여 있다는 것을 모른 채 같은 방식으로 다루면 한쪽이 반드시 틀립니다.
확인하는 방법은 단순합니다. 같은 입력으로 두 번 돌려서 같은 답이 나오는지 봅니다. 답이 갈리면 그 판정은 어딘가에서 바깥 상태를 읽고 있습니다.

판정을 고정하는 설계에는 재계산 경로가 함께 있어야 합니다
판정을 고정하면 기준이 바뀐 뒤에는 값이 낡을 수 있습니다. 생성 시점의 판정을 계속 신뢰하는 구조이므로, 그 판정이 틀렸을 때 고칠 창구가 따로 필요합니다. 이 건에서는 강제 재계산 경로가 그 자리를 맡았습니다. 굳히는 쪽으로 설계한다면 되살릴 손잡이를 같은 작업에서 같이 만드는 편이 안전합니다.
모든 도메인에 맞는 방식도 아닙니다. 법이 바뀌어 과거까지 소급 적용해야 하는 계산이나, 잘못된 기준으로 생성된 데이터를 바로잡는 정정 배치에서는 굳힌 값이 오히려 장애물이 됩니다. 그럴 때는 재판정 경로를 따로 열어 둡니다.
판정 대상이 짧게 사는 시스템에도 과합니다. 하루 안에 끝나는 배치라면 매번 다시 재도 그 사이에 규칙이 바뀔 틈이 없습니다. 이 문제는 한 건의 수명이 규칙의 변경 주기보다 길 때 생깁니다.
같은 사고를 막는 점검 항목
판정 함수를 고치기 전에 그 함수를 부르는 경로를 전부 셉니다. 함수 하나가 아니라 경로 전부를 고치는 일입니다
꺼져 있는 코드의 주석은 켜는 사람을 향해 씁니다. 경고 문구만 두지 말고, 영향 건수를 세는 쿼리를 주석 옆에 같이 둡니다
대상에서 빠지는 모든 경로에 사유 행이 남는지 확인합니다. 사유가 없으면 원인 조사를 못 합니다
월 단위로 다시 도는 배치에서, 두 달의 판정 기준이 다를 수 있는지 봅니다
배포 후 검증은 표본이 아니라 실제로 물려 있는 행 전량으로 돌립니다
이 사고에서 가장 오래 남는 장면은 주석 문구였습니다. 켜면 무슨 일이 생기는지가 이미 정확히 적혀 있었는데, 그 경고가 주석 안에만 있었습니다. 코드가 아닌 자리에 적힌 규칙은 지켜지지 않는 편이 정상에 가깝습니다.
조용히 빠지는 데이터는 이 건만의 이야기가 아닙니다. 코드가 기다리는 표기와 실제 데이터의 표기가 어긋나 0건이 나온 사례를 같이 보면, 빠지는 자리에 흔적을 남기는 일이 왜 먼저인지가 더 선명해집니다.
스스로 경고하던 코드를 지웠는데 테스트가 그대로 통과한 이야기도 같은 축에 있습니다. 경고를 코드 밖에 두면 아무도 강제받지 않는다는 점에서 이 건과 원인이 같습니다.