[Design Pattern] 전략 패턴

olwooz·2023년 3월 2일
0

Design Pattern

목록 보기
20/22
알고리즘들의 패밀리를 정의해 각각 별도의 클래스에 집어넣어 객체들끼리 상호 교환 가능하게 만드는 행동 패턴

문제

내비 앱 가정

  • 가장 빠른 경로 찾기 기능, 자동차 경로만 지원하다가 도보 경로, 대중교통, 자전거 등 추가
  • 새 경로 알고리즘 추가할 때바다 내비 클래스는 두 배로 커지는 상황
  • 알고리즘 하나가 변경되면 클래스 전체에 영향 끼침
  • 새로운 기능 구현 → 거대한 클래스 변경, 코드 충돌 발생

해결책

전략 패턴 - 특정 작업을 다양한 방법으로 하는 클래스에서 알고리즘들을 전략이라는 별도 클래스로 추출

원본 클래스(컨텍스트) - 전략 중 하나에 대한 참조 저장, 컨텍스트는 작업을 연결된 전략 객체에 위임

컨텍스트는 알고리즘을 선택하는 데 책임 없음, 클라이언트가 컨텍스트에 원하는 전략 전달

  • 컨텍스트는 동일한 일반 인터페이스를 통해 모든 전략을 다루기 때문에 전략에 대해 잘 모름

→ 컨텍스트는 concrete 전략에 의존하지 않아 기존 코드에 영향을 주지 않고 새 알고리즘 추가 가능

내비 앱 → 각 경로 알고리즘은 각각 단일 buildRoute 메서드를 가지는 클래스로 추출될 수 있음

  • 해당 메서드는 출발지와 도착지를 받아 경로 체크포인트들의 집합 반환
  • 각 경로 클래스는 같은 인수를 받아도 다른 경로를 만듦
  • 메인 내비 클래스의 역할은 체크포인트 집합을 지도에 렌더링하는 것이기 때문에 어떤 알고리즘이 선택되든 신경쓰지 않음
  • 클래스는 활성화된 경로 구축 전략을 변경하는 메서드를 가져 UI에 있는 버튼과 같은 클라이언트들이 경로 구축 전략을 바꿀 수 있게 해줌

구조

1. 컨텍스트 - concrete 전략 중 하나에 대한 참조를 가지며, 전략 인터페이스를 통해서만 해당 객체와 소통함

2. 전략 - 모든 concrete 전략들에게 공통된 인터페이스
   - 컨텍스트가 전략을 실행하는 데 사용하는 메서드 선언
    
3. concrete 전략 - 컨텍스트가 사용하는 알고리즘의 다양한 변형 구현

4. 컨텍스트는 알고리즘을 실행해야 할 때마다 연결된 전략 객체의 실행 메서드 호출
   - 컨텍스트는 무슨 전략인지, 어떻게 알고리즘이 실행되는지 모름
   
5. 클라이언트 - 특정 전략 객체를 생성해 객체에 전달
   - 컨텍스트는 클라이언트가 컨텍스트와 연관된 전략을 런타임에 변경할 수 있게 해주는 setter 노출

적용

객체 내부 알고리즘의 여러 변형을 사용하고 싶고 런타임에 알고리즘 전환이 필요한 경우

- 특정 서브태스크를 다른 방식으로 수행할 수 있는 서브 객체를 연동해 
  객체의 행동을 런타임에 간접적으로 변경할 수 있게 해줌

특정 행동을 실행하는 방식만 다른 여러 개의 비슷한 클래스가 있는 경우

- 변화하는 행동을 별도 클래스 계층으로 추출하고 기존 클래스들을 하나로 합쳐 코드 중복 감소

클래스의 비즈니스 로직을 로직의 컨텍스트에서 중요하지 않은 알고리즘의 구현 세부 사항으로부터 격리하려는 경우

- 다양한 알고리즘들의 코드, 내부 데이터, 의존성을 나머지 코드로부터 분리 가능
- 클라이언트들은 알고리즘을 실행하고 런타임에 변경하는 간단한 인터페이스 획득

클래스에 같은 알고리즘의 여러 변형들끼리 변경하는 거대한 조건문이 존재하는 경우

- 모든 알고리즘들을 같은 인터페이스를 구현하는 별도 클래스들로 추출
- 기존 객체는 알고리즘의 모든 변형을 구현하는 대신 전략 객체 중 하나에 실행을 위임

구현방법

1. 컨텍스트 클래스에서 자주 변화하는 알고리즘 식별
   - 런타임에 동일한 알고리즘의 변형을 선택하고 실행하는 거대한 조건문일수도 있음
    
2. 알고리즘의 모든 변형들에 공통되는 전략 인터페이스 선언

3. 모든 알고리즘을 각각 전략 인터페이스를 구현하는 별도 클래스로 추출

4. 컨텍스트 클래스에 전략 객체에 대한 참조를 저장하는 필드 추가
   - 해당 필드 값을 변경하는 setter 제공
   - 컨텍스트는 전략 인터페이스를 통해서만 전략 객체와 소통해야 함
   - 컨텍스트는 전략이 자신의 데이터에 접근할 수 있게 해주는 인터페이스를 정의할 수도 있음
    
5. 컨텍스트의 클라이언트들은 원하는 주요 작업 수행 방식에 맞게 컨텍스트와 전략을 매치시켜줘야 함

장단점

장점

- 객체 내부에서 사용되는 알고리즘을 런타임에 변경 가능
- 알고리즘의 구현 세부 사항을 알고리즘을 사용하는 코드로부터 격리 가능
- 상속을 합성으로 변경 가능
- OCP - 컨텍스트 변경 없이 새 전략 추가 가능

단점

- 거의 바뀌지 않는 몇 개의 알고리즘만 있는 경우, 굳이 패턴을 적용해 프로그램을 과도하게 복잡하게 만들 필요 없음
- 클라이언트가 적절한 전략을 선택하기 위해 전략 간 차이점에 대해 인지하고 있어야 함
- 대부분 현대 프로그래밍 언어들은 익명 함수 집합 안에 알고리즘의 다양한 버전을 구현할 수 있게 해주는 함수 타입 지원 
  → 추가 클래스, 인터페이스 없이 전략 패턴과 똑같이 사용할 수 있음

다른 패턴과의 관계

- 브리지, 파사드, 전략, 어댑터 - 다른 객체에 작업을 위임하는 합성 기반 패턴이라는 점에서 유사하지만 
  모두 다른 문제를 해결함

- 커맨드 패턴과 전략 패턴은 객체를 특정 작업으로 매개변수화할 수 있다는 점에서 비슷해 보이지만 다른 의도를 가짐
  커맨드 - 어떤 작업이든 객체로 변환할 수 있음, 작업의 매개변수는 객체의 필드가 됨, 
          해당 변환은 작업 지연 실행, 작업을 대기열에 추가, 커맨드들의 history 저장, 
          커맨드들을 원격 서비스에 전달하는 등 기능을 가능하게 함
  전략 - 주로 같은 일을 하는 여러 방법들을 설명해, 단일 컨텍스트 클래스 내부에서 
         해당 알고리즘들을 바꿔가며 사용할 수 있게 함
    
- 데코레이터 패턴은 객체의 피부를 변경할 수 있고, 전략 패턴은 객체의 내장을 변경할 수 있음

- 템플릿 메서드 패턴 - 상속 기반, 알고리즘의 부분들을 서브클래스에서 확장해 변경, 
  클래스 레벨에서 동작해 정적임
  전략 패턴 - 합성 기반, 객체 행동의 부분들을 다른 전략을 제공해줌으로써 변경, 
  객체 레벨에서 동작해 런타임에 행동들을 변경
    
- 상태 패턴 - 전략 패턴의 확장으로 간주할 수 있음, 두 패턴 모두 합성 기반
    - 전략 패턴 - 객체들은 완전히 독립적이고 서로 인식 못 함
    - 상태 패턴 - concrete 상태 간 의존성에 제한을 두지 않고 스스로 컨텍스트의 상태를 변경할 수 있게 함

TypeScript 예제

/**
 * The Context defines the interface of interest to clients.
 */
class Context {
    /**
     * @type {Strategy} The Context maintains a reference to one of the Strategy
     * objects. The Context does not know the concrete class of a strategy. It
     * should work with all strategies via the Strategy interface.
     */
    private strategy: Strategy;

    /**
     * Usually, the Context accepts a strategy through the constructor, but also
     * provides a setter to change it at runtime.
     */
    constructor(strategy: Strategy) {
        this.strategy = strategy;
    }

    /**
     * Usually, the Context allows replacing a Strategy object at runtime.
     */
    public setStrategy(strategy: Strategy) {
        this.strategy = strategy;
    }

    /**
     * The Context delegates some work to the Strategy object instead of
     * implementing multiple versions of the algorithm on its own.
     */
    public doSomeBusinessLogic(): void {
        // ...

        console.log('Context: Sorting data using the strategy (not sure how it\'ll do it)');
        const result = this.strategy.doAlgorithm(['a', 'b', 'c', 'd', 'e']);
        console.log(result.join(','));

        // ...
    }
}

/**
 * The Strategy interface declares operations common to all supported versions
 * of some algorithm.
 *
 * The Context uses this interface to call the algorithm defined by Concrete
 * Strategies.
 */
interface Strategy {
    doAlgorithm(data: string[]): string[];
}

/**
 * Concrete Strategies implement the algorithm while following the base Strategy
 * interface. The interface makes them interchangeable in the Context.
 */
class ConcreteStrategyA implements Strategy {
    public doAlgorithm(data: string[]): string[] {
        return data.sort();
    }
}

class ConcreteStrategyB implements Strategy {
    public doAlgorithm(data: string[]): string[] {
        return data.reverse();
    }
}

/**
 * The client code picks a concrete strategy and passes it to the context. The
 * client should be aware of the differences between strategies in order to make
 * the right choice.
 */
const context = new Context(new ConcreteStrategyA());
console.log('Client: Strategy is set to normal sorting.');
context.doSomeBusinessLogic();

console.log('');

console.log('Client: Strategy is set to reverse sorting.');
context.setStrategy(new ConcreteStrategyB());
context.doSomeBusinessLogic();
// Output.txt

Client: Strategy is set to normal sorting.
Context: Sorting data using the strategy (not sure how it'll do it)
a,b,c,d,e

Client: Strategy is set to reverse sorting.
Context: Sorting data using the strategy (not sure how it'll do it)
e,d,c,b,a

참고 자료: Refactoring.guru

0개의 댓글