항목 하나를 지웠는데 지난달 손익이 같이 사라졌습니다
프랜차이즈 ERP 의 손익계산서에 엑셀 업로드를 붙이는 작업이었습니다. 매장마다 손익 양식이 제각각이라 본사 담당자가 화면에 손으로 넣고 있었고, 특정 매장의 특정 월 데이터를 파일 내용으로 통째로 교체하는 업로드 기능을 만들기로 했습니다. 업로드 규칙을 맞추다가 손익 항목이 어떻게 저장되는지를 들여다봤는데, 거기서 예상하지 않은 것이 나왔습니다.
손익 항목은 공통 항목과 매장 전용 항목으로 구성됩니다. 그런데 화면의 '항목 삭제'가 항목 마스터에서 해당 항목을 삭제하고 있었고, 지난달 화면은 그 마스터를 기준으로 그려지고 있었습니다. 두 사실을 이어 붙이면 결론이 하나뿐입니다.
이번 달을 정리했는데 지난달 줄이 사라졌습니다
이번 달 데이터를 정리하면서 항목 하나를 삭제하면, 그 항목에 금액이 들어 있던 지난달 화면에서도 그 줄이 같이 빠집니다. 값이 지워진 것이 아닙니다. 데이터베이스에는 그대로 있는데 화면이 그 줄을 안 그리는 상태입니다.
실제로 그런 매장이 세 곳 있었습니다. 한 곳은 담당자가 이번 달을 입력하면서 거래처 이름이 그대로 박힌 매장 전용 원가 항목을 표준 항목으로 옮기다가 옛 항목을 지운 경우였습니다. 나머지 두 곳은 결제수수료를 플랫폼별로 나누는 재편을 하면서 예전의 합계 행을 삭제한 경우였습니다. 세 건 모두 그 달을 고치려던 작업이 아니라 다른 달을 고치려던 작업이었습니다.
이 결함의 성격이 까다롭습니다. 예외도 안 나고 로그도 안 남습니다. 화면은 정상적으로 열리고 계산도 오류 없이 실행됩니다. 원가와 판관비 합계만 낮게 나와, 화면과 실행 로그만으로는 오류를 알아차리기 어려웠습니다.
사용자 실수만으로 볼 수 있는 문제가 아닙니다. 담당자는 '이 항목 이제 안 쓴다'는 뜻으로 눌렀고, 시스템은 '전 기간에서 없앤다'로 실행했습니다. 버튼 하나에 두 뜻이 겹쳐 있었고 어느 쪽인지가 아무 데도 적혀 있지 않았습니다.

과거를 무엇으로 그릴지 두 가지를 놓고 골랐습니다
데이터 모델을 고치기 전에 두 방식을 비교했습니다. 둘 다 장단이 분명했습니다.
방식 | 장점 | 대가 |
|---|---|---|
항목 마스터 기준 (원래) | 구현이 단순하다. 항목을 정리하면 전 기간에 즉시 반영된다 | 마스터가 곧 과거의 해석기다. 마스터를 건드리면 과거가 따라 움직인다 |
그 달의 행 기준 (채택) | 항목을 내려도 지난달은 그대로다. 그 달의 구성이 그 달에 남는다 | 행이 없는 것과 0원인 것을 구분할 수단이 따로 필요하다 |
해당 월에 저장된 행을 기준으로 삼았습니다. 그 달에 행이 있으면 그 행들의 항목 집합이 그 달의 손익이 됩니다. 같이 '항목 삭제'의 의미 자체를 좁혔습니다. 이제 삭제는 그 달에서만 적용됩니다.
재무·정산·집계처럼 '그때의 값'이 곧 의미인 도메인에서는 이쪽이 맞습니다. 마스터는 새 입력을 도울 뿐이고 과거를 해석해서는 안 됩니다. 다만 모든 도메인이 이래야 하는 것은 아닙니다. 상품 카탈로그나 조직도처럼 지금의 정의를 전 기간에 일관되게 보여주는 것이 목적인 화면도 있습니다. 거기서 월별 스냅샷을 만들면 정의가 바뀔 때마다 과거가 갈라져서 오히려 비교가 안 됩니다.
빈 값 하나로 두 가지를 말하고 있었습니다
데이터 모델을 바꾸자 다음 문제가 드러났습니다. 그 달에 행이 아예 없는 것과, 행은 있는데 금액이 0원인 것이 저장 모양으로는 같았습니다. 둘 다 금액 칸이 비어 있었습니다.
그래서 표현을 바꿨습니다. 금액 칸을 비워 두는 대신 '입력 여부'를 따로 두는 칸을 만들고, 금액은 0 으로 채웠습니다. '안 넣은 것'과 '0원으로 넣은 것'이 여기서 갈립니다. 빈 값으로 두는 표현을 유지했다면 이 둘은 계속 저장 상태만으로 구분할 수 없었을 것입니다.
같은 종류의 함정을 다른 도메인에서도 만났습니다. 값은 있는데 코드가 그 값을 못 알아보는 구조입니다.
상태값 하나가 네 가지 표기로 갈려 있어서, 코드가 기다리던 표기는 실제 데이터에 한 건도 없던 사례입니다.

통과 기준을 미리 적어두고 전수로 비교했습니다
데이터베이스는 세 단계로 나눠 적용했습니다. 1단계에서 백업 테이블을 뜨고 '입력 여부' 칸을 추가하며 빈 행 161개를 채웠습니다. 3단계에서 남은 빈 값 162행을 0 으로 확정한 뒤 금액 칸을 필수로 바꿨습니다.
검증은 숫자 하나를 찍어보는 방식이 아니었습니다. 매장과 월을 엮은 화면을 전수로 놓고 단계마다 비교했습니다. 여기서 중요한 것은 비교 자체가 아니라 순서입니다. 각 단계에서 기대하는 차이가 몇 건인지를 먼저 적어두고, 그 건수와 실제 차이가 일치하는지로 통과를 판정했습니다.
단계 | 기대한 차이 | 실제 |
|---|---|---|
DB 1단계 적용 뒤 | 0건 — 표현만 바뀌고 화면은 그대로여야 한다 | 0건 |
백엔드 배포 뒤 | 세 건 — 안 보이던 매장 세 곳의 줄이 돌아와야 한다 | 세 건 |
관리자 화면 배포 뒤 | 0건 — 앞 단계 결과가 유지돼야 한다 | 0건 |
백엔드 배포 단계에서만 세 건이 달라진 것이 곧 모델 변경이 의도대로 동작했다는 증거입니다. 앞뒤 단계에서 차이가 0 이었다는 것도 같은 무게의 증거입니다. 기대치를 적어두지 않았다면 '세 건 달라졌다'는 사실 하나로는 성공인지 사고인지 판단할 근거가 없었을 겁니다.
부수적으로 저장한 적 없는 달에는 새 항목이 0원 줄로 보입니다. 기본 틀 규칙에 따른 것이고 합계는 변하지 않습니다. 이것도 기대치 목록에 미리 적어둔 항목이었습니다. 적어두지 않았다면 배포 뒤에 이 화면을 보고 한참을 뒤졌을 겁니다.

쓰기 전에 보여주는 창구가 검증 도구로 쓰였습니다
마무리로 그 세 매장의 고객 원본 엑셀을 업로드 양식으로 옮겨 다시 넣었습니다. 이때 업로드 기능에 같이 만들어 둔 '미리보기'가 검증 도구 역할을 했습니다. 읽기 전용이라 운영 데이터를 건드리지 않고 결과를 먼저 볼 수 있습니다. 대조해보니 원가·판관비·현금지출액이 원 단위까지 원본과 맞았고, 오류와 경고와 빠지는 항목이 모두 0 이었습니다.
원본을 옮길 때는 요약표가 아니라 세부내역을 기준으로 삼았습니다. 한 매장의 요약표에는 작은 수수료 한 줄이 아예 빠져 있었고, 플랫폼별 수수료와 광고와 할인은 세부내역에만 갈려 있었습니다. 요약표를 기준으로 삼았다면 그 한 줄이 조용히 빠진 채로 확정됐을 겁니다.
원 단위까지 맞는지로 통과를 판정하는 방식은 다른 정산 작업에서도 같은 자리에서 썼습니다.
같은 함정을 미리 피하려면
이번 건에서 남는 점검 항목을 정리하면 다섯 가지입니다. 재무·정산·집계 화면을 설계하거나 인수할 때 먼저 물어볼 것들입니다.
과거 화면을 지금의 마스터로 그리고 있는가. 마스터를 고치면 지난달 숫자가 바뀌는가
삭제 버튼의 범위가 적혀 있는가. 이 달에서 빼는 것인지, 앞으로 안 쓰는 것인지, 전 기간에서 없애는 것인지
빈 값 하나로 두 가지를 말하고 있지 않은가. 안 넣은 것과 0인 것이 같은 모양으로 저장되는가
마이그레이션 단계마다 기대하는 차이 건수를 미리 적었는가. 그 건수로 통과를 판정하는가
운영 데이터를 건드리지 않고 결과를 먼저 보는 창구가 있는가
발견하기 어려운 결함은 오류 없이 데이터가 화면에서 사라지는 경우입니다. 예외도 로그도 안 남고 화면은 멀쩡하며 합계만 낮습니다. 이 종류는 전수 비교 말고는 잡는 방법이 사실상 없습니다.
이 방식이 비용을 옮기는 자리
데이터 모델을 바꾸면서 운영 비용도 늘었습니다. 월별 행 기준은 정리 비용을 늘립니다. 잘못 만든 항목을 전 기간에서 없애려면 달마다 지워야 합니다. 전 기간 정리가 잦은 조직이라면 그 작업용 창구가 따로 필요합니다.
덮어쓰기 업로드도 되돌리기가 비쌉니다. 파일에 없는 매장 전용 항목이 그 달에서 빠지는 규칙이라, 잘못된 파일 한 장이 그 달을 통째로 바꿉니다. 미리보기와 백업이 없으면 채택하기 어려운 설계입니다.
전수 비교도 규모가 커지면 그대로 쓰지 못합니다. 이번에는 대상이 사람이 눈으로 훑을 수 있는 크기여서 가능했습니다. 더 커지면 비교를 자동화하거나 표본과 합계 대조로 내려야 하고, 그때 조용히 안 보이는 결함을 놓칠 위험이 다시 생깁니다.
마무리
화면에서 빠져 있던 세 곳의 금액이 다시 보입니다. 새로 만든 값이 아니라 처음부터 있던 값입니다. 금액 데이터를 새로 만들거나 복구한 것이 아니라, 과거를 무엇으로 그릴지를 바꾼 결과입니다.
이 건의 논점은 사람이 아니라 모델이었습니다. 담당자는 항목을 정리했을 뿐이고, 삭제의 범위를 정의하지 않은 쪽이 시스템입니다. 버튼 이름이 무엇을 뜻하는지가 안 적혀 있으면 쓰는 사람은 좁은 뜻으로 누르고 시스템은 넓은 뜻으로 실행합니다.