JAVA

전략 패턴 이란 ?

1son 2026. 5. 14. 22:09

 

바뀔 수 있는 행동을 인터페이스로 뽑아내고,

구현체를 갈아끼워서 유연하게 동작을 바꾼다 

 

 


쉬운 예를 들어볼까 

게임 케릭터가 공격할 때 무기를 바꿀 수 있다

 

- 칼로 공격    => "베기" 

- 활로 공격    => "쏘기"

- 마법으로 공격  => "펑"

 

캐릭터는 그대로인데, 공격 방법(전략)만 갈아끼우는 것 

= 전략 패턴 

 


 

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