[2] 디자인패턴: 전략 패턴 (Strategy)

2025. 12. 28. 23:54·Java/Design Pattern

1. 들어가며

 디자인 패턴의 세계에 발을 들이면 가장 먼저, 그리고 가장 빈번하게 마주치는 것이 바로 전략 패턴(Strategy Pattern)이다. 이는 단순히 객체지향 패턴 중 하나라기보다는, 변경에 대응하는 소프트웨어를 만드는 가장 기본적인 사고방식에 가깝다.

 

 특히 스프링 부트의 핵심 철학인 의존성 주입(DI) 은 전략 패턴을 프레임워크 차원에서 일반화한 것이라 해도 과언이 아니다. 이번 장에서는 전략 패턴의 정의를 넘어, 왜 실무에서 이 패턴이 자연스럽게 등장하는지, 그리고 스프링 부트 환경에서 어떻게 활용되는지를 중심으로 살펴본다.


2. 전략 패턴의 정의와 필요성

전략 패턴이란 객체가 수행할 수 있는 행위들을 각각의 전략(Strategy) 으로 분리하고, 실행 시점에 어떤 전략을 사용할지 결정하도록 만드는 디자인 패턴이다.

  • “무엇을 할 것인가” 는 고정하고
  • “어떻게 할 것인가” 만 교체 가능하게 만드는 구조

이 패턴의 핵심은 변하는 부분을 고립시키는 것이며, 이를 통해 변경에 강한 구조를 만들 수 있다.

2.1. 문제 상황: 끊임없는 if-else의 굴레

 우리가 배달 어플리케이션을 만든다고 가정해보자. 결제 수단으로 '카드 결제'만 존재할 때는 아무런 문제가 없다. 하지만 사업이 확장되어 '네이버페이', '카카오페이', '쿠폰 결제'가 추가된다면 코드는 어떻게 변할까?

public class PaymentService {
    public void processPayment(String method, int amount) {
        if (method.equals("CARD")) {
            // 카드 결제 로직...
        } else if (method.equals("NAVER_PAY")) {
            // 네이버페이 로직...
        } else if (method.equals("KAKAO_PAY")) {
            // 카카오페이 로직...
        }
        // 새로운 결제 수단이 추가될 때마다 이 코드를 계속 수정해야 한다.
    }
}

하지만 서비스가 성장하면서 결제 수단이 추가될수록 문제는 점점 명확해진다. 이러한 구조는 단순히 코드가 지저분해지는 문제를 넘어, 테스트 작성과 유지보수까지 어렵게 만드는 구조적 문제로 이어진다.

[1] 조건문이 계속 늘어나 가독성이 떨어진다.
[2] 새로운 결제 수단을 추가할 때마다 기존 코드를 수정해야 한다.
[3] OCP(Open-Closed Principle), 즉 확장에는 열려 있고 수정에는 닫혀 있어야 한다는 원칙을 정면으로 위반한다.

2.2. 해결책: 알고리즘의 캡슐화 (★)

 전략 패턴은 이 문제를 “변하는 부분과 변하지 않는 부분을 분리” 함으로써 해결한다. 결제라는 행위 자체는 변하지 않지만, 결제를 수행하는 구체적인 알고리즘(카드, 카카오페이, 네이버페이 등) 은 계속 변한다.

 따라서 결제 로직을 인터페이스 뒤로 숨기고, PaymentService는 구체적인 결제 방식에 대해 알지 못하도록 만든다. PaymentService는 단지 “결제하라”라는 역할만 수행하고, 실제 결제 방식은 주입된 전략 객체에게 위임하게 된다.


3. 전략 패턴의 구조

전략 패턴은 크게 세 가지 구성 요소로 나뉜다. 즉, 전략 패턴을 적용할 때 아래에 있는 3개의 요소를 코드로 구현해야 한다.

  1. 전략 인터페이스 (Strategy Interface): 모든 전략 서비스가 구현해야 하는 공통의 인터페이스다.
  2. 구체적인 전략 클래스 (Concrete Strategy): 인터페이스를 상속받아 실제로 비즈니스 로직을 구현한 클래스들이다.
  3. 컨텍스트 (Context): 전략을 사용하는 주체다. 여기서는 PaymentService가 해당된다.

4. 스프링 부트로 구현하는 전략 패턴

 이제 스프링 부트의 기능을 활용해 실제 동작하는 코드로 구현해보자. 스프링은 생성자 주입을 통해 전략 패턴을 매우 우아하게 처리한다.

4.1. Enum 기반의 타입 안정성 확보

먼저 결제 타입을 Enum으로 관리하여 오타로 인한 런타임 에러를 원천 차단한다.

public enum PaymentType {
    CARD, KAKAO_PAY, NAVER_PAY, COUPON
}

4.2. 전략 인터페이스와 구현체

각 전략은 자신이 어떤 PaymentType을 처리할 수 있는지 명시해야 한다.

public interface PaymentStrategy {
    void pay(int amount);
    PaymentType getType(); // 자신이 처리할 수 있는 타입을 반환
}
@Component
public class CardPaymentStrategy implements PaymentStrategy {

    @Override
    public void pay(int amount) {
        System.out.println("신용카드로 " + amount + "원을 결제합니다.");
    }
    
    @Override
    public PaymentType getType() {
        return PaymentType.CARD;
    }
    
}
@Component
public class KakaoPayStrategy implements PaymentStrategy {

    @Override
    public void pay(int amount) {
        System.out.println("카카오페이로 " + amount + "원을 결제합니다.");
    }
    
    @Override
    public PaymentType getType() {
        return PaymentType.KAKAO_PAY;
    }
    
}

4.3. 전략 선택기(Provider)의 도입

PaymentService가 직접 Map에서 전략을 찾는 것보다, 전용 선택기를 두는 것이 응집도를 높이는 길이다.

@Component
public class PaymentStrategyProvider {

    private final Map<PaymentType, PaymentStrategy> strategies;

    // 생성자 주입을 통해 모든 구현체를 리스트로 받아 Map으로 변환
    public PaymentStrategyProvider(List<PaymentStrategy> strategyList) {
        this.strategies = strategyList.stream()
            .collect(Collectors.toUnmodifiableMap(PaymentStrategy::getType, s -> s));
    }

    public PaymentStrategy getStrategy(PaymentType type) {
        return Optional.ofNullable(strategies.get(type))
            .orElseThrow(() -> new IllegalArgumentException("지원하지 않는 결제 타입입니다."));
    }
}

4.4. 컨텍스트(Service)에서의 실행

이제 PaymentService는 전략을 '고르는 로직'에서 해방되어 본연의 비즈니스 흐름에만 집중한다.

@Service
@RequiredArgsConstructor
public class PaymentService {

    private final PaymentStrategyProvider strategyProvider;

    @Transactional
    public void processOrder(PaymentType type, int amount) {
        // 1. 사전 검증 로직
        // 2. 적절한 전략 선택 (위임)
        PaymentStrategy strategy = strategyProvider.getStrategy(type);
        // 3. 전략 실행
        strategy.pay(amount);
        // 4. 사후 처리 로직 (이벤트 발행 등)
    }
}

5. 실무적 고려사항: 언제 쓰고 언제 피해야 하는가?

5.1. 도입 기준 (Why & When)

  • 복잡한 분기 로직: 동일한 행위에 대해 3개 이상의 복잡한 if-else 혹은 switch가 존재할 때.
  • 빈번한 요구사항 변화: 새로운 결제 수단, 새로운 할인 정책 등이 주기적으로 추가될 가능성이 높을 때.
  • 런타임 결정: 사용자 설정이나 환경 변수에 따라 실행 중에 로직을 갈아 끼워야 할 때.

5.2. 전략 패턴의 단점 (Over-engineering 주의)

  • 클래스 수 폭증: 로직이 매우 단순함에도 패턴을 적용하면 관리해야 할 클래스와 인터페이스가 늘어나 가독성을 해칠 수 있다.
  • 오버헤드: 단순히 값 하나를 바꾸는 정도라면 전략 패턴보다는 단순 파라미터화가 낫다.
  • 복잡성 증가: 분기문이 2개 이하이며 향후 확장이 거의 확실히 없는 경우라면 if문이 훨씬 명확하다.

5.3. 테스트 코드의 명확성

전략 패턴을 적용하면 각 알고리즘을 독립적으로 검증하기 매우 쉬워진다.

class CardPaymentStrategyTest {

    @Test
    void 카드_결제_로직이_정상_동작한다() {
        // Given
        PaymentStrategy strategy = new CardPaymentStrategy();

        // When & Then
        assertThatCode(() -> strategy.pay(10_000))
                .doesNotThrowAnyException();
    }

    @Test
    void 결제_금액이_0_이하면_예외가_발생한다() {
        // Given
        PaymentStrategy strategy = new CardPaymentStrategy();

        // When & Then
        assertThatThrownBy(() -> strategy.pay(0))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("결제 금액은 0보다 커야 합니다.");
    }
}

 

'Java > Design Pattern' 카테고리의 다른 글

[6] 디자인패턴: 팩토리 패턴 (Factory)  (0) 2025.12.29
[5] 디자인패턴: 데코레이터 패턴 (Decorator)  (0) 2025.12.29
[4] 디자인패턴: 프록시 패턴 (Proxy)  (0) 2025.12.29
[3] 디자인패턴: 템플릿 패턴 (Template Method/Callback)  (0) 2025.12.29
[1] 디자인패턴: 왜 스프링 부트 개발자에게 GoF는 필수인가  (0) 2025.12.28
'Java/Design Pattern' 카테고리의 다른 글
  • [5] 디자인패턴: 데코레이터 패턴 (Decorator)
  • [4] 디자인패턴: 프록시 패턴 (Proxy)
  • [3] 디자인패턴: 템플릿 패턴 (Template Method/Callback)
  • [1] 디자인패턴: 왜 스프링 부트 개발자에게 GoF는 필수인가
h6bro
h6bro
백엔드 개발자의 기술 블로그
  • h6bro
    Jun's Tech Blog
    h6bro
  • 전체
    오늘
    어제
    • 분류 전체보기 (250) N
      • Java (18)
        • Core (9)
        • Design Pattern (9)
      • Spring (80)
        • Core (24)
        • MVC (6)
        • DB (10)
        • JPA (26)
        • Monitoring (3)
        • Security (11)
        • WebSocket (0)
      • Database (33)
        • Redis (15)
        • MySQL (18)
      • MSA (25) N
        • MSA 기본 (11)
        • MSA 아키텍처 (14) N
      • Kafka (30)
        • Core (18)
        • Connect (12)
      • ElasticSearch (11)
        • Search (11)
        • Logging (0)
      • Test (4)
        • k6 (4)
      • Docker (9)
      • CI&CD (10)
        • GitHub Actions (6)
        • ArgoCD (4)
      • Kubernetes (18)
        • Core (12)
        • Ops (6)
      • Cloud Engineering (4)
        • AWS Infrastructure (3)
        • AWS EKS (1)
        • Terraform (0)
      • Project (8)
        • LinkFolio (1)
        • Secondhand Market (7)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

    • Cloud Engineering 포스팅 정리
  • 인기 글

  • 태그

    ㅈ
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
h6bro
[2] 디자인패턴: 전략 패턴 (Strategy)
상단으로

티스토리툴바