같은 계좌 23,168건을 두 화면이 다르게 분류했습니다 — 통일하지 않기로 했습니다
운영 중인 시스템에서 같은 이름의 지표가 화면마다 다른 숫자로 나오는 일이 있습니다. 대시보드에서 본 건수와 상세 조회 화면에서 센 건수가 맞지 않는 상황입니다. 이런 보고를 받으면 대개 한쪽이 잘못됐다고 보고 그쪽을 고치러 갑니다. 어떤 B2B 채권 분석 시스템에서 비슷한 정리 작업을 하다가, 그 전제 자체가 근거 없는 추정이었다는 것을 데이터로 확인했습니다.
작업의 출발점은 리팩터링이었습니다. '현재 채권분류'를 만들어내는 파생 로직이 세 곳에 복붙돼 있었습니다. 조건분석 화면의 SQL CTE, 대시보드 매퍼의 분포 집계 쿼리, 그리고 공통 매퍼의 표현식입니다. 분류 기준이 한 번 바뀌면 세 곳을 다 고쳐야 하고, 그중 하나를 빠뜨리면 화면마다 다른 답이 나옵니다. 한 곳으로 모으기로 했습니다.
합치기 전에 베이스라인부터 떴습니다
복붙을 합칠 때 가장 먼저 하는 일은 코드를 읽는 것이 아니라 지금 결과를 뽑아 두는 것입니다. 합친 뒤에 분포가 달라지면 그게 이번 작업이 만든 회귀인지 원래부터 그랬는지 구분할 방법이 없기 때문입니다. 개발 DB의 매입 계좌 23,168건을 대상으로 세 곳의 분류 결과를 각각 집계해 나란히 놓았습니다.
그 표를 보고 리팩터링 계획을 접었습니다. 세 곳 중 두 곳은 같은 결과를 냈는데, 대시보드 쪽은 똑같은 23,168건을 전혀 다르게 분류하고 있었습니다.
구분 | 대시보드 계열 | 조건분석 계열 |
|---|---|---|
참조 범위 | 채권 평가 마스터의 계좌상태 컬럼 하나 | 계좌상태 + 회생 평가 테이블의 법원상태 |
세분 정도 | '개인회생(인가)' 같은 굵은 분류 | 인가·개시·금지를 나눈 분류 |
가장 큰 분류 | '개인회생(인가)' 9,864건 | '인가' 9,799건 |
다음 분류 | '연체' 1,671건 | '실효' 1,814건 |
이름이 겹친 분류 | 확정 8,563 · 미확정 343 · 선택안함 6 | 동일 |
두 방식이 공통으로 같은 이름을 붙인 분류는 세 개뿐이었습니다. 확정 8,563건, 미확정 343건, 선택안함 6건입니다. 나머지는 라벨 체계 자체가 달랐습니다. 대시보드 계열은 전체 채권의 평가 시점 마스터에 있는 계좌상태 컬럼 하나만 보고 굵은 분류를 만들었고, 조건분석 계열은 회생 평가 테이블의 법원상태까지 들어가 인가와 개시와 금지를 나눠 놓았습니다.

대시보드가 부정확하다는 추정을 폐기했습니다
작업을 시작할 때 저는 대시보드 쪽이 부정확하다고 적어 두었습니다. 참조하는 컬럼이 하나뿐이니 덜 정교하고, 덜 정교하니 틀렸을 것이라는 흐름이었습니다. 베이스라인을 놓고 보니 그 추정을 뒷받침하는 근거가 어디에도 없었습니다.
데이터가 보여준 것은 '다르다'였습니다. '한쪽이 틀렸다'가 아니었습니다. 둘은 같은 질문에 다르게 답한 것이 아니라, 애초에 다른 질문에 답하고 있었을 가능성이 있었습니다.
대시보드가 참조하는 테이블은 전체 채권의 평가 시점 마스터입니다. 포트폴리오 전체를 한 화면에 얹는 자리에서 굵은 분류를 쓰는 것은 그 자체로 말이 됩니다. 반대로 조건 검색으로 대상 계좌를 좁혀 들어가는 화면에서는 인가와 개시를 구분하는 것이 실무상 필요합니다. 두 화면은 목적이 달랐고, 분류도 그 목적을 따라간 것일 수 있었습니다.
어느 쪽이 정본인지는 코드만 봐서는 나오지 않았습니다. 개발자가 읽어서 판정할 문제가 아니라, 이 숫자로 판단하는 사람이 답해야 할 문제였습니다. 그래서 '대시보드가 부정확 — 근거 없는 추정이었음, 폐기'를 작업 노트에 그대로 남겼습니다. 지우지 않고 남긴 이유는, 같은 추측이 몇 달 뒤에 다시 올라올 것이기 때문입니다.
단일화와 통일은 다른 작업입니다
선택지는 셋이었습니다.
한쪽 로직으로 세 곳을 전부 통일한다. 코드는 가장 깔끔해지지만, 차이가 의도였을 경우를 통째로 덮어쓴다.
손대지 않고 둔다. 복붙 세 곳이 그대로 남고 다음 수정 때 같은 위험이 반복된다.
같은 의미를 쓰는 곳만 한 곳으로 모으고, 다른 의미를 쓰는 곳은 별도 결정으로 넘긴다.
3번을 택했습니다. 단일화라는 한 단어가 성격이 다른 두 작업을 덮고 있었기 때문입니다. 같은 의미의 코드를 한 곳에 모으는 것과, 다른 의미의 코드를 하나로 합치는 것입니다.
앞쪽은 리팩터링이고 뒤쪽은 정의 변경입니다. 리팩터링은 결과가 보존되므로 개발자가 판단해 진행해도 됩니다. 정의 변경은 화면에 찍히는 숫자가 바뀌므로 그 숫자를 쓰는 사람의 승인이 필요합니다. 둘을 한 커밋에 담으면 나중에 분포가 달라졌을 때 원인이 리팩터링인지 정의 변경인지 가려낼 수 없습니다.

같은 기준을 쓰는 두 곳만 DB 함수로 뽑았습니다
조건분석 SQL의 CTE와 공통 매퍼에 똑같이 복붙돼 있던 분류 CASE 식을 DB 함수 하나로 추출했습니다. 매퍼 XML 양쪽에는 함수 호출만 남고, 분류 본체는 한 곳에만 존재합니다. 다음에 분류 기준이 바뀌면 함수 하나만 고치면 모든 호출처가 같이 따라옵니다.
BEFORE
조건분석 SQL CTE → CASE ... END (분류 로직 본체)
공통 매퍼 표현식 → CASE ... END (같은 내용 복붙)
대시보드 분포 쿼리 → CASE ... END (다른 기준)
AFTER
조건분석 SQL CTE → 분류 함수 호출
공통 매퍼 표현식 → 분류 함수 호출
대시보드 분포 쿼리 → 별도 결정 대상으로 분리추출한 뒤 같은 베이스라인을 다시 떴습니다. 함수 추출 전후의 분류별 분포는 완전히 일치했습니다. 이 대조가 없었다면 '리팩터링만 했으니 결과는 같을 것'이라는 말로 넘어갔을 것이고, 실제로 어긋났더라도 몇 달 뒤 엉뚱한 문의를 통해 알게 됐을 것입니다.
매퍼 XML 여러 곳에 흩어진 CASE 식을 DB 함수로 모으는 방식은 판정이 SQL 안에서 완결된다는 점이 장점입니다. 조건절과 SELECT 목록 양쪽에서 같은 판정을 써야 할 때 특히 그렇습니다. 다만 이 선택이 항상 옳지는 않습니다. JPA나 QueryDSL로 쿼리를 조립하는 환경이면 DB 함수는 도구 체인과 마찰을 만들고, 애플리케이션 쪽으로 추출하는 편이 낫습니다.

대시보드는 통일하지 않기로 결정했습니다
세 번째 자리인 대시보드는 별도 작업으로 분리했습니다. 검토 끝에 대시보드 안쪽만 평가 마스터 단독 기준으로 정리하고, 분석 계산에 쓰이는 분류는 손대지 않기로 했습니다. 분석 쪽 기준을 건드리면 이미 그 숫자를 전제로 돌아가는 계산들이 함께 흔들리기 때문입니다.
결과적으로 '두 화면은 서로 다른 분류 기준을 쓴다'가 그대로 남았습니다. 이번에는 그것을 방치가 아니라 결정으로 적었습니다. 문서에 안 적힌 차이는 다음 사람이 버그로 읽고 조용히 통일해 버립니다. 그리고 그 통일은 리팩터링이라는 이름을 달고 들어오기 때문에 리뷰에서도 잘 안 걸립니다.
단일화는 같은 로직을 한 곳에 모으는 작업입니다. 다른 로직을 강제로 합치는 것은 단일화가 아니라 정의 변경입니다.
합치기 전에 확인할 것
같은 상황을 만났을 때 순서대로 짚어볼 항목입니다.
합치기 전에 지금 결과를 뽑아 두었는가 — 합친 뒤에 뽑으면 비교 대상 자체가 없습니다
복붙된 곳들의 결과가 실제로 같은가 — 코드가 비슷하다는 것과 결과가 같다는 것은 다릅니다
결과가 다르다면 그 차이가 버그인지 의도인지 누가 답할 수 있는가
통일이 필요하다면 그것이 리팩터링이 아니라 정의 변경이라고 알리고 승인받았는가
통일하지 않기로 했다면 그 결정이 코드 옆이나 문서에 남아 있는가
베이스라인을 매번 뜨는 것이 항상 남는 장사는 아닙니다. 화면 문구 한 줄이 복붙된 정도라면 과한 절차입니다. 다만 복붙된 값이 도메인 핵심 분류이고 여러 화면이 그 값으로 숫자를 세고 있다면, 집계 쿼리 몇 개 돌리는 비용이 훨씬 쌉니다.
'통일하지 않기로 했다'가 판단을 미루는 문장이 되지 않도록 조심할 필요는 있습니다. 실제로는 기준을 정해야 하는 상황인데 결정 주체를 찾기가 번거로워서 '의도된 차이'라고 이름을 붙이면, 몇 달 뒤 같은 자리에서 같은 질문이 다시 올라옵니다. 이번 건에서는 대시보드 쪽 기준을 하나로 정리하는 결정을 함께 내려서 그 자리를 비워두지 않았습니다.
복붙을 합치는 작업이 데이터 정의를 다시 묻는 작업으로 바뀌는 지점이 있습니다. 그 지점이 어디인지 알려주는 것이 베이스라인입니다. 코드를 열기 전에 집계부터 떠보는 데 드는 시간은 대체로 반나절을 넘지 않습니다.