DB 스키마 정합성 불일치 원인을 데이터로 검증한 분석 보고서 작성
목차
DB 스키마와 실제 데이터 사이의 간극은 생각보다 조용하게 벌어진다. 스키마 정의 상으론 아무 문제 없는데 운영 중 특정 집계가 맞지 않거나, 어떤 조건에서 카운트가 예상보다 적게 나오거나. 에러 로그엔 아무것도 남지 않는 경우가 더 골치 아프다. 레드 얼럿이 없으니 심각성을 가늠하기 어렵고, 오차 범위 내려나 하고 그냥 넘기고 싶은 유혹도 생긴다.
이번 분석 보고서도 그런 상황에서 시작됐다. 운영 중 특정 수치 불일치가 감지됐고, 단순 로그 확인으로는 전체 그림이 잡히지 않았다. SQL로 직접 집계하고 결과를 HTML 보고서로 정리하는 방식을 택했다.
왜 코드 수정보다 보고서를 먼저 쓰는가
원인이 얼추 보이면 곧바로 픽스하고 싶어지는 충동은 자연스럽다. 근데 DB 정합성 문제에서 그 충동을 참는 게 낫다는 걸 여러 번 경험하고 나서 이 순서를 고정했다.
이유는 두 가지다. 첫째, 정합성 불일치의 원인은 보통 한 곳이 아니다. 집계 수치가 맞지 않는 것처럼 보이는 증상이 실은 스키마 제약 누락, 앱 로직의 엣지 케이스, 마이그레이션 적용 순서 오류 중 하나이거나 셋 전부일 수 있다. 첫 번째 원인만 잡고 닫으면 두 번째가 다음 스프린트에 조용히 다시 뜬다. 둘째, 수정 이후의 검증 기준이 없어진다. 보고서가 있으면 픽스 전 수치와 픽스 후 수치를 직접 비교할 수 있다. "줄었나?"가 아니라 "얼마나 줄었고 아직 남은 게 있나?"를 데이터로 답할 수 있게 된다.
그래서 이번에도 분석 먼저, 코드 수정은 그 다음 순서였다.
보고서 안에 들어가는 섹션은 이런 구조로 잡는다.
- 분석 대상 테이블과 조건 명시 - "어디를 봤는가"
- 기대값 vs 실제값 비교 - "얼마나 어긋나는가"
- 이상 항목 목록 + 원인 가설 - "왜 그런 것 같은가"
- 재현 케이스 - "어떤 조건에서 재현되는가"
- 후속 액션 - "다음에 무엇을 할 것인가"
가설은 어디까지나 가설로 명시해두는 게 중요하다. 수정 이후에 맞았는지 틀렸는지 기록해두면 다음번 유사 증상이 왔을 때 참고가 된다. 틀린 가설도 자산이다.
분석 쿼리 구조
핵심은 기대값과 실제값을 같은 쿼리 블록 안에서 계산해서 gap을 바로 확인하는 것이다. 분리해서 각각 실행하면 비교하는 데 컨텍스트 스위칭이 생기고, 숫자를 복붙하다 실수할 여지도 생긴다.
-- 기대값: 조건을 만족해야 하는 레코드 수
SELECT COUNT(*) AS expected_count
FROM target_table
WHERE status = 'active';
-- 실제값: 연관 테이블에서 실제로 처리된 레코드 수
SELECT COUNT(DISTINCT t.id) AS actual_count
FROM target_table t
JOIN processed_table p ON p.ref_id = t.id
WHERE t.status = 'active';
-- 한 쿼리에서 gap 직접 확인
SELECT
(SELECT COUNT(*) FROM target_table WHERE status = 'active') AS expected,
(SELECT COUNT(DISTINCT t.id)
FROM target_table t
JOIN processed_table p ON p.ref_id = t.id
WHERE t.status = 'active') AS actual,
(SELECT COUNT(*) FROM target_table WHERE status = 'active')
- (SELECT COUNT(DISTINCT t.id)
FROM target_table t
JOIN processed_table p ON p.ref_id = t.id
WHERE t.status = 'active') AS gap;
gap이 0인 항목은 넘어가고, 0이 아닌 항목이 집중 검토 대상이 된다. "어딘가 이상하다"는 감각에서 "이 테이블, 이 조건에서 N건 차이 난다"는 구체적 좌표로 내려오는 게 이 패턴의 역할이다. 좌표가 생기면 그때부터 코드를 보는 게 훨씬 빠르다.
보고서는 HTML로 생성해서 브라우저에서 바로 열어볼 수 있게 했다. 숫자가 많은 섹션은 표와 시각 요소를 함께 구성해서 이상 여부를 한눈에 파악할 수 있도록 함. 팀에 공유할 때도 파일 링크 하나면 되니까 커뮤니케이션 비용이 낮다.
이번 보고서에서 확인된 항목 요약은 아래와 같다.
| 항목 | 상태 |
|---|---|
| 데이터 정합성 | 일부 불일치 발견 |
| 처리 누락 건 | 확인 및 후속 조치 필요 |
| 쿼리 성능 | 허용 범위 내 |
| 엣지 케이스 | 추가 검토 필요 |
쿼리 성능은 허용 범위 내라 이번엔 손대지 않았다. 정합성 문제와 성능 최적화를 동시에 건드리면 어떤 수정이 어떤 영향을 줬는지 추적이 어려워진다. 변수를 하나씩 통제하면서 가는 게 맞다.
보고서 → 수정 → 재검증 사이클
이 사이클을 유지하는 이유는 피드백 루프를 짧게 가져가기 위해서다. 보고서에서 도출된 항목은 별도 커밋으로 픽스하고, 수정 이후 보고서를 다시 실행해서 gap이 줄었는지 확인한다. 줄지 않았으면 가설이 틀린 거고, 다른 원인을 찾아야 한다는 신호다.
DB 스키마 불일치는 코드베이스에서 가장 조용히 쌓이는 부채 중 하나다. 스키마 정의는 마이그레이션 파일에 있고, 실제 데이터는 운영 DB에 있고, 앱 로직은 또 다른 곳에 있다. 이 셋이 항상 동기화되어 있다는 보장은 없다. 특히 마이그레이션을 여러 사람이 순서 없이 작성할 때, 또는 핫픽스 중에 스키마 변경이 코드보다 먼저 나가거나 늦게 나갈 때 틈이 생긴다. 서비스를 분리하는 과도기엔 더 자주 발생하는 패턴이기도 하다.
그 틈을 인식하는 가장 직접적인 방법이 "내가 예상한 숫자"와 "실제 DB 숫자"를 나란히 놓는 것이다. 차이가 있는 곳이 버그나 로직 오류의 위치를 가리킨다. 감각에서 시작해서 좌표로 좁혀지면, 거기서부터 코드 리뷰가 훨씬 빠르게 진행된다.
엣지 케이스 항목 일부는 이번에도 가설만 기재하고 별도 티켓으로 분리했다. 재현 조건이 명확하지 않은 항목까지 한 번에 잡으려다 보고서가 늘어지면 오히려 액션이 늦어진다. 핵심 이슈를 픽스하고 그 결과를 빠르게 확인하는 게 우선이고, 엣지 케이스는 다음 사이클에서 검토하는 방식이 이 구조의 장점이다. 보고서가 쌓이면 엣지 케이스들끼리 패턴이 보이기 시작하는데, 그게 다음 리팩터링의 입력값이 된다.
댓글 0
첫 댓글 달아줘.