들어가며
클린 코드 14장에서는 명령행 인수 처리 프로그램(Args.java)을 점진적으로 리팩터링해가는 과정을 소개합니다. 단순한 if-else 분기에서 출발해, 점점 더 유연하고 확장 가능한 구조로 나아가는 모습을 보여주죠. 이번 글에서는 제가 실제로 진행 중인 타임딜 할인 시스템에서 중복된 조건문 제거를 목표로, 클린 코드에서 소개한 점진적 개선 원칙을 어떻게 적용했는지 공유해 보겠습니다.
문제 상황: 할인 정책 판단 로직에 if가 너무 많았다
먼저 제코드입니다. 처음에는 이렇게 구현했습니다:
@Component
@RequiredArgsConstructor
public class CouponDiscountStrategy implements DiscountStrategy {
@Override
public int applyDiscount(int originalPrice, DiscountPolicy policy, User user) {
...
// 3. 할인 금액 계산
int discountAmount = calculateDiscountAmount(originalPrice, policy);
...
}
private int calculateDiscountAmount(int originalPrice, DiscountPolicy policy) {
// 정액 할인
if (policy.getAmount() != null) {
return policy.getAmount();
}
// 정률 할인
if (policy.getPercentage() != null) {
double discountRate = policy.getPercentage() / 100.0;
return (int) Math.round(originalPrice * discountRate);
}
return 0;
}
}
정책에 따라 어떤 할인 계산기를 쓸지 판단하는 구조였는데, 할인 방식이 늘어날수록 if-else 조건이 계속해서 증가할 수밖에 없는 구조였습니다. 새로운 정책이 추가되면 항상 이 코드를 수정해야 하니, 확장에 닫혀 있는 구조였죠.
점진적 개선 1단계: 전략 객체 분리
우선 할인 계산 책임을 전략 객체로 분리했습니다:
public abstract class DiscountCalculator {
public abstract int calculateDiscount(int price, DiscountPolicy policy);
}
public class AmountDiscountCalculator extends DiscountCalculator { ... }
public class PercentageDiscountCalculator extends DiscountCalculator { ... }
이렇게 하면 calculateDiscountAmount() 내부의 복잡한 로직이 각 클래스 안으로 깔끔하게 분리되고, 전략 객체에 책임을 위임할 수 있게 됩니다.
점진적 개선 2단계: if 제거를 위한 supports() 도입
그런 다음, 각 전략 클래스가 자신이 처리할 수 있는 정책인지 스스로 판단하게 만들었습니다:
public interface DiscountCalculator {
boolean supports(DiscountPolicy policy);
int calculateDiscount(int price, DiscountPolicy policy);
}
이제 팩토리는 다음처럼 단순한 스트림 한 줄로 전략을 선택할 수 있습니다:
public class DiscountCalculatorFactory {
private final List<DiscountCalculator> calculators;
public DiscountCalculator resolve(DiscountPolicy policy) {
return calculators.stream()
.filter(calculator -> calculator.supports(policy))
.findFirst()
.orElse(new NoOpDiscountCalculator());
}
}
이제 할인 정책이 추가되어도 기존 코드는 그대로 두고, 새로운 Calculator만 만들면 됩니다.
창발적인 구조가 생겨났다
이제 CouponDiscountStrategy에서는 더 이상 조건문을 직접 다루지 않고, 아래처럼 단순하고 읽기 쉬운 형태로 리팩터링되었습니다:
// 이전코드
//int discountAmount = calculateDiscountAmount(originalPrice, policy);
int discountAmount = calculatorFactory.getCalculator(policy) // 리팩토링 후 코드
.calculate(originalPrice, policy);
무수히 많은 if문이 사라지고, 구조는 더 유연해졌습니다. 조건이 사라지자 확장과 테스트도 쉬워졌고, 각 할인 정책마다 독립적인 로직이 응집되면서 SRP(단일 책임 원칙)도 만족하게 되었습니다.
그런데 이구조! 어디서 본듯한 익숙함이 느껴진다면...ㅎㅎ정답입니다!
무엇과 닮았냐면?
1. HandlerMapping + HandlerAdapter 구조 (Spring MVC)
요청 → DispatcherServlet
→ HandlerMapping (어떤 핸들러?)
→ HandlerAdapter (어떻게 실행?)
→ 제가 만든 구조는 이렇게 대응된다고 볼 수 있습니다! :
HandlerMapping | DiscountCalculatorFactory.resolve() |
HandlerAdapter | DiscountCalculator.calculateDiscount() |
다양한 Controller/Service | 다양한 할인 정책 Calculator들 |
할인 정책 리팩터링에서 사용한 전략 패턴 구조는 단순히 조건문을 제거하는 데 그치지 않고, 여러 소프트웨어 설계 원칙을 자연스럽게 만족시켰습니다. 예를 들어, 새로운 할인 정책이 추가될 때 기존 코드를 전혀 건드리지 않아도 되는 OCP(개방-폐쇄 원칙), 각 전략 클래스가 자신만의 책임을 갖는 SRP(단일 책임 원칙), 그리고 전략만 따로 테스트하면 되는 높은 테스트 용이성까지 갖췄죠.
즉 리팩토링 후 만든 구조는 의존성 주입(DI)과 전략 선택 흐름을 통해 자연스럽게 “스프링스러운” 아키텍처를 갖게 되었고, 이는 클린 코드에서 강조하는 설계 원칙들과도 완벽히 맞닿아 있었습니다.
마무리하며
클린 코드의 점진적 개선 과정 챕터 읽으며 가장 인상 깊었던 점은 "처음부터 완벽한 코드를 짜지 않아도 된다"는 메시지였습니다. 오히려 지저분한 초안에서 시작해, 테스트를 통과하면서 점진적으로 리팩터링해나가는 과정이야말로 실전 개발에 더 가까운 방식이라는 걸 느꼈습니다.
'개발프로젝트' 카테고리의 다른 글
[Clean Code] 프로젝트에 '창발성' 원칙을 녹여낸 할인 정책 설계 이야기 (2) | 2025.06.12 |
---|---|
[CleanCode] 실제 프로젝트에서 Clean Code 원칙을 적용한 "좋은 경계" (0) | 2025.06.02 |
[CleanCode] 실제 프로젝트에서 Clean Code 원칙을 적용한 예외 처리 리팩토링 경험기 (1) | 2025.05.26 |
[개발프로젝트] 스프링 API 예외 처리, 이제 try-catch는 그만! (1) | 2025.05.26 |
[CleanCode] 형식 맞추기 – 형식 규칙에 대한 깊은 고찰,클린코드를 읽고... (1) | 2025.05.22 |