미사용 테이블 데이터 정합성 불일치 감사 보고서 작성
목차
운영 DB에 사용 흔적이 없는 테이블이 쌓이기 시작했다는 건 어떤 팀이든 한 번쯤 맞닥뜨리는 상황이다. 처음에는 그냥 두게 된다. "언젠가 쓸 수도 있으니까"라거나 "누가 만들었는지 모르니 일단 건드리지 말자"는 심리가 작동한다. 그러다가 "이 테이블 참조해도 됩니까?"라는 질문이 Slack에 뜨고, 아무도 자신 있게 답을 못 하는 상황이 온다. 그래서 이번에 unused tables 전체를 대상으로 데이터 정합성 감사를 돌렸음. 결과물은 20260427_1144_unused_tables_audit.html.
로그만 보는 건 한계가 있다. 특정 수치 불일치가 애플리케이션 레이어에서 뻥 뚫리고 있는 건지, 아니면 DB에서 이미 어긋난 건지를 로그로 구분하기 쉽지 않다. 사실 로그는 "무언가 잘못됐다"는 신호는 줄 수 있어도 "정확히 어디서 얼마나 잘못됐는지"를 확인하는 용도로는 부족하다. 그래서 처음부터 SQL로 직접 집계해서 "내가 예상한 숫자"와 "실제 DB 숫자"를 나란히 놓고 비교하는 방식을 택했음. 차이가 있는 곳이 버그나 로직 오류의 위치를 직접 가리켜준다.
감사 방법론과 쿼리 설계
쿼리를 짜기 전에 먼저 "정상 상태라면 이 칼럼 값이 어떻게 분포해야 하는가"를 정의했다. 기댓값이 명확하지 않으면 이상 항목을 뽑아도 "이게 실제 문제인가, 원래 이렇게 설계된 건가"를 판별하기 어렵다. 기댓값을 먼저 문서화하고 나서 실측치를 가져와 비교하는 구조를 잡았음.
감사 흐름은 다음과 같다.
- 대상 테이블 선별: 최근 N개월 간 INSERT/UPDATE가 없거나 애플리케이션 코드에서 참조되지 않는 테이블
- 칼럼별 기댓값 정의: NULL 허용 여부, 유니크 제약 준수 여부, FK 정합성 등
- 실측 집계 쿼리 실행
- 이상 항목 테이블로 정리 + 원인 가설 기재
- 재현 가능한 케이스 추출
- HTML 보고서 생성 후 팀 공유
FK 무결성이 깨진 레코드를 찾을 때 가장 자주 쓴 패턴이 LEFT JOIN + IS NULL 조합이다.
-- FK 참조 누락 케이스 탐지
SELECT
t.id,
t.ref_id,
t.created_at,
r.id AS resolved_ref
FROM unused_target_table t
LEFT JOIN reference_table r ON t.ref_id = r.id
WHERE r.id IS NULL
AND t.deleted_at IS NULL
ORDER BY t.created_at DESC;
이걸 기반으로 "논리적으로 존재해야 할 부모 레코드가 없는 행이 몇 개인가"를 카운트했음. 단순 COUNT만으로도 문제가 있는지 없는지는 바로 드러난다. 0이면 깨끗한 것이고, 0이 아니면 그 숫자 자체가 수정 범위가 된다.
NULL 분포를 확인할 때는 이런 방식을 썼다.
SELECT
COUNT(*) AS total,
COUNT(some_column) AS non_null_count,
COUNT(*) - COUNT(some_column) AS null_count,
ROUND(
(COUNT(*) - COUNT(some_column))::numeric / COUNT(*) * 100, 2
) AS null_ratio_pct
FROM unused_target_table
WHERE deleted_at IS NULL;
NULL 비율이 튀는 칼럼이 있으면 의도한 건지 아닌지 커밋 히스토리와 같이 확인했다. 쿼리 혼자서는 "이 NULL이 버그인가 아닌가"까지 판단해주지 않는다. 원인 가설은 사람이 달아야 한다.
HTML 보고서로 만든 이유는 팀원들이 별도 툴 없이 브라우저에서 바로 열어볼 수 있어서다. 숫자가 많아지면 표와 간단한 인라인 차트를 넣어서 한눈에 이상 여부를 확인할 수 있게 구성했음. Confluence나 노션보다 포매팅 자유도가 높고, 데이터 많은 표를 넣을 때 레이아웃이 무너지지 않는다는 점도 컸다.
발견 사항 요약
이번 감사에서 나온 주요 항목을 정리하면 이렇다.
| 항목 | 상태 | 비고 |
|---|---|---|
| 데이터 정합성 | 일부 불일치 발견 | FK 참조 누락 건 포함 |
| 처리 누락 건 | 후속 조치 필요 | 별도 커밋으로 픽스 예정 |
| 쿼리 성능 | 허용 범위 내 | 풀스캔 없음 확인 |
| 엣지 케이스 | 추가 검토 필요 | NULL 분기 로직 의심 |
정합성 불일치가 나온 영역은 주로 참조 무결성 제약이 느슨하게 걸려 있던 테이블들이었다. 테이블 자체가 unused로 분류된 이유가 "코드에서 안 쓰이기 때문"인데, 안 쓰이다 보니 정리 로직도 같이 멈춰 있었음. 살아 있는 테이블은 그래도 정기적으로 신경 쓰게 되는데, unused 테이블은 방치되면서 초기 마이그레이션 당시의 결함이 그대로 굳어버리는 경우가 많다. 테이블이 "죽어 있다"고 느슨하게 관리한 대가다.
엣지 케이스 쪽은 NULL을 허용하는 칼럼에서 의도치 않게 NULL이 들어간 건지, 로직이 NULL을 명시적으로 사용한 건지 구분이 안 됐음. 쿼리만으로는 판단이 안 되고 당시 커밋 히스토리와 같이 봐야 해서 후속 검토 항목으로 분류했다. 이런 케이스는 서두르면 오히려 "멀쩡한 걸 고쳤다"는 상황이 생긴다. 판단 보류가 옳은 선택일 때도 있다.
쿼리 성능은 인덱스 상황을 같이 확인했는데, unused 테이블 특성상 인덱스도 방치된 경우가 있었다. 집계 쿼리 실행 중에 풀스캔이 뜨는지 EXPLAIN으로 확인했고, 이번엔 허용 범위 안에서 끝났음. 다만 행 수가 더 늘어났다면 얘기가 달랐을 수 있다.
감사 사이클과 이 방식을 유지하는 이유
보고서 작성 자체가 목적이 아니다. 보고서는 이후 코드 수정의 근거 문서 역할을 한다. 발견된 항목들은 별도 커밋으로 픽스하고, 픽스 이후에 같은 쿼리를 다시 돌려보면서 수치가 0으로 떨어졌는지 확인했음. 보고서 → 수정 → 재검증 사이클을 한 번 돌고 나면 "이 테이블 지금 상태가 어떤지"에 대한 타임스탬프가 붙은 기록이 남는다는 게 가장 큰 이점이다.
불일치를 발견했을 때 바로 코드 수정으로 넘어가지 않는 것도 이 방식의 핵심이다. 원인 가설을 먼저 기록해두고, 재현 케이스를 확인한 뒤에 수정하는 흐름이 나중에 "왜 이걸 바꿨지?"라는 질문에 답하기 훨씬 쉽다. 팀 규모가 커질수록 이 근거 문서가 없으면 코드 리뷰에서 컨텍스트 전달이 제대로 안 된다. 리뷰어 입장에서 커밋 메시지만 보면 "이게 왜 필요한 변경인지" 판단하기 어렵고, 결국 리뷰 시간이 늘어나거나 리뷰 품질이 떨어지게 된다.
감사 주기를 짧게 가져가는 것도 고민 중이다. 문제가 터지고 나서 찾는 것보다 정기적으로 돌려보면서 숫자 변화를 추적하는 게 훨씬 낫다. unused 테이블은 "지금 당장 문제없어 보인다"는 착각을 심어주기 쉬운데, 데이터가 계속 쌓이거나 참조 구조가 바뀌면 갑자기 불일치가 폭발적으로 늘어나는 경우가 있다. 조기에 잡는 것과 나중에 한꺼번에 잡는 것의 비용 차이는 꽤 크다.
복잡한 분석 툴 없이도 SQL 집계 + 비교 + 표 정리만으로 충분한 경우가 대부분이었다. 과하게 자동화하거나 멋진 대시보드를 만드는 것보다, 일단 손으로 돌려보면서 구조를 파악하는 게 먼저라고 생각한다. 어차피 자동화는 "뭘 체크해야 하는지"를 알고 나서 해도 늦지 않는다. 이번 보고서는 그 "알고 나서"를 만들기 위한 작업이었음.
댓글 0
첫 댓글 달아줘.