개발 slecs

공급사 포털 데이터 정합성 불일치 원인 분석 보고서 작성

목차

공급사 포털 ralplan v3 관련 데이터 정합성 분석 보고서(20260426 0036 supplier portal ralplan v3)를 작성했음. 파일명에 타임스탬프를 박는 건 나름의 규칙인데, 보고서가 쌓이면 시간순 추적이 돼서 "그때 왜 이 커밋을 넣었지?" 같은 질문에 곧장 답할 수 있다.

왜 코드 먼저 안 열고 보고서부터 쓰는가

운영 중 수치 불일치를 감지했을 때 바로 코드를 열고 싶은 충동이 생기는데, 그걸 참는 게 맞다. 공급사 포털처럼 외부 데이터 소스와 내부 테이블이 맞물리는 구조에서는 원인 위치조차 모르는 상태에서 수정을 먼저 넣으면 증상만 숨기는 픽스가 나온다. 어느 레이어에서 어긋났는지 모르면 고친 것처럼 보이지만 다른 불일치를 만들어내는 경우가 실제로 있다.

로그가 있어도 마찬가지다. 로그는 이벤트 단위라 "어떤 시점에 어떤 레코드가 기대값과 달랐는가"를 집계 레벨로 보려면 SQL을 직접 써야 한다. Kibana나 CloudWatch로 로그 훑는 것과 GROUP BY로 집계해서 비교하는 것은 전혀 다른 정보를 준다. 전자는 타임라인, 후자는 수량 기준 위치.

이번에도 단순 로그 확인으로는 전체 그림이 잡히지 않았다. 그래서 관련 내부 테이블에서 집계 쿼리를 짜고 결과를 HTML 보고서로 정리했음.

분석 쿼리 구조와 보고서 구성

내가 쓰는 패턴은 "기대값"과 "실제값"을 CTE로 각각 분리하고, 마지막에 JOIN으로 차이를 계산하는 방식이다.

WITH expected AS (
    SELECT supplier_id, plan_version, expected_qty
    FROM supplier_plans
    WHERE plan_date = '2026-04-26'
      AND version = 'v3'
),
actual AS (
    SELECT supplier_id, SUM(received_qty) AS actual_qty
    FROM receipts
    WHERE receipt_date = '2026-04-26'
    GROUP BY supplier_id
)
SELECT
    e.supplier_id,
    e.expected_qty,
    COALESCE(a.actual_qty, 0) AS actual_qty,
    e.expected_qty - COALESCE(a.actual_qty, 0) AS diff
FROM expected e
LEFT JOIN actual a ON e.supplier_id = a.supplier_id
WHERE e.expected_qty != COALESCE(a.actual_qty, 0)
ORDER BY ABS(e.expected_qty - COALESCE(a.actual_qty, 0)) DESC;

여기서 LEFT JOIN + COALESCE 조합이 핵심이다. INNER JOIN으로 짜면 기대값은 있는데 실제값이 아예 없는 누락 건이 결과에서 사라진다. 가장 심각한 케이스인데도 조용히 빠져나가는 것. LEFT JOIN을 써야 NULL로라도 잡힌다. ORDER BY ABS(diff) DESC로 정렬하면 차이가 큰 항목부터 보이니 우선순위를 잡기 편하다.

보고서 HTML은 쿼리 결과를 <table>로 옮기되, diff 절댓값 크기에 따라 배경색 클래스를 붙이는 방식으로 구성했다. 팀 내부용이라 Chart.js 같은 라이브러리 없이 순수 HTML/CSS로 충분했음. 숫자가 많을 때는 인라인 막대 형태의 시각화를 붙이면 이상 항목이 한눈에 들어온다.

이번 분석 결과 요약:

항목 상태 비고
데이터 정합성 일부 불일치 발견 supplier_id 기준 특정 건 누락
처리 누락 건 후속 조치 필요 재처리 스크립트 별도 커밋 예정
쿼리 성능 허용 범위 내 집계 대상 row 수 문제없음
엣지 케이스 추가 검토 필요 plan_version 전환 시점 경계값

엣지 케이스 항목이 흥미로웠는데, v3로 버전 전환이 이뤄지는 시점에 in-flight 상태인 레코드가 v2 기준으로 처리되다가 v3 집계에서 누락되는 패턴이 있었다. 버전 전환 경계값은 단순 diff 쿼리로는 안 잡히고 타임스탬프 기준으로 별도로 뜯어봐야 했음.

보고서 → 수정 → 재검증 사이클

보고서가 완성되면 그때부터 코드를 열었다. 도출된 항목별로 별도 커밋을 만들어서 어떤 근거로 수정이 들어갔는지 추적 가능하게 했다. 커밋 메시지에 보고서 타임스탬프나 항목 ID를 달면 나중에 git log로 볼 때 맥락이 연결된다. "왜 이 줄을 고쳤지?"라는 질문에 커밋 메시지에서 보고서로, 보고서에서 쿼리로 역추적이 된다.

수정 후에는 같은 쿼리를 다시 돌린다. 재검증을 생략하면 픽스가 실제로 먹혔는지 확인이 안 된다. 유닛 테스트가 있어도 운영 데이터에만 있는 패턴은 테스트에서 재현이 안 되는 경우가 많다. ralplan처럼 외부 공급사 데이터가 들어오는 도메인은 특히 그렇다. 실제 DB 기준으로 한 번 더 보는 단계가 의미가 있다.

트레이드오프를 말하자면, 이 방식은 초반에 보고서 작성 시간이 들어간다. 급한 상황에서는 바로 코드부터 고치고 싶은 압박이 있는 게 사실이다. 그런데 원인 모르고 수정하다가 다른 곳이 깨지면 그게 더 오래 걸린다. 보고서 한 장이 그 리스크를 줄여주는 보험이라고 생각하고 있음.

가장 효과적인 비교 방식은 역시 "내가 예상한 숫자"와 "실제 DB 숫자"를 같은 화면에 나란히 놓는 것이다. diff가 0이 아닌 행이 버그나 로직 오류의 좌표다. 막연하게 "어딘가 이상하다"는 느낌을 "supplier_id X에서 Y만큼 차이난다"로 좁혀주는 것만으로도 수정 시간이 절반 이하로 줄어든다.

이 흐름을 팀 전체 습관으로 가져가는 게 목표라서, 보고서 포맷 자체를 재사용 가능한 형태로 정형화해두고 있다. 다음 불일치가 터졌을 때 처음부터 구조를 고민할 필요 없이 쿼리만 바꿔서 돌릴 수 있도록.

댓글 0

첫 댓글 달아줘.