대시보드 PG 세부 항목 필터·집계 버그 수정
목차
대시보드 PG 세부 항목 필터·집계 버그 수정. 변경된 파일은 SQL 매퍼 하나, 커밋 diff는 단촐했지만 원인 파악까지 시간이 꽤 걸린 케이스였다.
결제(PG) 대시보드에서 세부 항목 컬럼을 조회할 때 두 가지 버그가 동시에 박혀 있었음. 하나는 GLOBAL 필터가 의도치 않은 스코프에 적용된 것, 다른 하나는 ROUND 함수의 위치. 각각 따로 보면 사소해 보이는데, 둘이 겹치니 숫자가 미묘하게 어긋나고 특정 조건에서는 아예 틀린 값이 나왔음.
결제 도메인 대시보드라는 특성상 숫자가 맞지 않으면 즉시 신뢰 문제로 번진다. "반올림 오차겠지"로 넘어가면 안 된다는 걸 이번에 다시 확인했다. 이쪽 도메인에서 "대충 맞는 것 같다"는 없어야 한다.
ROUND 위치가 집계 결과를 바꾼다
SQL에서 ROUND 함수를 어디에 두느냐는 생각보다 결과에 크게 영향을 미친다. 집계 전에 반올림하는지, 집계 후에 반올림하는지가 다르기 때문이다.
-- 잘못된 순서: 행마다 먼저 반올림 → 합산
SELECT SUM(ROUND(amount, 0)) FROM pg_transactions;
-- 올바른 순서: 합산 후 반올림
SELECT ROUND(SUM(amount), 0) FROM pg_transactions;
위쪽은 행이 수천 개일 때 개별 반올림 오차가 누적되어 합계가 실제 합산 값과 미묘하게 달라진다. 각 행을 반올림하면 +0.5 혹은 -0.5 범위에서 값이 조정되는데, 이게 수천 건 쌓이면 무시하기 어려운 차이로 벌어질 수 있음. 금액이 정수 단위로 맞아야 하는 결제 집계에서는 이 차이가 특정 조건에서 눈에 띄게 튀거나, 반대로 너무 미세해서 한동안 모르고 지나가는 두 경우 중 하나다. 이번엔 후자에 가까웠음. 오랫동안 화면에서 "그냥 반올림 오차겠지" 하고 넘어갔을 가능성이 있는 수치였다.
행 단위로 ROUND를 먼저 걸어야 하는 경우는 따로 있다. 단가처럼 행 자체를 특정 자릿수로 표현해야 하거나, 표시용 포맷팅이 집계 로직과 분리되어 있는 케이스. PG 세부 항목처럼 건별 금액을 합산해서 집계 지표로 올려야 하는 흐름이라면 순서는 항상 "집계 완료 → ROUND"여야 한다.
이 패턴이 유독 발견하기 어려운 이유는 잘못 짜인 쿼리도 읽으면 자연스럽게 보이기 때문이다. SUM(ROUND(amount, 0))이 얼핏 "금액을 반올림해서 더한다"로 읽히는데, 의미상 그렇게 표현하고 싶더라도 "집계 결과를 반올림한다"와는 엄연히 다름. SQL 리뷰할 때 집계 함수 안에 ROUND가 들어가 있으면 의식적으로 한 번 더 확인하는 게 좋다.
GLOBAL 필터 스코프 문제
이쪽이 좀 더 까다로웠다. 쿼리 구조상 WHERE 조건이 잘못된 위치에 있어서 전체 집계 범위에 영향을 주고 있었음. PG 세부 항목 컬럼은 전체 결제 데이터 중 특정 조건에 해당하는 건만 따로 집계해야 하는데, 해당 필터가 상위 레벨로 빠져나와 다른 컬럼 집계 범위까지 같이 좁히고 있었다.
MyBatis 동적 쿼리에서 이런 케이스가 생기기 쉽다. 조건 분기를 <if>나 <choose>로 조립하다 보면, 특정 집계 블록 안에 들어가야 하는 조건이 블록 바깥으로 나와 전체에 항상 적용되거나, 반대로 범위가 더 넓어야 할 조건이 서브쿼리 안에 갇히는 경우가 생긴다. 컬럼별로 집계 조건이 다른 대시보드 쿼리는 하나의 매퍼에 여러 집계 블록이 뭉쳐 있어서 스코프 경계를 놓치기 쉬움.
단순화하면 이런 상황이었다:
-- 문제: 세부 항목 전용 필터가 전체 SELECT 범위에 걸림
SELECT
SUM(total_amount) AS total, -- 이 집계도 같이 좁혀짐
SUM(pg_detail_amount) AS pg_detail -- 이 컬럼만 필터링되어야 하는데
FROM summary_view
WHERE pg_type = 'SPECIFIC_TYPE'; -- 글로벌하게 적용되어버림
-- 수정: 필터 스코프를 세부 항목 집계에만 한정
SELECT
SUM(total_amount) AS total,
SUM(CASE WHEN pg_type = 'SPECIFIC_TYPE'
THEN pg_detail_amount ELSE 0 END) AS pg_detail
FROM summary_view;
실제 쿼리는 이보다 복잡하지만 핵심은 같았음. 조건 스코프를 의도한 집계 범위 안에 가두는 것. 수정 방향은 해당 필터를 세부 항목 집계 블록 내부로 내려 스코프를 올바르게 좁히는 것이었음. 바깥에서 전체에 영향을 주던 조건을 세부 항목 부분에만 적용되도록 이동했고, 나머지 컬럼은 기존 집계 범위대로 유지했다.
버그 수정하면서 챙긴 것들
단순히 증상만 픽스하는 게 아니라 왜 발생했는지 원인을 파악하고 수정했다. 비슷한 패턴이 다른 경로에도 있는지 확인했고, 위험한 케이스는 함께 수정했음.
| 체크 항목 | 확인 내용 |
|---|---|
| 중복 코드 | 같은 집계 로직이 다른 쿼리 경로에도 있는지 |
| ROUND 패턴 | 매퍼 내 다른 집계 블록에서도 같은 실수가 있는지 |
| 필터 스코프 | 비슷한 구조의 다른 집계에서도 스코프가 올바르게 잡혀 있는지 |
| 회귀 방지 | 수정이 기존 정상 케이스를 망가뜨리지 않는지 |
| 실동작 확인 | 해당 화면에서 실제 수치 검증 |
| 숫자 cross-check | 관련 화면·집계 지표와 비교 |
ROUND 위치 실수는 특히 매퍼 파일 전체를 훑어봐야 한다. 한 군데서 잘못 잡힌 패턴이 다른 집계 블록에 그대로 복붙되어 있는 경우가 많기 때문. 이번엔 세부 항목 컬럼에만 해당했지만, 다른 집계 블록도 같이 확인하고 넘어갔음.
엣지 케이스를 꼼꼼히 따지는 게 귀찮아 보여도, 나중에 같은 버그로 다시 오는 시간 비용이 훨씬 크다. 특히 이번처럼 두 가지 실수가 겹친 케이스는 하나만 고쳐서는 완전히 수정된 게 아님. 증상이 나아 보여도 다른 조건에서 다시 터질 수 있어서, 원인을 끝까지 파고드는 게 결국 빠름. 수정 후에는 버그를 직접 재현해 정상 동작을 확인했고, 숫자 정합성도 관련 화면과 비교했다.
사내 서비스를 만들다 보면 기능 하나가 화면에 버튼 하나 추가하는 것으로 끝나지 않는다는 걸 계속 체감함. SQL 집계, 상태 처리, 예외 처리, 화면 렌더링, 권한 체크가 모두 엮여 있어서 어느 하나만 빠뜨려도 숫자가 맞지 않거나 특정 사용자에게 이상한 화면이 나타남. 특히 결제 대시보드처럼 여러 집계 지표를 한 화면에 올리는 구조는, 쿼리 하나가 다른 컬럼 결과에 영향을 미치는 케이스가 생길 수 있어서 변경할 때마다 조심하게 됨.
금융/결제 도메인은 숫자 하나가 틀리면 신뢰가 무너질 수 있어서 꼼꼼함이 기본값이어야 한다. 이번 케이스도 언뜻 보면 반올림 오차처럼 보이는데, 파고들면 필터 스코프까지 얽혀 있었음. 증상만 보고 원인을 단정하면 절반만 고치고 끝나는 상황이 생긴다.
개발 습관으로는 변경 전 현재 동작 수치를 메모해두고, 수정 후 같은 케이스로 비교 확인하는 루틴을 유지 중이다. 커밋 메시지는 "무엇을"보다 "왜"를 담으려고 노력하고, 논리적으로 독립된 단위로 커밋을 쪼개는 습관도 유지하고 있음. 작은 커밋을 자주 남기면 나중에 어느 변경에서 깨졌는지 추적하기 훨씬 쉬움. diff 하나가 ROUND 수정, 다른 diff 하나가 필터 스코프 수정이면 리뷰도, 롤백도 훨씬 깔끔하다.
댓글 0
첫 댓글 달아줘.