매출 총합 화면 결제 정산 구조 리팩토링으로 유지보수성 개선
목차
admin/system-revenue/total-summary 매출 총합 화면. 시간축 4row 그리드로 기간별 집계를 보여주고, 파트너 모드에서는 PG사별·차등 수수료 구조를 별도로 정산해 표시하는 화면이다. 기능 자체는 멀쩡히 돌아가고 있었다. 근데 어느 순간부터 이 화면을 건드리는 게 살짝 무서워졌다. 이유 없이 조심스러워지는 코드는 대개 이유가 있다.
리팩토링 범위는 내부 클래스 1개, SQL 매퍼 1개, 뷰/스타일 파일 1개. 숫자만 보면 작아 보이지만 문제는 각 파일 안의 밀도였다.
뭐가 문제였나
한 파일에 책임이 너무 많이 몰려 있었다. 뷰 파일 안에 인라인 JS가 꽤 길게 들어가 있었고, 그 JS 안에서 집계 로직 일부를 클라이언트에서 재계산하고 있었다. 같은 계산 패턴이 SQL 매퍼 쪽에도 조금 다른 형태로 또 있었다. 어느 쪽이 진짜 수치 기준인지 한눈에 파악이 안 되는 구조.
파트너 모드 분기가 특히 문제였다. PG사별로 수수료율이 다르고, 차등 구간이 있는 파트너는 집계 방식도 달라진다. 그 분기 처리가 클래스 메서드 중간에 if (isPartnerMode) 블록으로 박혀 있었다. 일반 케이스를 수정하다가 그 if 블록 근처를 잘못 건드리면 파트너 정산 숫자가 틀어질 수 있는 구조.
// Before: 한 메서드 안에 일반/파트너 로직 혼재
public Map<String, Object> buildSummary(SummaryParams params) {
BigDecimal total = calcBase(params);
if (params.isPartnerMode()) {
// PG별 차등 수수료 재계산 - 이 블록만 70줄
total = recalcWithPgFee(total, params.getPgType(), params.getTierConfig());
}
// 후처리 - 파트너/일반 모두 공유
return buildResult(total, params);
}
이런 구조에서 버그 수정은 항상 두 번 고민하게 된다. "이 줄 바꾸면 파트너 케이스는 괜찮나?" 이게 작은 것 같아도 반복되면 개발 속도를 실질적으로 갉아먹는다. 코드 한 줄 고치는 게 아니라 관련 케이스 전부를 머릿속에 올려놓고 시뮬레이션해야 하는 구조가 되는 거라.
변경 내용과 의도
인라인 JS 분리
뷰 파일에 박혀 있던 JS를 별도 파일로 뺐다. 직접적인 효과가 두 가지다. 첫째는 브라우저 캐시. 인라인 JS는 매 요청마다 HTML과 함께 내려오는데, 외부 파일로 분리하면 한 번 받고 캐시에 올려두게 된다. 어드민이라 트래픽이 크진 않지만 반복적으로 쓰는 사람 입장에선 체감이 된다. 둘째는 개발 편의성. IDE 심볼 검색, 린트, 리뷰어가 diff 읽기 - 전부 외부 파일일 때 훨씬 낫다. 뷰 파일 안에 JS가 길게 들어가 있으면 뷰 로직과 스크립트 로직이 섞여서 파일 전체를 읽어야 전체 그림이 잡힌다.
SQL 매퍼 중복 제거
집계 쿼리에서 같은 서브쿼리 패턴이 중복으로 박혀 있던 걸 정리했다. SQL은 함수로 추상화하기 어려운 편이라 중복이 은근히 쌓이는데, 이번엔 명시적으로 정리했다. CTE나 뷰로 묶는 방법도 있었는데 이번 범위를 넓히지 않으려고 쿼리 내 중복 제거 수준에서 끊었다.
변수명·의존성 로딩 순서
d, tmp, res 같은 변수명을 맥락이 드러나는 이름으로 바꿨다. 의존성 로딩 순서는 실제 문제가 있던 건 아닌데, 스크립트 순서가 뒤섞여 있으면 나중에 다른 스크립트 추가할 때 타이밍 이슈가 생길 수 있어서 선제적으로 정리했다. 작동은 하지만 언제 터질지 모르는 시한폭탄 같은 상태였다.
이번 리팩토링에서 지킨 원칙을 정리하면 이렇다.
| 원칙 | 이번 적용 |
|---|---|
| 동작 불변 | 리팩토링 전후 집계 수치 케이스별 확인 |
| 단일 책임(SRP) | 파트너 모드 집계 로직 분리, 인라인 JS 외부화 |
| DRY | 중복 함수·서브쿼리 통합 |
| 최소 변경 | 연관 화면은 건드리지 않음 |
"동작 불변"이 가장 중요하다. 리팩토링이라는 이름 아래 슬쩍 동작이 바뀌는 경우가 있는데, 그건 리팩토링이 아니라 기능 변경이다. 특히 결제 정산 화면에서는 이 구분이 더 엄격해야 한다.
금융 도메인 리팩토링에서 특히 챙기는 것
기능 리팩토링과 금융 도메인 리팩토링은 체감 긴장감이 다르다. 숫자 하나가 틀리면 신뢰가 흔들리는 영역이라서 "대충 같아 보이면 OK"가 안 된다. "대충 맞는 것 같다"로 넘어가면 나중에 반드시 돌아온다. 이번에도 검증을 나름 체계적으로 밟았다.
- 리팩토링 전 주요 케이스 수치 메모 - 일반, 차등 수수료 파트너, 데이터 없는 케이스
- 변경 후 동일 케이스 화면 재확인
- 연관 화면 cross-check - 다른 집계 화면과 총합이 일치하는지
- 커밋은 논리 단위로 쪼개기 - JS 분리, 클래스 정리, SQL 정리를 각각 따로
커밋을 작게 쌓아두면 나중에 "언제부터 숫자가 틀렸지?" 같은 상황에서 원인을 좁히는 시간이 확 줄어든다. 단위가 크면 그 커밋 전체를 뒤져야 하고, 단위가 작으면 바이너리 서치가 된다. 정산 관련 버그는 발견이 늦어지는 경우가 있어서, 그때 git bisect 같은 게 실질적으로 도움이 된다.
커밋 메시지는 "무엇을 바꿨는지"보다 "왜 바꿨는지"를 담으려고 한다. 중복 함수 제거보다는 파트너 모드 분기 수정 시 일반 집계 로직이 같이 깨지는 구조 개선 쪽이 나중에 히스토리를 볼 때 훨씬 유용하다. 커밋 메시지에 왜를 담는 습관은 코드 리뷰 때도, 몇 달 뒤 내가 내 코드를 다시 볼 때도 빛을 발한다.
사내 어드민 화면은 SQL 집계, 상태 머신, 예외 처리, 렌더링, 권한 체크가 전부 엮여 있어서, 구조가 정리돼 있지 않으면 새 기능 하나 추가할 때마다 어디서 어디까지 영향을 줄지 가늠하기 어렵다. 기능은 잘 돌아가고 있었어도 이번에 손댄 이유는 그거다. 이대로 두면 다음 기능 추가 때 반드시 이 구조 때문에 막히게 되어 있었다. 기술 부채는 이자가 붙는다.
댓글 0
첫 댓글 달아줘.