전략은 일련의 행동을 객체로 전환하고 원래의 컨텍스트 객체 안에서 상호 호환이 가능하도록 하는 behavioral design pattern 이다.
컨텍스트라고 하는 원래 객체는 전략 객체에 대한 참조를 보유하고 동작 실행을 위임한다. 컨텍스트의 작업 방식을 변경하기 위해 다른 객체는 현재 연결된 전략 객체를 다른 객체로 바꿀 수 있다.
내비게이션 앱을 만든다고 가정해보자. 앱에서 가장 많이 요청된 기능으로 자동 경로 계획을 만들어야 하며 사용자는 주소를 입력하면 지도에 표시된 목적지까지의 가장 빠른 경로를 볼 수 있어야 한다.
앱의 초창기에는 도로위의 경로를 보여주어 자동차로 여행하는 사람들은 좋아했지만 모든 사람들이 자동차로 여행을 하지 않기 때문에 보행 경로 계획, 대중교통 경로 계획등 다양한 옵션들을 추가하였다.
이는 시작에 불과하여 나중에 자전거를 타는 사람들을 위한 기능과 같은 다양한 옵션을 추가하게 되면 코드는 점점 부풀어 오르게 될 것이다.
네비게이터의 코드가 부풀어 올랐다.
비즈니스 관점에서 이 앱은 성공했지만 기술적인 부분에서 골칫거리를 안겨주었다. 새 라우팅 알고리즘을 추가할 때마다 네비게이터의 기본 클래스의 크기가 두 배로 늘어났다.
단순한 버그 수정이든 거리 점수 약간 조정이든 알고리즘 중 하나를 변경하면 클래스 전체에 영향을 미쳐 이미 작동 중인 코드에 오류가 발생할 가능성이 높아졌다.
게다가 팀워크도 비효율적이게 되었다. 성공적인 출시 직후 채용된 팀원들은 Merge Conflict를 해결하는 데 너무 많은 시간을 소비한다고 불평하였고 새 기능을 구현하려면 다른 사용자가 생성한 코드와 충돌하면서 동일한 거대한 클래스를 변경해야했다.
전략 패턴은 다양한 방식으로 특정 작업을 수행하는 클래스를 선택하여 이 모든 알고리즘을 Strategy라고 하는 별도의 클래스로 추출할 것을 제안한다. (state 패턴과 비슷 해 보이지만 state는 다음 상태들에대해 어느정도 알고있지만 이 패턴은 아님)
컨텍스트라는 원래 클래스에는 전략 중 하나에 대한 참조를 저장하기 위한 필드를 두고, 컨텍스트는 작업을 자체적으로 실행하는 대신 연결된 전략 객체로 위임한다.
컨텍스트에서 작업에 적합한 알고리즘을 선택할 수 없는 대신 클라이언트는 원하는 전략을 컨텍스트에 전달하여 사용할 수 있다. 이렇듯, 컨텍스트는 전략에 대해 잘 알지 못하면서 동일한 일반 인터페이스를 통해 모든 전략과 함께 작동하며, 선택한 전략 내에 캡슐화된 알고리즘을 트리거하는 단일 방법만 노출된다.
이렇게 하면 컨텍스트가 구체적인 전략과 독립적이므로 컨텍스트의 코드나 다른 전략을 변경하지 않고 새 알고리즘을 추가하거나 기존 알고리즘을 수정할 수 있다.
경로 계획 전략
내비게이션 앱에서 각 라우팅 알고리즘은 단일 buildRoute 메서드로 자체 클래스로 추출할 수 있다. 메서드는 출발지와 대상을 수락하고 경로의 체크포인트 컬렉션을 반환한다.
동일한 인수가 주어지더라도 각 라우팅 클래스는 서로다른 경로를 만들 수 있지만 메인 네비게이터는 렌더링 하는 것이 기본 작업이기 때문에 그것이 어떤 알고리즘을 선택했는지에 대해 별로 신경 쓰지 않는다. 클래스에는 활성 라우팅 전략을 전환하는 메서드가 있으므로 사용자 인터페이스의 버튼과 같은 클래스의 클라이언트는 현재 선택된 라우팅 동작을 다른 동작으로 바꿀 수 있다.
공항에 가기 위한 다양한 전략
공항에 갈때 버스, 택시, 자전거와 같은 교통수단 전략 중 하나를 선택할 수 있다.
컨텍스트는 구체적인 전략 중 하나에 대한 참조를 유지하며 전략 인터페이스를 통해서만 이 객체와 통신한다.
전략 인터페이스는 모든 구체적인 전략에 공통적입니다. 컨텍스트가 전략을 실행하는 데 사용하는 메서드를 선언한다.
구체적 전략은 컨텍스트가 사용하는 알고리즘의 다양한 변형을 구현한다.
컨텍스트는 알고리즘을 실행해야 할 때마다 연결된 전략 개체에 대한 실행 메서드를 호출한다. 컨텍스트는 어떤 유형의 전략으로 동작하는지, 알고리즘이 어떻게 실행되는지 알 수 없다.
클라이언트가 특정 전략 개체를 만들어 컨텍스트에 전달합니다. 컨텍스트는 클라이언트가 런타임에 컨텍스트와 관련된 전략을 대체할 수 있는 세터를 제공한다.
객체 내에서 다른 유형의 알고리즘을 사용하고 런타임 중에 한 알고리즘에서 다른 알고리즘으로 전환할 수 있는 경우 전략 패턴을 사용한다.
일부 동작의 실행 방식만 다를 뿐 유사한 클래스가 많을 경우 전략을 사용하라.
이 패턴을 사용하여 클래스의 비즈니스 로직을 해당 로직의 컨텍스트에서 중요하지 않을 수 있는 알고리즘의 구현 세부 정보와 분리한다.
클래스에 동일한 알고리즘의 다른 변형 간에 전환하는 대규모 조건부 연산자가 있는 경우 패턴을 사용한다.
복잡도: ★☆☆
인기: ★★★
사용 예: Strategy 패턴은 TS 코드에서 매우 일반적이다. 사용자가 클래스를 확장하지 않고 클래스의 동작을 변경할 수 있는 방법을 제공하기 위해 다양한 프레임워크에서 자주 사용된다.
식별: 전략 패턴은 중첩된 객체가 실제 작업을 수행하게 하는 메서드와 해당 객체를 다른 객체로 바꿀 수 있는 세터로 인식될 수 있다.
index.ts
// 컨텍스트는 클라이언트의 관심 인터페이스를 정의한다.
class Context {
private strategy: Strategy;
constructor(strategy: Strategy) {
this.strategy = strategy;
}
public setStrategy(strategy: Strategy) {
this.strategy = strategy;
}
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(','));
}
}
interface Strategy {
doAlgorithm(data: string[]): string[];
}
class ConcreteStrategyA implements Strategy {
public doAlgorithm(data: string[]): string[] {
return data.sort();
}
}
class ConcreteStrategyB implements Strategy {
public doAlgorithm(data: string[]): string[] {
return data.reverse();
}
}
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();
결과
전략 패턴은 컨텍스트의 일련의 행동을 객체로 전환하고 원래의 컨텍스트 객체 안에서 상호호환이 가능하도록 한다. 이를 통해 전략 객체에 대한 참조를 보유하고 동작 실행을 위임한다.
이를 통해 클라이언트가 사용할 때 전략에 따른 행동을 취할 수 있고, 비즈니스 로직을 알고리즘 구현 세부정보와 분리할 수 있다.
리액트에서는 간단하게 함수를 props으로 받아 컴포넌트에서 실행하는 것 또한 전략 패턴이 될 수 있다고 한다.