대시보드 이용중지 집계 기준 불일치 버그 수정
목차
대시보드에서 이용중지 건수 카드를 보면 50건인데, 그 카드를 눌러 들어간 목록 화면 기본 필터 결과는 47건. 이런 숫자 불일치를 "아마 타이밍 차이겠지"로 넘길 수 없는 게 이용중지·정지 같은 상태 관리 도메인이다. 운영팀이 매일 직접 쓰는 화면이고, 숫자가 다르면 "3건이 어디 있지? 내가 놓친 건가?"부터 시작해 불필요한 재확인 작업이 생긴다.
원인은 단순했음. 카드(집계)는 tb_suspend_master를 기준으로 count를 뽑는데, 목록의 기본 필터는 다른 테이블 혹은 다른 조건으로 데이터를 조회하고 있었다. 두 쿼리가 애초에 다른 소스를 보고 있었으니 숫자가 맞을 리가 없었다. 변경 파일은 내부 클래스 1개, SQL 매퍼 1개. 변경 자체는 크지 않았지만 원인 파악이 먼저였다.
집계 기준이 어긋나는 구조적 이유
대시보드 카드는 대부분 별도 API나 집계 쿼리로 빠르게 count만 뽑고, 목록은 페이징·필터 조건이 붙은 별개 쿼리로 움직인다. 처음 기능을 만들 때는 둘이 같은 로직을 공유하거나 적어도 같은 테이블을 보도록 설계했더라도, 시간이 지나면서 한쪽만 요구사항이 바뀌거나 다른 개발자가 한쪽 쿼리만 수정하는 상황이 생긴다. 이번 케이스도 그랬다.
tb_suspend_master가 이용중지의 단일 진실 소스(single source of truth)여야 하는데, 집계 쿼리와 목록 쿼리가 각자 다른 방식으로 "이용중지 상태"를 판단하고 있었다. 집계는 마스터 테이블 직접 count, 목록은 조인 결과의 특정 컬럼 필터 - 이 구조에서 엣지 케이스가 생기면 숫자가 벌어진다.
수정은 두 쿼리가 같은 기준을 보도록 맞추는 것. 단순화하면 아래와 같은 방향이었다.
-- 수정 전: 목록 기본 필터 (집계와 기준이 달랐던 쪽)
SELECT *
FROM tb_user u
JOIN tb_suspend_detail sd ON u.user_id = sd.user_id
WHERE sd.status = 'ACTIVE'
-- 수정 후: tb_suspend_master 기준으로 통일
SELECT *
FROM tb_user u
JOIN tb_suspend_master sm ON u.user_id = sm.user_id
WHERE sm.is_suspended = 'Y'
AND sm.expire_dt > NOW()
카드의 count 쿼리도 동일 조건(expire_dt > NOW() 포함 여부 등)으로 맞췄다. 이렇게 하면 카드와 목록이 항상 같은 기준을 보고, 조건이 하나 추가될 때도 한 곳만 바꾸면 된다.
증상 패치와 원인 수정의 차이
증상만 고치는 건 쉽다. 숫자가 다르면 어느 쪽에 보정값을 넣거나, "어차피 비슷하니까" 하고 덮는 식. 당장은 눈에 안 보이지만 조건이 하나 더 추가되거나 테이블 구조가 바뀌면 또 어긋난다. 이번에는 집계와 목록 양쪽이 tb_suspend_master를 단일 기준으로 보도록 구조를 맞췄고, 수정하면서 같은 패턴으로 짜인 유사 집계가 더 있는지도 확인했다. 위험해 보이는 케이스는 함께 정리했음.
버그를 수정할 때 내가 챙기는 체크리스트:
- 같은 로직이 다른 경로에도 있는지 (중복 코드 탐색)
- 수정이 기존 정상 케이스를 망가뜨리지 않는지 (회귀 확인)
- 해당 화면/API에서 직접 동작 확인
- 관련 숫자를 다른 화면과 cross-check
마지막 항목이 특히 중요한데, 집계 카드·목록·상세가 각각 있는 화면은 세 군데 숫자가 모두 같아야 한다. 하나만 확인하고 "됐다"고 넘어가면 다른 곳에서 빠진 게 나온다. 수정 후에는 버그를 직접 재현하는 케이스를 만들어 이전/이후 숫자를 비교했고, 카드 count와 목록 총건수가 동일한지 cross-check도 통과했음.
아래는 이 도메인처럼 집계-목록 이중 조회 구조를 가진 화면에서 기준이 어긋날 때 흔히 나타나는 패턴과 대응이다.
| 상황 | 증상 | 근본 원인 |
|---|---|---|
| 카드 ≠ 목록 건수 | 집계와 필터 기준 테이블 상이 | single source 미통일 |
| 날짜 경계 케이스 | 만료 당일 건수 불일치 | >= vs > 조건 차이 |
| 상태 전이 직후 불일치 | 카드는 갱신, 목록은 캐시 | 집계 캐싱 정책 불일치 |
SQL 집계와 상태 관리, 왜 같이 봐야 하나
사내 서비스를 만들다 보면 기능 하나가 단순히 화면에 버튼 하나 추가하는 것으로 끝나지 않는다는 걸 계속 체감한다. SQL 집계, 상태 머신, 예외 처리, 화면 렌더링이 모두 엮여 있어서 어느 하나만 빠뜨려도 숫자가 맞지 않거나 특정 사용자에게 이상한 화면이 나온다.
사내 서비스라고 해서 품질 기준을 낮추면 안 된다. 오히려 운영 담당자들이 매일 쓰는 도구는 외부 서비스보다 실수에 둔감해지기 쉽고, 그게 더 위험하다. 숫자가 이상한 게 익숙해지면 진짜 이상한 숫자가 나와도 그냥 넘어가게 된다. 서비스 신뢰는 거창한 곳에서 한 번에 깨지는 게 아니라, 이런 작은 불일치가 쌓이면서 조금씩 깎인다.
변경 전에 현재 동작을 수치로 메모해두고, 수정 후에 같은 케이스로 확인하는 흐름을 유지하고 있다. 커밋 메시지도 "무엇을 바꿨다"보다 "왜 tb_suspend_master 기준으로 일치시켰는지"를 담으려 한다. 3개월 뒤에 이 커밋을 열었을 때, context 없이도 의도가 읽혀야 하니까.
작은 커밋을 자주 하면 문제가 생겼을 때 어느 변경에서 깨졌는지 찾기 훨씬 쉽다. 그래서 논리적으로 독립된 단위로 커밋을 쪼개는 습관을 유지 중인데, 이번처럼 SQL 매퍼와 내부 클래스가 같은 문제의 두 축이라면 같은 커밋에 묶는 게 맞다. 흩어진 커밋은 나중에 bisect하거나 revert할 때 오히려 방해가 된다. 엣지 케이스를 꼼꼼히 따지는 게 귀찮아 보여도, 같은 버그로 다시 오는 시간 비용이 훨씬 크다.
댓글 0
첫 댓글 달아줘.