[Spring] 디자인패턴 적용 - 전략 패턴
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");
}
}
}
위 코드를 보면 우선 얼핏 봐도 세 가지 정도의 문제점이 보인다.
- 결제 수단이 추가 되는 경우 고칠 코드가 너무 많다.
- 결제 관련 기능이 추가되는 경우 고칠 코드가 너무 많다.
- 결제 수단이 늘어날 때 마다 의존성 또한 늘어난다. (단위테스트 시 불필요한 코드작성 시간 증가)
이제 다음 코드를 전략 패턴을 적용함으로써 문제점을 해결해보자.
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
를 상속받는 클래스만 추가함으로써 안전하고 편리한 확장이 가능 했었다. 👍