문제 정의 및 도입 배경
쇼핑몰 프로젝트에서 Customer는 다양한 방식으로 결제할 수 있다.
- 일반 카드 결제
- 포인트 결제
- 쿠폰 할인 결제
이러한 다양한 결제 로직을 PaymentService 내부에서 모두 처리하게 되면
- 조건문이 복잡해짐
- 새로운 결제 방식 추가 시 기존 코드를 수정해야 함
- 한 메소드에서 여러가지를 테스트하기 어려움
따라서, 전략 패턴(Strategy Pattern)을 통해 결제 방식의 구현을 외부로 위임하여 결제 방식 구현에 확장성을 높이려고 한다.
추후에 로그인 방식의 확장도 이 방법을 통해 활용할 수 있을 것 같다.
Strategy Pattern을 이용한 다양한 결제 방식 구현
public interface Payment {
public void pay(int amount, Customer customer);
}
일단 결제방식마다 결제하는 방법이 다 다르지만 소비자가 특정 가격을 지불한다는 공통점이 있기 때문에 pay() 추상메소드를 Payment 인터페이스에 작성한다.
처음에 pay() 메소드의 파라미터를 Order로 받으려고 했으나 int amount 와 Customer로 받았다.
그 이유는, 전략 객체가 Order에 개입하지 않기 위함과 테스트 시 가격과 소비자 외의 Order의 객체들을 세팅해야하는 불필요함이 있기 때문이다.
@Component("CARD")
public class CardPayment implements Payment {
@Override
public void pay(int amount, Customer customer) {
// 카드결제
}
}
@Component("KAKAO")
public class KakaoPayment implements Payment {
@Override
public void pay(int amount, Customer customer) {
// 카카오페이 결제
}
}
이렇게 각 결제 전략마다 클래스를 만들어준다.
이 때, @Component를 각 전략 클래스마다 붙여준다. 왜 이렇게 @Component를 붙여줄까?
전략 패턴을 스프링 환경에서 사용할 때, 각 전략 구현체(CardPayment, KakaoPayment 등)를 직접 new로 생성하는 대신,
스프링이 자동으로 생성하고 관리를 해주는것이 더 편리하다. 스프링 환경에서는 @Component를 붙이게 되면 자동으로 그 클래스의 객체가 Bean으로 따로 관리되기 때문이다.
private final Map<String, Payment> strategyMap = Map.of(
"CARD", new CardPayment(),
"KAKAO", new KakaoPayment()
);
이런식으로 코드를 작성하게 되면, 나중에 확장성 측면에서 코드를 수정해야 할 일이 생기기에 위처럼 @Component("CARD"), @Component("KAKAO")로 선언해두면, 스프링은 내부적으로 Map을 자동으로 생성할 수 있다.
@Service
@RequiredArgsConstructor
public class PaymentService {
private final Map<String, Payment> paymentStrategyMap;
public void pay(String paymentType, int amount, Customer customer) {
Payment strategy = paymentStrategyMap.get(paymentType.toUpperCase());
if (strategy == null) {
throw new IllegalArgumentException("Unknown payment type: " + paymentType);
}
strategy.pay(amount, customer);
}
}
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentService paymentService;
@Transactional
public void payForOrder(PaymentRequest request) {
Order order = orderRepository.findById(request.getOrderId())
.orElseThrow(() -> new IllegalArgumentException("Not found order"));
paymentService.pay(request.getPaymentType(), order.getTotalAmount(), order.getCustomer());
order.setStatus(OrderStatus.PAYMENT_COMPLETED);
}
}
전략 패턴을 이용하지 않은 경우와 한 경우의 차이점
항목 | 전략 적용 X | 전략 적용 O |
전략 관리 방식 | if / switch (복잡함) | Map<String, Payment> (간단함) |
결제 방식 추가 시 | 서비스 수정 필요 | 전략 클래스만 추가 |
테스트 | 하나의 메서드에서 해야 함 | 전략별로 단위 테스트 가능 |
이렇게 전략 패턴을 이용하게 된다면, 결제 방식이 늘어나더라도 서비스 로직에 영향을 주지 않고 새로운 결제 전략 클래스만 추가하면 되기 때문에 OCP(개방-폐쇄 원칙)을 지킬 수 있다.
또한, 각 전략은 독립적인 객체이기 때문에 전략 단위로 테스트할 수 있어 테스트 효율성이 향상된다.
추후 전략 패턴 이용 예시
전략 패턴은 결제 방식 뿐만아니라 쇼핑몰에서 사용하는 다양한 전략에 이용할 수 있다는 것을 알았다.
- 로그인: EmailLoginStrategy, OAuthLoginStrategy
- 할인: NewMemberDiscountStrategy, CouponDiscountStrategy
'ShoppingMall Project' 카테고리의 다른 글
[Shop Project] HTTP vs HTTPS, SSL인증서 생성하기(2) (0) | 2025.05.29 |
---|---|
[Shop Project] HTTP vs HTTPS, SSL인증서 생성하기 (1) (1) | 2025.05.28 |
[Shop Project] 팩토리 메소드 패턴(Factory Method Pattern)을 활용한 주문 방식 분리 (0) | 2025.05.26 |