개발 slecs

플랫폼 수익 누적 산식 정정

목차

platform-revenue 모듈 리팩토링을 진행했다. 핵심은 누적수익 산식 정정이고, 거기에 KPI 라벨 통일을 묶었다. 변경 파일은 뷰/스타일 4개, 내부 클래스 1개, SQL 매퍼 1개, 총 6개 파일.

숫자만 보면 작은 범위 같지만 계층을 가로질렀다. SQL 매퍼의 집계 쿼리를 손대면 내부 클래스의 모델 필드가 따라와야 하고, 모델이 바뀌면 뷰에서 바인딩하는 방식도 달라진다. 파일 수가 6개여도 실제 변경의 무게는 가볍지 않았다.

산식이 왜 틀리는가

집계 쿼리에서 누적 수치가 어긋나는 패턴은 생각보다 정해져 있다.

가장 흔한 건 기준 기간 불일치다. 화면 필터 기간과 SQL의 WHERE 조건이 미묘하게 다르거나, 타임존이 섞이거나, 집계 함수의 적용 시점이 GROUP BY 기준으로 뒤집혀 있는 경우. 그다음은 조인 복제 문제다. 1:N 관계 테이블을 잘못 조인하면 행이 복제되고, 거기에 SUM을 씌우면 실제보다 부풀린 값이 나온다. 이건 개발 환경에서 데이터가 적을 때 잘 안 보이다가 운영 데이터에서 터진다.

KPI 라벨 통일은 별개처럼 보이지만 산식 오류와 얽혀 있다. 화면마다 "누적수익", "총수익", "합계"를 혼용하면 같은 숫자를 표현하면서도 기준이 다르게 읽힌다. 실제로 기준이 같아도 라벨이 다르면 "화면마다 다른 로직이 있는 것 아닌가" 하는 의심이 생긴다. 운영 중인 시스템에서 이 의심이 생기기 시작하면 해소하는 비용이 라벨 수정 몇 줄보다 훨씬 커진다.

금융/결제 도메인에서 숫자 오류의 체감 온도는 기능 오류와 다르다. 기능이 안 되면 명확하게 신고가 들어오고 바로 인지한다. 반면 산식 오류는 "일부 케이스에서 조금 다른 것 같다" 수준으로 시작해서 오래 방치되고, 발견됐을 때 쌓인 데이터에 얼마나 영향을 줬는지 역추적이 필요해진다. "대충 맞는 것 같다"로 넘어가면 반드시 다시 돌아온다는 건 이 도메인에서 반복해서 체감하는 현실이다.

변경 범위와 접근 방식

이번에 손댄 계층을 정리하면 이렇다.

계층 파일 수 주요 변경 내용
SQL 매퍼 1 누적수익 집계 쿼리 산식 정정
내부 클래스 1 모델 필드 정리, KPI 라벨 상수화
뷰/스타일 4 인라인 JS 외부 분리, 중복 함수 통합, 변수명 명확화, 의존성 로딩 순서 정리

인라인 JS를 외부 파일로 분리하면 브라우저 캐시 효과가 생긴다. 처음 방문 때 파일을 받으면 다음 방문에서는 캐시에서 바로 올린다. 그리고 인라인 스크립트는 뷰 파일 전체를 훑어야 찾을 수 있는데, 분리된 파일은 바로 열거나 검색하면 된다. 코드 리뷰할 때 "이 파일은 뭐 하는 파일인가"가 명확해진다는 것도 부수 효과다.

중복 함수는 원래 산식 정정 범위만 잡으려 했는데 함께 정리하지 않으면 고쳐도 일부 화면에서 다시 어긋날 수 있었다. 거의 동일한 로직이 뷰 파일 두 곳에 나뉘어 있었고, 한쪽이 수정되면 다른 쪽이 낡은 채로 남는 전형적인 drift 패턴이었다. 의존성 로딩 순서도 비슷하다. 외부 라이브러리가 로드되기 전에 그걸 참조하는 코드가 실행되면 타이밍 오류가 생기는데, 네트워크가 느리거나 특정 브라우저에서만 재현되기 때문에 원인을 모르면 엉뚱한 데서 한참 헤맨다.

KPI 라벨 상수화는 구조적으로 간단한 변경이지만 효과는 뚜렷하다.

// 변경 전: 뷰/클래스마다 문자열 직접 사용
model.addAttribute("kpiLabel", "누적수익");  // 어떤 뷰
model.addAttribute("kpiLabel", "총수익");    // 다른 뷰

// 변경 후: 클래스에서 상수 정의, 모든 화면이 단일 출처 참조
public class RevenueKpi {
    public static final String LABEL_CUMULATIVE = "누적수익";
}
// 각 뷰에서
model.addAttribute("kpiLabel", RevenueKpi.LABEL_CUMULATIVE);

라벨을 바꿔야 할 때 한 곳만 고치면 되고, "이 라벨이 어디서 오는가"를 추적하기 쉬워진다. 하드코딩 문자열이 여러 파일에 흩어져 있으면 하나를 바꿨을 때 다른 걸 놓치기 쉽고, 놓쳤다는 사실을 바로 알기도 어렵다.

리팩토링 원칙은 동작을 바꾸지 않는 것, 필요한 곳만 건드리는 것, 두 가지를 기준으로 잡았다. 리팩토링하다 보면 "이것도 좀 이상한데"가 계속 보이기 시작한다. 거기까지 손대다 보면 어느 순간 범위가 원래 의도와 멀어지고, 테스트해야 할 케이스도 덩달아 늘어난다. 이번엔 직접 연결되지 않은 부분은 눈에 보여도 건드리지 않고 메모만 해뒀다. 다음 커밋에서 따로 처리하는 게 맞다.

검증과 커밋 습관

리팩토링 이후 화면 동작이 기존과 동일한지 주요 케이스를 확인하고 커밋했다. 방법은 단순하다. 수정 전 화면의 수치를 메모하거나 스크린샷으로 찍어 두고, 수정 후 같은 케이스로 확인한다. 관련 화면이 여러 개면 숫자를 cross-check한다. 번거로워 보여도 산식이 얽힌 화면에서는 이게 가장 확실한 방법이다.

커밋은 논리적으로 독립된 단위로 쪼갰다. SQL 산식 정정, JS 분리, 중복 통합을 한 커밋에 몰아 넣으면 나중에 어느 변경에서 문제가 생겼는지 추적하기 어렵다. 작은 커밋이 귀찮게 느껴질 때도 있는데, 문제가 생겼을 때 커밋 로그를 훑는 속도를 생각하면 할수록 이득이다. git bisect를 써본 사람이라면 커밋 단위가 얼마나 중요한지 한 번 경험하고 나면 습관이 바뀐다.

커밋 메시지는 "무엇을"보다 "왜"를 담으려 했다. "인라인 JS 분리"보다 "반복 방문 캐시 및 코드 탐색 개선을 위해 인라인 JS 외부 파일 이동"이 나중에 맥락이 살아 있다. 결국 커밋 로그는 그 시점의 판단을 기록하는 것이고, 판단 없이 행동만 적으면 나중에 "왜 이렇게 했지"를 다시 역추적해야 한다.

사내 서비스를 만들다 보면 기능 하나가 화면에 버튼 추가 하나로 끝나지 않는다는 걸 계속 체감한다. SQL 집계, 상태 처리, 예외 처리, 화면 렌더링, 권한 체크가 다 엮여 있어서 어느 하나만 빠뜨려도 숫자가 안 맞거나 특정 케이스에서 이상한 화면이 나온다. 이번처럼 산식 정정 하나가 뷰/클래스/매퍼를 같이 건드려야 했던 게 그 단적인 예다. 각 계층이 어떻게 엮여 있는지 파악하는 데 시간을 쓰지 않으면 수정 범위를 잘못 잡고 반쪽짜리 픽스가 나온다. 꼼꼼함이 기본값이어야 한다는 걸, 이 도메인 작업할 때마다 다시 새긴다.

댓글 0

첫 댓글 달아줘.