코딩 · 디자인 패턴
전략 패턴 예제로 쉽게 이해하기
핵심 답변
전략(Strategy)은 알고리즘군을 정의하고, 각각을 공통 인터페이스를 가진 별도의 객체에 담아 서로 교체할 수 있게 하는 행동 패턴입니다. 알고리즘을 사용하는 코드인 컨텍스트는 전략에 대한 참조를 가지고, 어떤 전략인지 모른 채 호출합니다. TypeScript에서 전략은 흔히 array.sort()에 넘기는 비교 함수처럼 단순한 함수입니다.
의도와 해결하는 문제
의도: 알고리즘군을 정의하고, 각각을 캡슐화하며, 서로 교체할 수 있게 합니다. 전략은 알고리즘을 사용하는 클라이언트와 독립적으로 알고리즘이 달라질 수 있게 합니다. GoF(Gang of Four) 책에 실린 행동 패턴 중 하나이며, 거기서는 정책(Policy)이라고도 부릅니다.
결제 단계에서 배송비를 계산한다고 가정해 봅시다. 일반 배송은 정액 요금, 특급 배송은 킬로그램당 더 많이 들고, 매장 수령은 무료입니다. 첫 버전은 세 공식을 모두 하나의 메서드에 넣고 배송 유형에 대한 if/else 연쇄로 처리합니다. 제휴 택배사 같은 새 옵션이 생길 때마다 그 메서드를 또 고쳐야 하고, 공식 하나를 테스트하려면 나머지를 모두 거쳐야 합니다.
전략 패턴에서는 각 공식이 같은 시그니처를 가진 독립된 객체나 함수가 됩니다. 결제 단계는 선택된 전략을 받아 호출합니다. 택배사를 추가하려면 전략 하나를 새로 추가하면 되고, 결제 단계는 바뀌지 않습니다.
구조와 참여자
| 역할 | 책임 |
|---|---|
| 전략(Strategy) | 알고리즘의 모든 버전이 공유하는 공통 인터페이스 |
| 구체 전략(Concrete strategy) | 알고리즘의 구현 하나 |
| 컨텍스트(Context) | 전략을 가지고 알고리즘이 필요할 때 호출하며, 클라이언트가 교체하게 할 수도 있습니다 |
| 클라이언트(Client) | 구체 전략을 고르고 컨텍스트에 전달합니다 |
TypeScript 구현
TypeScript에서 함수는 값이므로 가장 단순한 전략 인터페이스는 함수 타입입니다. 컨텍스트는 Checkout 클래스입니다.
type Parcel = { weightKg: number; subtotal: number };
type ShippingStrategy = (parcel: Parcel) => number;
const standard: ShippingStrategy = (p) => (p.subtotal >= 50 ? 0 : 4.99);
const express: ShippingStrategy = (p) => 9.99 + 1.5 * p.weightKg;
const pickup: ShippingStrategy = () => 0;
class Checkout {
constructor(private shipping: ShippingStrategy) {}
setShipping(strategy: ShippingStrategy) {
this.shipping = strategy;
}
total(parcel: Parcel): number {
return Math.round((parcel.subtotal + this.shipping(parcel)) * 100) / 100;
}
}
const parcel = { weightKg: 2, subtotal: 30 };
const checkout = new Checkout(standard);
console.log(checkout.total(parcel)); // 34.99
checkout.setShipping(express);
console.log(checkout.total(parcel)); // 42.99
checkout.setShipping(pickup);
console.log(checkout.total(parcel)); // 30전략에 설정이나 여러 메서드가 필요하다면 대신 인터페이스와 클래스를 사용하세요. 이 버전은 각 전략에 UI가 표시할 수 있는 이름도 줍니다.
interface Shipping {
readonly label: string;
cost(weightKg: number): number;
}
class Courier implements Shipping {
readonly label: string;
constructor(private ratePerKg: number, company: string) {
this.label = company + " courier";
}
cost(weightKg: number) { return this.ratePerKg * weightKg; }
}
const options: Shipping[] = [new Courier(2, "North"), new Courier(3.5, "Fast")];
for (const o of options) console.log(o.label, o.cost(4));
// North courier 8
// Fast courier 14적용 전과 후
적용 전에는 알고리즘 선택이 메서드 안에 하드코딩되어 있습니다.
function shippingCost(type: string, p: Parcel): number {
if (type === "standard") return p.subtotal >= 50 ? 0 : 4.99;
else if (type === "express") return 9.99 + 1.5 * p.weightKg;
else if (type === "pickup") return 0;
throw new Error("Unknown shipping type " + type);
}
// After: a lookup table of strategies replaces the chain
const strategies: Record<string, ShippingStrategy> = { standard, express, pickup };이 코드 조각은 첫 번째 예제의 타입과 함수를 재사용합니다. 표를 쓰면 옵션 집합이 코드가 아닌 데이터가 되므로 설정에서 만들거나 플러그인으로 확장할 수 있습니다.
언제 쓰고 언제 쓰지 말아야 할까
- 같은 작업을 하는 방법이 여러 가지이고 선택이 설정, 사용자 입력, 데이터에 달려 있을 때 사용합니다.
- 알고리즘을 고르는 큰 조건문 블록을 없애고 싶을 때 사용합니다.
- 각 알고리즘을 독립적으로 테스트하고, 다른 코드가 컨텍스트를 고치지 않고 새 알고리즘을 추가하게 하고 싶을 때 사용합니다.
- 알고리즘이 하나뿐이고 바꿀 이유가 없다면 사용하지 않습니다. 인터페이스가 하나 더 생겨 아무 이득 없이 간접 계층만 늘어납니다.
- 함수 매개변수로 충분한데 클래스를 만들지 마세요. TypeScript에서 함수를 넘기는 것이 전략의 가벼운 형태입니다.
실제 사용 사례
Array.prototype.sort(compareFn)은 비교 전략을 함수로 받으므로 같은 정렬이 숫자, 날짜, 이름을 모두 정렬할 수 있습니다.- Java의
Comparator인터페이스는Collections.sort와List.sort의 전략입니다. - Node.js 인증 미들웨어인 Passport.js는 각 로그인 방식(로컬 비밀번호, OAuth 제공자)을 전략이라 부르며 애플리케이션이 이를 끼워 넣게 합니다.
- 압축과 해시 라이브러리는 호출자가 공통 인터페이스로 알고리즘을 고르게 합니다.
관련 패턴
| 패턴 | 전략 패턴과의 차이 |
|---|---|
| 상태 | 구조는 같지만, 상태는 객체가 변함에 따라 스스로 전환됩니다. 전략은 보통 클라이언트가 선택하며 교체될 때까지 유지됩니다. |
| 템플릿 메서드 | 템플릿 메서드는 상속으로 알고리즘의 일부를 달리하고 전체 단계는 고정하며, 전략은 합성으로 알고리즘 전체를 교체합니다. |
| 커맨드 | 커맨드는 나중에 실행할 요청이고, 전략은 컨텍스트가 필요할 때 호출하는 작업 수행 방식입니다. |
| 데코레이터 | 데코레이터는 객체를 바깥에서 감싸 동작을 바꾸고, 전략은 한 부분을 교체해 안쪽에서 동작을 바꿉니다. |
자주 묻는 질문
전략 패턴과 상태 패턴의 차이는 무엇인가요?
구조는 거의 같습니다. 전략은 작업을 어떻게 수행할지 정하려고 클라이언트가 고르고, 상태 객체는 컨텍스트의 동작 결과로 컨텍스트의 현재 상태를 스스로 바꿉니다.
콜백을 넘기는 것이 전략 패턴인가요?
네. 일급 함수가 있는 언어에서 비교 함수처럼 알고리즘의 가변 부분을 구현하는 콜백은 가장 단순한 형태의 전략 패턴입니다.
전략 패턴과 템플릿 메서드 패턴의 차이는 무엇인가요?
템플릿 메서드는 상속을 사용합니다. 기반 클래스가 단계를 고정하고 하위 클래스가 일부를 채웁니다. 전략은 합성을 사용합니다. 컨텍스트가 실행 중에 교체할 수 있는 객체나 함수를 가집니다.
전략 패턴은 개방-폐쇄 원칙을 따르나요?
네. 컨텍스트를 바꾸지 않고 새 전략을 추가할 수 있으므로 컨텍스트는 확장에 열려 있고 수정에 닫혀 있습니다.