파트너 정산 서비스 계층을 정적 유틸로 전환해 구조 단순화
목차
파트너 정산 쪽 코드는 오래된 기능별 패키지 구조 그대로 Controller, Service, Mapper가 뒤섞여 있었다. 기능이 추가될 때마다 각 패키지 안에 파일이 하나씩 더 생겼고, 얼마 지나지 않아 어느 Service가 어느 Controller에 물려 있는지 파악하는 것 자체가 일이 됐다. 이번 리팩토링의 목적은 거기서 시작했다. 기능은 건드리지 않고, 읽기 어렵고 의존성이 꼬인 구조만 정리.
Service를 static Util로 바꾼 이유
Service 계층이 Spring Bean으로 관리되면 편한 점도 있지만, 파트너 정산 같은 도메인에서는 되레 복잡도를 올리는 경우가 많다. 특히 이번 코드는 상태를 갖지 않는 순수 로직이 대부분이었다. 외부 의존성 없이 입력만 받아서 계산하고 반환하는 메서드들이 Service 클래스 안에 @Autowired와 함께 묶여 있었다.
static Util로 전환하면 몇 가지가 바뀐다.
- 호출 경로가 명확해진다.
PartnerCalcUtil.calculate(param)처럼 클래스 명만 봐도 어디서 무슨 일을 하는지 보인다. - DI 컨테이너를 거치지 않으니 테스트할 때 Mock 설정을 최소화할 수 있다.
new도 필요 없고 그냥 메서드 호출하면 끝. - Bean 생명주기나 스코프 문제에서 자유롭다. 정산 로직처럼 호출 타이밍이 다양한 코드는 이게 특히 편하다.
물론 트레이드오프가 없진 않다. 인터페이스를 끼워서 구현체를 교체해야 하는 상황, 예를 들어 테스트 환경에서 전략 패턴으로 동작을 바꿔야 한다면 static 메서드는 제약이 된다. 이번 대상 코드는 그런 케이스가 없었고, 오히려 Bean 체계가 복잡도만 키우고 있었다. 구조 선택은 늘 그 코드가 실제로 어떻게 쓰이는지 기준으로 봐야 한다.
패키지 구조는 이렇게 바꿨다.
[before]
partner/
calc/
PartnerCalcController.java
PartnerCalcService.java
PartnerCalcMapper.java
balance/
PartnerBalanceController.java
PartnerBalanceService.java
...
[after]
partner/
web/
PartnerCalcController.java
PartnerBalanceController.java
utl/
PartnerCalcUtil.java
PartnerBalanceUtil.java
기능별로 묶었을 때는 파일 수가 늘수록 calc/ balance/ 디렉토리가 계속 생겼다. 역할별로 묶으니 web과 utl 두 개만 남고, 새 기능이 생겨도 두 곳 중 한 곳에 파일 하나 추가하면 끝이다.
단계별로 쪼개서 진행한 이유
리팩토링 중에 기능이 망가지면 그건 리팩토링이 아니라 버그다. 범위가 클수록 한 번에 바꾸고 싶은 충동이 생기지만, 그렇게 하면 어느 지점에서 문제가 생겼는지 추적이 안 된다. 이번 전환 순서는 의존성 방향을 기준으로 잡았다.
| Phase | 대상 | 내용 |
|---|---|---|
| 1-1 | 독립 Service 6개 | 다른 Service를 참조하지 않는 것부터 |
| 1-2 | 중간 의존성 4개 | 1-1 완료 후, 그것만 참조하는 것 |
| 1-3 | 핵심 금융 Service 3개 | 정산 계산 핵심 로직, 마지막에 신중하게 |
| 2 | XML 정리 | 이미 죽은 MyBatis XML 제거 |
1-3에 해당하는 금융 Service는 잔액 계산, 수수료 처리 같은 핵심 로직이 들어 있어서 가장 마지막에 뒀다. 앞 단계가 다 안정된 걸 확인하고 나서야 손댔다. 각 Phase 끝에 빌드를 돌려서 컴파일 에러 없는 것 확인하고 커밋 하나 끊었다. 롤백이 필요하면 그 커밋으로 돌아오면 된다.
XML 정리는 마지막으로 뺐다. Util 전환이 다 되고 나면 어느 XML이 실제로 살아 있는지 명확해지기 때문이다. 순서를 바꿔서 XML 먼저 지웠다가 나중에 Util에서 참조하는 게 남아 있으면 골치 아파진다.
리팩토링 커밋을 분리해야 하는 진짜 이유
코드 리뷰에서 리팩토링과 기능 변경이 섞인 PR을 받으면 리뷰어 입장에서 힘들다. 어느 부분이 동작을 바꾼 건지, 어느 부분이 구조만 바꾼 건지 계속 추적해야 한다. 집중력 분산이 생기고, 중요한 로직 변경을 놓칠 가능성도 높아진다.
| 잘 된 리팩토링 | 피해야 할 것 |
|---|---|
| 중복 코드 메서드 추출 | 리팩토링하면서 조용히 동작 변경 |
| 변수명·메서드명 의미 있게 수정 | 테스트 없이 넓은 범위 한 번에 변경 |
| 복잡한 조건식 분리 및 명확화 | 리팩토링 커밋에 기능 추가 혼재 |
| 단계별 커밋으로 롤백 지점 확보 | 거대한 단일 커밋 ("refactor everything") |
이번 작업도 기능 변경 없음을 기준으로 각 Phase를 독립 커밋으로 유지했다. 덕분에 1-2 단계에서 컴파일 에러가 났을 때 1-1 커밋 상태로 바로 되돌아가서 원인을 좁혔다. 커밋 단위가 작았기 때문에 가능한 일이었다.
파트너 잔액 체크 로직 제거도 이 흐름의 일부였다. 사용되지 않는 로직이 Service 안에 남아 있었고, 그걸 Util로 옮기는 김에 같이 정리했다. 죽은 코드는 미래의 누군가가 "이거 왜 있지?" 하고 분석하는 시간 낭비를 부른다. 정리 비용보다 방치 비용이 더 크다.
구조가 단순해지면 온보딩도 쉬워진다. 새로 합류한 팀원이 web과 utl 두 디렉토리만 보고 파트너 정산 흐름을 파악할 수 있다면, 기능별로 파편화된 패키지 구조보다 확실히 낫다. 리팩토링의 효과는 바로 드러나지 않지만, 다음 기능 추가할 때 체감이 온다.
댓글 0
첫 댓글 달아줘.