전략 패턴(Strategy Pattern)

서버란·2024년 10월 13일

CS 지식

목록 보기
21/25

전략 패턴(Strategy Pattern)은 행위 디자인 패턴 중 하나로, 객체의 행위를 변경할 수 있도록 하는 패턴입니다. 이 패턴은 실행 중에 알고리즘을 선택할 수 있게 해주며, 특정 기능을 수행하는 다양한 방법을 정의하고, 그 중 하나를 런타임에 선택하여 사용할 수 있도록 해줍니다. 즉, 상속 대신 객체의 구성(Composition)을 활용하여 동작을 변경할 수 있게 하는 패턴입니다.

1. 전략 패턴의 정의

전략 패턴은 알고리즘을 각각의 클래스(전략)로 캡슐화하고, 이 알고리즘들을 상호 교체 가능하도록 만드는 패턴입니다. 클라이언트는 동적으로(런타임에) 사용할 알고리즘을 선택할 수 있으며, 알고리즘을 사용하는 코드는 변경되지 않습니다.

이 패턴을 사용하면 클래스의 행위를 변경할 때 코드의 수정 없이 알고리즘을 쉽게 교체할 수 있기 때문에 유연성과 확장성을 높일 수 있습니다.

2. 전략 패턴의 구조

전략 패턴은 주로 세 가지 구성 요소로 이루어져 있습니다.

  1. Context(문맥):
  • 전략을 사용하는 클래스입니다.
  • 클라이언트로부터 전략 객체를 받아와서, 이를 사용하여 특정 작업을 수행합니다.
  • 내부적으로 전략 인터페이스에 정의된 메서드를 호출하여 동작을 위임합니다.
  1. Strategy(전략 인터페이스):
  • 알고리즘을 추상화한 인터페이스 또는 추상 클래스입니다.
  • 이 인터페이스는 전략에서 사용할 메서드의 구조를 정의하며, 실제 구현은 구체적인 전략 클래스들이 담당합니다.
  1. Concrete Strategy(구체적인 전략 클래스):
  • Strategy 인터페이스를 구현하는 여러 구체적인 알고리즘 클래스입니다.
  • 각 클래스는 Strategy 인터페이스를 구현하여 특정 알고리즘이나 행동을 제공합니다.

3. 전략 패턴의 UML 다이어그램


  +------------------+             +------------------+
  |      Context      |<--------+  |      Strategy     |
  +------------------+           |  +------------------+
  | strategy:Strategy |           |  | execute()        |
  +------------------+           |  +------------------+
  | executeStrategy() |           |
  +------------------+           |
                                 +--------+
                                          |
                     +------------------------------------+
                     |                ConcreteStrategyA   |
                     |                ConcreteStrategyB   |
                     |                ConcreteStrategyC   |
                     +------------------------------------+
  1. Context: strategy 객체를 가지고 있고, executeStrategy() 메서드에서 이 전략을 사용합니다.
  2. Strategy: execute()라는 메서드를 정의하는 추상 인터페이스입니다.
  3. ConcreteStrategy: Strategy 인터페이스를 구현하여 각기 다른 알고리즘을 제공합니다.

4. 전략 패턴의 동작 방식

  • Context 클래스는 특정 작업을 처리하는 데 사용하는 전략을 참조합니다. 이 전략은 동적으로 교체 가능하며, 외부에서 주입받는 방식(주로 의존성 주입)으로 설정됩니다.
  • Strategy 인터페이스는 수행해야 할 동작의 틀을 정의합니다. 실제 구현은 이 인터페이스를 따르는 구체적인 전략 클래스(Concrete Strategy)에서 이루어집니다.
  • Concrete Strategy는 각각 특정한 알고리즘이나 동작을 구현하며, Context는 동적으로 이 전략 객체를 사용해 동작을 실행합니다.

5. 전략 패턴의 예시 (자바)

다음은 Java에서 전략 패턴을 활용한 간단한 예시입니다. 다양한 할인 정책을 적용하는 예를 생각해 보겠습니다.

// Strategy 인터페이스
interface DiscountStrategy {
    double applyDiscount(double price);
}

// Concrete Strategy 1: 학생 할인
class StudentDiscount implements DiscountStrategy {
    @Override
    public double applyDiscount(double price) {
        return price * 0.8; // 20% 할인
    }
}

// Concrete Strategy 2: VIP 할인
class VIPDiscount implements DiscountStrategy {
    @Override
    public double applyDiscount(double price) {
        return price * 0.7; // 30% 할인
    }
}

// Concrete Strategy 3: 일반 할인 없음
class NoDiscount implements DiscountStrategy {
    @Override
    public double applyDiscount(double price) {
        return price; // 할인 없음
    }
}

// Context 클래스
class PriceCalculator {
    private DiscountStrategy discountStrategy;

    // 전략을 동적으로 변경 가능
    public void setDiscountStrategy(DiscountStrategy discountStrategy) {
        this.discountStrategy = discountStrategy;
    }

    // 전략을 이용해 가격 계산
    public double calculatePrice(double price) {
        return discountStrategy.applyDiscount(price);
    }
}

// 클라이언트 코드
public class Main {
    public static void main(String[] args) {
        PriceCalculator calculator = new PriceCalculator();
        
        // 학생 할인 적용
        calculator.setDiscountStrategy(new StudentDiscount());
        System.out.println("학생 할인 가격: " + calculator.calculatePrice(100)); // 80.0
        
        // VIP 할인 적용
        calculator.setDiscountStrategy(new VIPDiscount());
        System.out.println("VIP 할인 가격: " + calculator.calculatePrice(100)); // 70.0
        
        // 할인 없음
        calculator.setDiscountStrategy(new NoDiscount());
        System.out.println("할인 없음 가격: " + calculator.calculatePrice(100)); // 100.0
    }
}

설명:

  • DiscountStrategy는 할인 전략을 정의하는 인터페이스입니다.
  • StudentDiscount, VIPDiscount, NoDiscount는 각기 다른 할인 정책을 구현한 구체적인 전략 클래스입니다.
  • PriceCalculator는 Context 클래스로, 동적으로 할인 정책(전략)을 설정하여 최종 가격을 계산합니다.
  • 클라이언트는 가격 계산 시 할인 전략을 자유롭게 변경할 수 있습니다.

6. 전략 패턴의 장점

  1. 유연성: 전략을 런타임에 동적으로 교체할 수 있으므로, 클라이언트는 상황에 맞는 전략을 선택해 사용할 수 있습니다.
  2. 확장성: 새로운 전략(알고리즘)을 추가할 때 기존 코드를 변경할 필요 없이 새로운 클래스만 추가하면 됩니다.
  3. 응집도 향상: 각 전략을 개별 클래스로 분리하므로, 한 클래스가 여러 기능을 처리하지 않고 하나의 책임만 갖도록 할 수 있습니다(Single Responsibility Principle).
  4. 유지보수 용이: 알고리즘이나 행위를 캡슐화했기 때문에 특정 전략만 수정하면 다른 코드에 영향을 미치지 않으므로 유지보수가 쉽습니다.

7. 전략 패턴의 단점

  1. 클래스 증가: 각 알고리즘을 별도의 클래스로 구현해야 하므로 클래스 수가 늘어나 복잡도가 증가할 수 있습니다.
  2. 클라이언트가 전략을 알아야 함: 클라이언트는 어떤 전략을 사용해야 하는지 결정해야 하며, 이에 따라 전략을 선택해야 합니다. 이는 클라이언트 코드에 전략 선택의 책임을 부여할 수 있습니다.
  3. 단순한 문제에 오버엔지니어링 위험: 알고리즘이 복잡하지 않고 변경 가능성이 적은 경우에는 전략 패턴이 불필요하게 복잡할 수 있습니다.

8. 전략 패턴을 사용하는 경우

  • 알고리즘이나 동작을 동적으로 변경해야 하는 경우. 예를 들어, 다양한 할인 정책이나 결제 방법 등을 사용해야 할 때 적합합니다.
  • 클라이언트에서 여러 알고리즘 중 하나를 선택하여 실행해야 하는 경우.
  • 특정 동작을 상속이 아닌 구성을 통해 변경하고 싶을 때.

9. 템플릿 메서드 패턴과 전략 패턴의 차이점

템플릿 메서드 패턴과 전략 패턴은 비슷한 개념처럼 보일 수 있지만, 사용 방식에서 차이가 있습니다:

  • 템플릿 메서드 패턴: 알고리즘의 고정된 큰 구조가 상위 클래스에 정의되고, 세부 동작은 하위 클래스에서 결정합니다. 즉, 상속을 통해 행위의 변형을 허용합니다.
  • 전략 패턴: 동작 자체를 외부에서 전략 객체로 주입하여 변경할 수 있도록 하고, 구성(Composition)을 사용합니다. 즉, 클라이언트가 직접 어떤 전략을 사용할지 선택할 수 있습니다.

Q1: 전략 패턴에서 알고리즘의 추가가 많아지면 클래스가 늘어나게 되는데, 이를 어떻게 효율적으로 관리할 수 있을까요?

답: 전략 패턴에서 알고리즘(전략)이 많아지면 클래스를 관리하기 어려워질 수 있습니다. 이를 효율적으로 관리하려면 다음과 같은 방법을 고려할 수 있습니다:

  • 팩토리 패턴(Factory Pattern)과 결합: 팩토리 패턴을 사용하여 전략 객체의 생성 과정을 단순화하고 관리할 수 있습니다. 예를 들어, 전략 객체를 직접 생성하는 대신 팩토리에서 적절한 전략을 생성하여 반환하도록 하면, 클래스의 수가 늘어나더라도 객체 생성을 더 쉽게 관리할 수 있습니다.
  • 전략 객체를 외부에서 구성 파일이나 설정 파일로 정의: 전략의 설정을 XML, JSON, 또는 다른 설정 파일로 분리하여 필요에 따라 쉽게 교체할 수 있습니다.
  • 익명 클래스 또는 람다 표현식 사용: 간단한 전략의 경우 구체적인 클래스를 생성하지 않고 익명 클래스나 람다식을 사용해 전략을 정의할 수 있습니다. 예를 들어, Java 8에서는 함수형 인터페이스를 활용해 전략을 람다식으로 표현할 수 있습니다.

Q2: 전략 패턴을 사용하여 여러 개의 결제 방식을 구현하려면 어떻게 설계할 수 있을까요?

답: 전략 패턴을 사용하여 여러 개의 결제 방식을 구현할 때는 각 결제 방식을 별도의 전략으로 캡슐화하여 관리하면 됩니다. 각 결제 방식(예: 신용카드, 페이팔, 은행 계좌)은 PaymentStrategy라는 인터페이스를 구현하는 구체적인 전략 클래스가 될 수 있습니다. 다음과 같이 설계할 수 있습니다:

  1. Strategy 인터페이스: 결제 방식에 공통적으로 필요한 메서드를 정의합니다.
interface PaymentStrategy {
    void pay(double amount);
}
  1. Concrete Strategy: 각 결제 방식을 구체적으로 구현합니다.
class CreditCardPayment implements PaymentStrategy {
    @Override
    public void pay(double amount) {
        System.out.println("신용카드로 " + amount + "원 결제합니다.");
    }
}

class PayPalPayment implements PaymentStrategy {
    @Override
    public void pay(double amount) {
        System.out.println("PayPal로 " + amount + "원 결제합니다.");
    }
}
  1. Context 클래스: 결제 방식(전략)을 동적으로 변경하고 실행합니다.
class PaymentContext {
    private PaymentStrategy paymentStrategy;

    public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
        this.paymentStrategy = paymentStrategy;
    }

    public void executePayment(double amount) {
        paymentStrategy.pay(amount);
    }
}
  1. 클라이언트 코드: 결제 방식을 선택하고 결제를 수행합니다.
public class Main {
    public static void main(String[] args) {
        PaymentContext context = new PaymentContext();

        // 신용카드 결제
        context.setPaymentStrategy(new CreditCardPayment());
        context.executePayment(5000);

        // PayPal 결제
        context.setPaymentStrategy(new PayPalPayment());
        context.executePayment(10000);
    }
}

이렇게 하면 새로운 결제 방식을 추가할 때는 기존 코드를 변경하지 않고 새로운 전략 클래스를 추가하기만 하면 됩니다.

Q3: 전략 패턴에서 Context 객체가 전략을 선택하는 과정을 자동화할 수 있는 방법은 무엇이 있을까요?

답: Context 객체가 전략을 자동으로 선택하게 하려면, 전략 선택을 외부 요인에 따라 동적으로 결정하는 메커니즘을 추가할 수 있습니다. 다음과 같은 방법들이 있습니다:

  1. 조건문에 따른 자동 선택: 결제 금액이나 사용자 입력에 따라 Context가 전략을 선택하도록 할 수 있습니다. 예를 들어, 금액이 크면 신용카드 결제를, 금액이 작으면 PayPal 결제를 자동으로 선택하는 방식입니다.
public void selectStrategy(double amount) {
    if (amount > 10000) {
        setPaymentStrategy(new CreditCardPayment());
    } else {
        setPaymentStrategy(new PayPalPayment());
    }
}
  1. 설정 파일 또는 데이터베이스에 저장된 전략 정보 사용: 외부의 설정 파일이나 데이터베이스에서 사용자가 선호하는 전략을 불러와서 적용할 수 있습니다. 이를 통해 사용자의 환경에 맞는 전략을 자동으로 적용하게 할 수 있습니다.

  2. DI 프레임워크 사용: Spring과 같은 의존성 주입(DI) 프레임워크를 활용하여 전략 객체를 컨텍스트에 자동으로 주입할 수 있습니다. 사용자는 별도로 전략을 설정할 필요 없이, 프레임워크가 런타임에 적절한 전략을 선택하여 주입하게 할 수 있습니다.

  3. 팩토리 패턴과 결합: 팩토리 클래스를 사용하여 Context에서 사용할 전략을 결정할 수 있습니다. 예를 들어, PaymentStrategyFactory라는 클래스를 만들어 조건에 맞는 전략을 반환하도록 할 수 있습니다.

profile
백엔드에서 서버엔지니어가 된 사람

0개의 댓글