바뀔 수 있는 행동을 인터페이스로 뽑아내고,
구현체를 갈아끼워서 유연하게 동작을 바꾼다
쉬운 예를 들어볼까
게임 케릭터가 공격할 때 무기를 바꿀 수 있다
- 칼로 공격 => "베기"
- 활로 공격 => "쏘기"
- 마법으로 공격 => "펑"
캐릭터는 그대로인데, 공격 방법(전략)만 갈아끼우는 것
= 전략 패턴
1. 언제 전략 패턴 사용하면 좋을까?
// if-else 지옥
public void pay(Order order, String type) {
if (type.equals("kakao")) {
// 카카오페이 로직 30줄
} else if (type.equals("card")) {
// 카드로직 30줄
} else if (type.equals("naver")) {
// 네이버페이 로직 30줄
}
// 새 결제수단 추가 -> 계속 추가 될 수 밖에 없는 구조
}
신규 결제 수단 추가할 때마다 기존 코드를 건드려야 한다
=> 버그 위험은 높아지고 테스트 범위도 많아짐
=> => 전략 패턴으로 해결할 수 있음
2. 실무 코드 - 결제 시스템
PaymentStrategy.java => interface
// 1. 전략 인터페이스
public interface PaymentStrategy {
PaymentResult pay(Order order);
String getType();
}
kakaoPayment.java ==> 각 결제수단 구현
@Component
public class KakaoPayStrategy implements PaymentStrategy {
private final KakaoPayClient kakaoPayClient; // 외부 API 클라이언트
public PaymentResult pay(Order order) {
// 카카오페이만의 로직이 여기에 격리됨
KakaoPayRequest req = KakaoPayRequest.from(order);
return kakaoPayClient.requestPay(req);
}
public String getType() { return "kakao"; }
}
@Component
public class CardPayStrategy implements PaymentStrategy {
public PaymentResult pay(Order order) {
// 카드 결제 로직
}
public String getType() { return "card"; }
}
@Component
public class TossPayStrategy implements PaymentStrategy {
public PaymentResult pay(Order order) {
// 토스 결제 로직
}
public String getType() { return "toss"; }
}
paymentContext.java ==> 전략을 관리하는 Context
@Service
public class PaymentService {
// Spring이 PaymentStrategy 구현체들을 전부 주입해줌
private final Map<String, PaymentStrategy> strategyMap;
public PaymentService(List<PaymentStrategy> strategies) {
this.strategyMap = strategies.stream()
.collect(Collectors.toMap(PaymentStrategy::getType, s -> s));
}
public PaymentResult pay(Order order, String payType) {
PaymentStrategy strategy = strategyMap.get(payType);
if (strategy == null) {
throw new IllegalArgumentException("지원하지 않는 결제수단: " + payType);
}
return strategy.pay(order);
}
}
네이버 페이 추가하고 싶으면 ?
=> 클래스 하나만 만들면 끝
@Component
public class NaverPayStrategy implements PaymentStrategy {
public PaymentResult pay(Order order) {...}
public String getType() { return "naver"; }
}
// paymentService 코드는 한 줄도 안건드림 !!
3. 쇼핑몰에서 더 쓸 수있는 곳 ?
알림 발송 -> 카카오 알림톡 분기
=> 조건에 따라 로직이 달라진다 하면 전략 패턴 고려 해보자
4. 실무에서 주의할 점은 ?
전략이 2~3개 이하고 앞으로 안늘어날 것 같으면
=> 그냥 if else가 나을 수 있음 , 오버엔지니어링 주의
5.한줄 요약
- if-else 가 길어지고, 조건이 앞으로도 늘어날 것 같다 => 전략 패턴 꺼낼 타이밍
+ 부록
paymentContext.java ==> 전략을 관리하는 Context
@Service
public class PaymentService {
// Spring이 PaymentStrategy 구현체들을 전부 주입해줌
private final Map<String, PaymentStrategy> strategyMap;
public PaymentService(List<PaymentStrategy> strategies) {
this.strategyMap = strategies.stream()
.collect(Collectors.toMap(PaymentStrategy::getType, s -> s));
}
public PaymentResult pay(Order order, String payType) {
PaymentStrategy strategy = strategyMap.get(payType);
if (strategy == null) {
throw new IllegalArgumentException("지원하지 않는 결제수단: " + payType);
}
return strategy.pay(order);
}
}
전체 흐름은 아래와 같이 이루어짐
Spring 컨테이너
│
├── KakaoPayStrategy Bean
├── CardPayStrategy Bean → List<PaymentStrategy>로 한번에 주입
└── NaverPayStrategy Bean
│
▼
PaymentService
strategyMap = {
"kakao" → KakaoPayStrategy,
"card" → CardPayStrategy,
"naver" → NaverPayStrategy
}
1. 생성자 주입 - Spring 마법 부분
public PaymentService(List<PaymentStrategy> strategies) {
Spring이 여기서 하는 일 :
PaymentStrategy 인터페이스를 구현한 Bean을 전부 찾아서 List로 만들어 넣어줌
@Component KakaoPayStrategy ┐
@Component CardPayStrategy ├→ List<PaymentStrategy> [kakao, card, naver]
@Component NaverPayStrategy ┘
내가 직접 new KakaoPayStrategy() 안해도됨. Spring 이 알아서 다 모아줌
2. Map 으로 변환 - 핵심 부분
public PaymentService(List<PaymentStrategy> strategies) {
this.strategyMap = strategies.stream()
.collect(Collectors.toMap(PaymentStrategy::getType, s -> s));
}
단계별로 쪼개면
// strategies = [KakaoPayStrategy, CardPayStrategy, NaverPayStrategy]
strategies.stream()
// → Stream<PaymentStrategy> 흐름 시작
.collect(Collectors.toMap(
PaymentStrategy::getType, // key : "kakao", "card", "naver"
s -> s // value : 전략 객체 자체
))
// 결과:
// {
// "kakao" → KakaoPayStrategy 객체,
// "card" → CardPayStrategy 객체,
// "naver" → NaverPayStrategy 객체
// }
Map 으로 만드는 이유 => 나중에 O(1) 로 꺼내 쓰려고
3.실제 결제 호출 부분 - 사용 부분
public PaymentResult pay(Order order, String payType) {
// "kakao" 넣으면 KakaoPayStrategy 꺼내옴
PaymentStrategy strategy = strategyMap.get(payType);
// 없는 결제수단이면 예외
if (strategy == null) {
throw new IllegalArgumentException("지원하지 않는 결제수단: " + payType);
}
// 꺼낸 전략으로 결제 실행
return strategy.pay(order);
}
호출 흐름 :
Controller
│
│ paymentService.pay(order, "kakao")
▼
PaymentService
│
│ strategyMap.get("kakao")
▼
KakaoPayStrategy.pay(order) ← 여기서 실제 카카오페이 API 호출
│
▼
PaymentResult 반환
5. 새 결제수단 추가할 때 변경범위
// 이것만 추가하면 끝
@Component
public class TossPayStrategy implements PaymentStrategy {
public PaymentResult pay(Order order) { ... }
public String getType() { return "toss"; }
}
PaymentService → 코드 변경 없음
KakaoPayStrategy → 코드 변경 없음
TossPayStrategy → 새로 추가
Spring이 @Component 붙은 걸 자동으로 감지해서 List 에 넣어주기 때문에,
PaymentService는 토스가 추가된 줄도 몰라. 그냥 Map에 "toss" 키가 하나 더 생길 뿐임
이게 OCP (개방 - 폐쇄 원칙) - 확장엔 열려있고, 수정엔 닫혀 있다.
+ 부록 2
bean 이란 ?
Spring 컨테이너가 관리하는 객체
- 인터페이스 구현 여부와는 상관 없음
@Component // => 이게 있으면 Bean
public class KakaoPayStrategy implements PaymentStartegy { ... }
@Component // => 이것도 Bean (인터페이스 구현 안해도 )
public class OrderService {... }
@Service // => 이것도 Bean (@Service도 내부적으로 @Component)
public class PaymentService {... }
Bean 등록 방법
@Component //일반 컴포넌트
@Service //서비스계층 (의미상 구분용)
@Repository //DB 접근 계층
@Controller //웹 계층
@Bean // @Configuration 클래스 안에서 수동 등록
이 중에 하나라도 붙어있으면 -> Sprig이 객체를 직접 만들어서 관리 = Bean
'JAVA' 카테고리의 다른 글
| 7장 여러 데이터를 하나에 넣을 수는 없을까요? (0) | 2023.03.07 |
|---|---|
| 6장 제가 조건을 좀 따져요 (0) | 2023.02.17 |
| 5장 계산을 하고 싶어요 (0) | 2023.02.04 |
| 자바의 신 4장_ [변수] 정보를 어디에 넣고 싶은데 (0) | 2023.01.29 |
| 3장 자바를 제대로 알려면 ..-2 (0) | 2023.01.28 |