안녕하세요! GoF 디자인 패턴 연재, 이번 시간에는 행위 패턴(Behavioral Patterns)의 한 종류로, 유사한 알고리즘들을 각각의 클래스로 캡슐화하고, 런타임에 동적으로 알고리즘을 교체할 수 있게 해주는 스트래티지(Strategy) 패턴에 대해 알아보겠습니다.


정의

스트래티지(Strategy) 패턴다양한 전략(알고리즘)들을 각각의 클래스로 정의하고, 이들을 서로 바꿔서 사용할 수 있게 하는 패턴입니다. 즉, '어떻게(How)' 일을 처리할 것인지를 나타내는 '전략'을 객체로 만들어, 필요에 따라 클라이언트가 선택하여 사용할 수 있도록 합니다.

서울에서 부산까지 가는 방법을 생각해보겠습니다.

  • KTX를 이용하는 전략(Strategy A)
  • 버스를 이용하는 전략(Strategy B)
  • 비행기를 이용하는 전략(Strategy C)

어떤 교통수단을 선택하든 '이동한다'는 목적은 동일하지만, 이동하는 방법(알고리즘)은 각각 다릅니다. 스트래티지 패턴은 이처럼 다양한 방법(전략)들을 각각의 독립된 객체로 만들고, '여행'이라는 문맥(Context) 안에서 이 전략들을 자유롭게 갈아 끼울 수 있게 해줍니다.

 

Strategy 패턴의 구현에는 다음과 같은 요소들이 사용됩니다.

  • Strategy (전략): 모든 구체적인 전략들이 구현해야 할 공통 인터페이스입니다. 보통 알고리즘을 실행하는 단일 메서드(예: execute())를 정의합니다.
  • ConcreteStrategy (구체적인 전략): Strategy 인터페이스를 구현하는 실제 알고리즘 클래스들입니다. (예: KtxStrategy, BusStrategy)
  • Context (문맥): Strategy 객체를 참조하는 주체 객체입니다. 클라이언트로부터 받은 요청을 자신이 참조하는 Strategy 객체에게 위임하여 처리합니다. Context는 구체적인 전략이 무엇인지는 알지 못합니다.

사용예시

쇼핑 카트에서 결제할 때, 다양한 결제 방법(신용카드, 카카오페이)을 선택하여 사용하는 예제를 통해 스트래티지 패턴을 구현해 보겠습니다.

1. 전략 (Strategy) 인터페이스 정의

모든 결제 방식이 공통으로 가질 인터페이스를 정의합니다.

// Strategy 인터페이스
public interface PaymentStrategy { void pay(int amount); }

2. 구체적인 전략 (ConcreteStrategy) 클래스 구현

실제 결제 알고리즘을 담고 있는 클래스들을 구현합니다.

// ConcreteStrategy A: 신용카드 결제
class CreditCardStrategy implements PaymentStrategy {
    private String name;
    private String cardNumber;

    public CreditCardStrategy(String name, String cardNumber) {
        this.name = name;
        this.cardNumber = cardNumber;
    }

    @Override
    public void pay(int amount) {
        System.out.println(amount + "원을 신용카드(" + cardNumber + ")로 결제했습니다.");
    }
}

// ConcreteStrategy B: 카카오페이 결제
class KakaoPayStrategy implements PaymentStrategy {
    private String email;

    public KakaoPayStrategy(String email) {
        this.email = email;
    }

    @Override
    public void pay(int amount) {
        System.out.println(amount + "원을 카카오페이(" + email + ")로 결제했습니다.");
    }
}

3. 문맥 (Context) 클래스 구현

결제 전략을 사용하여 실제 결제를 수행하는 ShoppingCart 클래스를 구현합니다.

// Context 클래스
public class ShoppingCart {
    private int amount = 0;

    public ShoppingCart(int amount) { this.amount = amount; }

    // 클라이언트가 선택한 전략을 받아 결제를 수행
    public void checkout(PaymentStrategy paymentMethod) { paymentMethod.pay(amount); }
}

4. 클라이언트 (Client) 코드

클라이언트는 결제 시점에 사용할 구체적인 전략을 선택하여 Context에 전달합니다.

public class OnlineMarket {
    public static void main(String[] args) {
        // 1. Context 객체 생성
        ShoppingCart cart = new ShoppingCart(10000);

        // 2. 신용카드로 결제 (전략 선택)
        cart.checkout(new CreditCardStrategy("홍길동", "1234-5678-9012-3456"));
        
        // 3. 카카오페이로 결제 (다른 전략 선택)
        cart.checkout(new KakaoPayStrategy("gildong@example.com"));

        // 실행 결과:
        // 10000원을 신용카드(1234-5678-9012-3456)로 결제했습니다.
        // 10000원을 카카오페이(gildong@example.com)로 결제했습니다.
    }
}


결론

Strategy 패턴을 이용하면 동적으로 런타임 도중에 동일한 객체에 대한 알고리즘을 다르게 하여 적용이 가능합니다. 그 외 아래와 같은 상황에서도 Strategy 패턴을 활용하기 좋습니다,

 

  • if/else나 switch 문을 사용하여 알고리즘을 선택하는 코드를 피하고 싶을 때: 스트래티지 패턴은 조건문을 없애고, 새로운 전략을 추가할 때 기존 코드를 수정할 필요가 없게 만듭니다(OCP 준수).
  • 알고리즘의 세부 구현을 클라이언트로부터 숨기고 싶을 때: 클라이언트는 Strategy 인터페이스만 알면 되므로, 알고리즘이 어떻게 구현되었는지 알 필요가 없습니다.

지난시간에 알아본 스테이트 패턴과 유사점이 있어서 기술사 답안에 쓰면 좋을거 같은데, 스트래티지 패턴은 스테이트 패턴과 구조가 매우 유사하지만, 의도에서 큰 차이가 있습니다.

  • 스트래티지 패턴: '어떻게' 할 것인가에 초점을 맞춥니다. 클라이언트가 다양한 전략 중 하나를 선택하여 Context에 주입하고, Context는 그 전략을 그대로 사용합니다. Context의 상태와는 무관하게 전략을 바꿀 수 있습니다.
  • 스테이트 패턴: '무엇을' 할 수 있는가에 초점을 맞춥니다. ContextState 객체 스스로가 자신의 내부 상태에 따라 행동과 다음 상태를 결정하고 변경합니다. 클라이언트가 상태를 직접 선택하지 않습니다.

간단히 말해, 스트래티지 패턴은 외부에서 주입된 전략을 사용하고, 스테이트 패턴은 내부 상태에 따라 행동이 자동 전환됩니다.

스트래티지 패턴은 '상속보다는 합성을 사용하라'는 객체 지향 원칙을 잘 보여주는 또 다른 훌륭한 예입니다. 알고리즘을 독립적인 객체로 다룸으로써, 시스템을 훨씬 더 유연하고 확장 가능하게 만들어줍니다. 다음 포스팅에서는 외부의 상태관찰에 따른 디자인 행위패턴, 옵저버 패턴을 알아보겠습니다.

+ Recent posts