Notice
Recent Posts
Recent Comments
Link
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

유비무환

결제 로직에 전략 패턴을 적용하여 재사용성 높이기 본문

IT/개인 프로젝트

결제 로직에 전략 패턴을 적용하여 재사용성 높이기

키다리병장 2020. 4. 2. 15:45

전략 패턴은 디자인 패턴의 한 종류로 '스프링 입문을 위한 자바 객체 지향의 원리와 이해' 서적에서는 디자인 패턴의 꽃이라고 말할 정도로 중요한 패턴 중 하나입니다.

 

전략 패턴은 3가지 요소로 구성됩니다.

  • 전략 메서드를 가진 전략 객체
  • 전략 객체를 사용하는 콘텍스트
  • 전략 객체를 생성해 콘텍스트에 주입하는 클라이언트

 

즉, 클라이언트가 여러 가지 전략들 중 하나를 선택해서 콘텍스트에 주입하는 방식입니다.

개인 프로젝트를 진행하면서 겪었던 문제를 해결하는 과정에서 전략 패턴을 적용했던 사례를 소개하고자 합니다.


결제 인터페이스 구현

최근 결제 방법이 카드, 휴대폰, OO페이 등 굉장히 다양해지면서 코드를 작성할 때 확장성에 대한 고민이 생겼고 '어떻게 확장성을 높일 수 있는지'에 대한 질문이 따라 나왔습니다. 그렇다면 결제 방법이 다양하니까 이걸 추상화시키면 확장성 높은 결제 로직을 구현할 수 있고 결과적으로 주문 로직의 재사용성을 높일 수 있겠다고 생각했습니다.

 

인터페이스를 통해 결제 메서드를 선언해놓고 각 결제 방법은 인터페이스를 상속받아서 구현하도록 코드를 짰습니다. 이렇게 구성함으로써 나중에 결제 방법이 추가되더라도 인터페이스만 상속받아서 원하는 방식으로 로직을 구현할 수 있게 되었고 결제 메서드를 가져다 사용하는 쪽은 구현 로직을 모르더라도 인터페이스에 선언된 메서드만 호출하면 기능을 사용할 수 있게 되었습니다.

 

그림1. 결제 인터페이스

 

추상화도 진행했으니 이제는 결제 방법을 어떻게 주입해야 할지를 고민해야 했습니다. 어떻게 하면 구현된 결제 방법들을 필요한 곳에서 꺼내 사용할 수 있을지 고민하다 보니 Map을 사용해보자는 생각을 하게 됩니다.


Map을 사용해서 결제 코드를 구현해보자

Map에 결제 방법을 담아놓고 클라이언트에서 넘어오는 파라미터를 key 값으로 사용해서 원하는 결제 방법을 꺼내도록 코드를 구현했습니다.

 

그림2. 결제 방법이 담긴 Map

 

결제 방법을 담은 Map을 생성하는 메서드를 구현하고 필요한 곳에 주입받아서 사용할 수 있도록 빈으로 등록했습니다.

@Bean
public Map<PaymentMethod, PaymentProcessInterface> paymentMap() {
    Map<PaymentMethod, PaymentProcessInterface> map = new HashMap<>();

    map.put(PaymentMethod.CARD, new CardPaymentProcess());
    map.put(PaymentMethod.PHONE, new PhonePaymentProcess());
    map.put(PaymentMethod.KAKAO, new KakaoPaymentProcess());

    return map;
}

 

앞서 스프링에 등록한 빈을 결제 서비스에서 의존성을 주입받아서 사용합니다. 클라이언트가 전달한 파라미터 order 객체에서 어떤 방식으로 결제하는지 정의한 paymentMethod 필드를 가져오고 paymentMethod를 key값으로 Map에서 결제 방법을 꺼내서 사용합니다.

@Service
public class PaymentService {
    @Autowired
    Map<PaymentMethod, PaymentProcessInterface> map;

    public boolean process(Order order) {
        // 어떤 방식으로 결제하는지 paymentMethod를 가져옴
        PaymentMethod method = PaymentMethod.valueOf(order.getPaymentMethod());

        // paymentMethod를 key값으로 map에서 결제 방법을 가져옴
        return map.get(method).paymentProcess();
    }
}

 

지금까지 진행한 내용을 그림으로 그려보면 다음과 같습니다.

 

그림3. Map을 사용한 결제 코드

 

나름대로 인터페이스로 추상화도 진행하고 의존성도 주입받으면서 결제를 구현해봤습니다만 만들어놓고 보니 Map을 사용했을 때의 단점들이 눈에 보이기 시작했습니다.

 

첫 번째로 null에 안전하지 않습니다. Map에서 값을 꺼내기 위해선 Key가 필요한데 이때 엉뚱한 값을 Key로 사용하게 된다면 Map이 null을 반환할 수 있기 때문입니다. 물론 PaymentMethod는 Enum 값이기 때문에 상대적으로 null에 안전하지만 Enum을 사용하지 않는 경우에 Map을 사용한다면 결제 오류를 발생시킬 수도 있습니다.

map.get(PaymentMethod.CARD)	// return CardPaymentProcess
map.get(PaymentMethod.RED)	// return null

 

두 번째로 새로운 결제 프로세스를 추가할 때 Map에 해당 프로세스를 추가하지 않을 수 있습니다. 예를 들어 계좌이체 방식을 추가한다고 했을 때 계좌이체 결제 프로세스는 개발을 완료했지만 개발자의 실수로 Map에 추가하지 않았다면 null을 반환할 수 있습니다.

 

앞서 언급한 단점들을 해결하기 위해 객체지향(책, 강의 등)에 대해 공부하다 보니 전략 패턴에 대한 내용을 접하게 되었습니다. 인터페이스로 추상화를 통해 다양한 결제 방식을 구현할 수 있고 클래스 간의 결합도도 낮다는 점이 지금 가지고 있는 문제를 풀어내기에 적합하다고 판단했습니다.


결제 코드에 전략 패턴을 적용해보자

전략 패턴을 적용하기 위해선 클라이언트 역할을 어떤 곳에 맡겨야 할지 정해야 했습니다. 그래야 클라이언트가 전략을 생성해서 콘텍스트에 주입해줄 테니까요.

 

그림 3을 보면 OrderController에 create 메서드가 하나만 있는 걸 볼 수 있는데 이때는 결제 방법에 상관없이 전부 create 메서드 하나에서 넘겨받은 파라미터를 통해 결제 방법을 꺼내 사용하는 식이었습니다. 그러다 보니 파라미터 하나에 여러 결제 관련 필드들이 섞이게 되고 결제 방법이 추가될 때마다 가독성이 점점 나빠지는 게 눈에 보였습니다.

 

그래서 차라리 결제 방법을 기준으로 구체적인 메서드를 분리하고 거기에 맞는 전략을 만들어서 주입하는 게 더 낫다고 판단했습니다. 클라이언트는 주문을 할 때 결제 방법을 결정해서 서버에 넘겨주기 때문에 OrderController는 구체적인 결제 방법을 알고 있으니 클라이언트 역할을 부여하게 되었습니다.

// 카드 주문
public HttpStatus orderByCardPayment(@RequestBody CardOrder cardOrder) {
    // ...
    orderService.registerOrder(cardOrder.getCardPayment(), new CardPaymentProcess());
}

// 휴대폰 주문
public HttpStatus orderByPhonePayment(@RequestBody PhoneOrder phoneOrder) {
    // ...
    orderService.registerOrder(phoneOrder.getPhonePayment(), new PhonePaymentProcess());
}

// 카카오 주문
public HttpStatus orderByKakaoPayment(@RequestBody KakaoOrder kakaoOrder) {
    // ...
    orderService.registerOrder(kakaoOrder.getKakaoPayment(), new KakaoPaymentProcess());
}
public void registerOrder(Payment payment, PaymentProcessInterface paymentProcessInterface) {
    paymentProcessInterface.paymentProcess(payment);
}

 

전략 패턴을 적용하여 메서드 주입으로 결제 프로세스를 넘겨줌으로써 Map에서 꺼내오는 과정을 없애고 null에 안전한 로직을 만들었습니다. 또한 패턴 적용 전에 존재했던 PaymentService와 PaymentConfig 파일을 제거하여 간소화된 로직을 구현할 수 있었고 결제 프로세스를 깜빡하고 Map에 추가하지 않는 실수를 방지할 수 있게 되었습니다.

 

그림4. 전략 패턴을 적용한 결제 코드

 

전략 패턴을 적용한 것까지는 좋았는데 뭔가 아쉬운 점이 보입니다. 전략을 생성하는 방식이 메서드를 호출할 때마다 new를 통해 이뤄지고 있는데 이럴 경우 heap 메모리를 사용하게 되고 서버에서는 메모리 할당 및 회수를 위해 가비지 콜렉터가 동작하면서 병목을 일으키는 등의 서버 부하를 줄 수 있습니다. 그러면 불필요하게 많은 객체를 생성하는 걸 방지해줘야 할 텐데 스프링에 대해 공부해보신 분들이라면 금방 싱글톤을 떠올릴 수 있을 겁니다.

 

다행히 제가 만든 프로젝트는 스프링 프레임워크를 사용하고 있으니 결제 프로세스를 스프링 빈으로 등록시켜서 스프링 컨테이너에서 관리하도록 만들면 싱글톤으로 사용할 수 있게 됩니다. 한 번 적용해 봅시다.

@Service
public class CardPaymentProcess implements PaymentProcessInterface {
    //...
}
public class OrderController {
    @Autowired
    private CardPaymentProcess cardPaymentProcess;

    // 카드 주문
    public HttpStatus orderByCardPayment(@RequestBody CardOrder cardOrder) {
        // ...
        orderService.registerOrder(cardOrder.getCardPayment(), cardPaymentProcess);
    }
}

 

결제 프로세스에 @Service 어노테이션을 붙여서 스프링 빈으로 만들고 그렇게 만들어진 빈을 OrderController에서 @Autowired 어노테이션으로 주입받아서 주문 메서드에서 사용하도록 변경했습니다. 이제는 주문 메서드가 호출될 때마다 새로운 결제 프로세스 객체가 만들어질 걱정은 하지 않아도 될 것 같습니다.


마무리

전략 패턴을 적용함으로써 SOLID 원칙에서 개방 폐쇄 원칙과 의존 역전 원칙이 적용되었으니 이후 새로운 결제가 추가되더라도 결제 프로세스를 구현하고 컨트롤러에 메서드를 추가하는 것만으로 결제를 추가할 수 있게 되었습니다. 또한 클라이언트는 결제가 어떤 식으로 구현되었는지 몰라도 단순히 주입받은 결제 프로세스를 사용하는 것만으로 결제를 진행할 수 있게 되었습니다.

 

참고

토비의 스프링 3.1

Comments