2 분 소요

1. 전략 패턴이란

전략 패턴은 알고리즘을 그룹으로 정의하고, 각 그룹을 별도의 클래스에서 관리하며 그룹의 알고리즘을 선택적으로 사용할 수 있도록 하는 행동 디자인패턴이다.

워낙 유명하며 스프링 프레임워크 및 JAVA API 내에서도 아주 많이 찾아볼 수 있는 디자인 패턴이다.

2. 적용 계기

기존에 회사에서 지원하는 결제 수단이 많지 않았는데, 이번에 신규 앱 런칭과 더불어 다양한 결제 수단을 지원하게 되었다.

정산 모듈도 그에 맞추어 여러 결제수단에 따른 비지니스 로직을 소화하도록 변경해야 하는 상황이었다.

기존에 지원하던 결제 수단을 편의상 A, B, C 라고 칭하겠다.

최초 결제 수단을 늘리려던 시점에서는 단순하게 분기문을 통해 A,B,C 결제수단을 분기하도록 작성되어 있었다.

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
32
33
34
35
36
37
38
39
40
@Service
@RequiredArgsConstructor
public class purchaseService {

    private final PaymentMethodAService aService;
    private final PaymentMethodBService bService;
    private final PaymentMethodCService cService;

    public Result purchase(PurchaseInfo purchaseInfo) {

        PaymentMethod paymentMethod = purchaseInfo.getPaymentMethod();

        if (paymentMethod == PaymentMethod.A) {
            return aService.pay(purchaseInfo);
        } else if (paymentMethod == PaymentMethod.B) {
            return bService.pay(purchaseInfo);
        } else if (paymentMethod == PaymentMethod.C) {
            return cService.pay(purchaseInfo);
        } else {
            throw new IllegalArgumentException("payment method not found");
        }
    }

    public Result refund(RefundInfo refundInfo) {

        PaymentMethod paymentMethod = refundInfo.getPaymentMethod();

        if (paymentMethod == PaymentMethod.A) {
            return aService.refund(refundInfo);
        } else if (paymentMethod == PaymentMethod.B) {
            return bService.refund(refundInfo);
        } else if (paymentMethod == PaymentMethod.C) {
            return cService.refund(refundInfo);
        } else {
            throw new IllegalArgumentException("payment method not found");
        }
    }
}

위 코드를 보면 우선 얼핏 봐도 세 가지 정도의 문제점이 보인다.

  1. 결제 수단이 추가 되는 경우 고칠 코드가 너무 많다.
  2. 결제 관련 기능이 추가되는 경우 고칠 코드가 너무 많다.
  3. 결제 수단이 늘어날 때 마다 의존성 또한 늘어난다. (단위테스트 시 불필요한 코드작성 시간 증가)

이제 다음 코드를 전략 패턴을 적용함으로써 문제점을 해결해보자.

3. 적용

우선 공통적으로 결제, 환불 등 핵심적인 내용을 추상화 하여 인터페이스로 만든다.

1
2
3
4
public interface PaymentMethodService

Result purchase(PurchaseInfo purchaseInfo);
Result refund(RefundInfo refundInfo);

그리고 결제 수단들이 해당 인터페이스를 상속받도록 변경한다.

1
2
3
4
@Service("AMethod")
public class PaymentMethodAService implements PaymentMethodService {
    //...
}
1
2
3
4
@Service("BMethod")
public class PaymentMethodBService implements PaymentMethodService {
    //...
}

다음과 같이 여러 클래스가 인터페이스를 상속 받으면 Bean 주입 시 Map 이나 List 와 같은 컬렉션 형태로 객체들을 가져올 수 있다.

따라서 처음 코드를 다음과 같이 수정 가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Service
@RequiredArgsConstructor
public class purchaseService {

    private final Map<String, PaymentMethodService> paymentMethodServiceMap;

    public Result purchase(PurchaseInfo purchaseInfo) {

        String paymentMethod = purchaseInfo.getPaymentMethod().getName();

        return paymentMethodServiceMap.computeIfAbsent(paymentMethod, key -> {
            throw new IllegalArgumentException("payment method not found");
        }).pay(purchaseInfo);
    }

    public Result refund(RefundInfo refundInfo) {

        String paymentMethod = refundInfo.getPaymentMethod().getName();

        return paymentMethodServiceMap.computeIfAbsent(paymentMethod, key -> {
            throw new IllegalArgumentException("payment method not found");
        }).refund(refundInfo);
    }
}

다음과 같이 전략 패턴을 적용 함으로써 앞서 다룬 3가지 문제점이 깔끔하게 해결되었다.

실제로 이 이후 결제 수단이 몇 가지 추가되었으나, 기존 코드의 수정 없이 PaymentMethodService 를 상속받는 클래스만 추가함으로써 안전하고 편리한 확장이 가능 했었다. 👍