개발 slecs

매출 총합 화면 결제 정산 구조 리팩토링으로 유지보수성 개선

목차

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

첫 댓글 달아줘.