ShoppingMall Project

[Shop Project] 전략 패턴(Strategy Pattern)을 활용한 결제 방식 구현

sagecode 2025. 5. 28. 15:00

문제 정의 및 도입 배경

쇼핑몰 프로젝트에서 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