Strategy: 알고리즘을 객체로 분리해 런타임에 교체하는 패턴
전략 패턴(Strategy Pattern)은 동일한 목적을 가진 여러 알고리즘을 공통 인터페이스 뒤로 캡슐화하고, 실행 시점에 교체할 수 있게 하는 행동 디자인 패턴이다. 조건문(if/else, switch)이 늘어나는 구조를 다형성(polymorphism)으로 대체해 개방-폐쇄 원칙(OCP)을 지키도록 돕는다. 이 글에서는 원리와 TypeScript·Py
By 송민성
1. 개념
전략 패턴(Strategy Pattern)은 같은 문제를 푸는 알고리즘 군(family)을 각각 별도의 클래스(또는 함수)로 분리하고, 공통 인터페이스로 묶어 서로 바꿔 쓸 수 있게 만드는 패턴이다. GoF(Gang of Four)의 디자인 패턴 분류에서는 행동 패턴(Behavioral Pattern)에 속한다.
구성 요소는 세 가지다.
| 역할 | 설명 |
|---|---|
| 전략(Strategy) | 알고리즘이 따라야 하는 공통 인터페이스 |
| 구체 전략(Concrete Strategy) | 인터페이스를 구현한 개별 알고리즘 |
| 컨텍스트(Context) | 전략을 보유하고, 필요할 때 위임(delegation)하여 호출하는 객체 |
핵심은 컨텍스트가 "어떤 알고리즘인지"를 모르고 "인터페이스만" 안다는 점이다.
2. 왜 사용하는가
결제 수단별 수수료 계산을 조건문으로 작성한 코드를 보자.
function calcFee(method: string, amount: number): number {
if (method === "card") return amount * 0.03;
if (method === "bank") return 500;
if (method === "point") return 0;
throw new Error("unknown method");
}결제 수단이 추가될 때마다 이 함수를 수정해야 한다. 이는 다음 문제를 만든다.
- 개방-폐쇄 원칙(OCP, Open-Closed Principle) 위반: 확장을 위해 기존 코드를 수정해야 한다.
- 테스트 어려움: 분기가 한 함수에 몰려 있어 각 알고리즘을 독립적으로 검증하기 어렵다.
- 책임 혼재: "어떤 알고리즘을 쓸지 고르는 일"과 "알고리즘 자체"가 한 곳에 섞인다.
전략 패턴은 알고리즘을 분리하여 새 전략 추가가 기존 코드 수정 없이 클래스(또는 함수) 하나를 추가하는 일로 끝나게 한다.
3. 동작 원리
- 전략 인터페이스를 정의한다. (예:
FeePolicy.calculate(amount)) - 각 알고리즘을 구체 전략으로 구현한다.
- 컨텍스트는 전략 참조를 필드로 가지며, 작업을 전략에 위임한다.
- 클라이언트(또는 설정/팩토리)가 어떤 전략을 주입할지 결정한다. 전략은 생성 시점에 주입하거나 세터로 런타임에 교체할 수 있다.
Client ──선택/주입──▶ Context ──위임──▶ «interface» Strategy
▲ ▲
StrategyA StrategyB상속으로 알고리즘을 바꾸는 방식(템플릿 메서드 패턴 등)은 컴파일 시점에 구조가 고정된다. 반면 전략 패턴은 합성(composition) 을 쓰므로 실행 중에도 교체할 수 있다.
4. 코드 예제
TypeScript: 결제 수수료 전략
interface FeeStrategy {
calculate(amount: number): number;
}
class CardFee implements FeeStrategy {
calculate(amount: number): number {
return Math.round(amount * 0.03);
}
}
class BankTransferFee implements FeeStrategy {
calculate(_amount: number): number {
return 500;
}
}
class PointFee implements FeeStrategy {
calculate(_amount: number): number {
return 0;
}
}
class Checkout {
constructor(private strategy: FeeStrategy) {}
setStrategy(strategy: FeeStrategy): void {
this.strategy = strategy;
}
total(amount: number): number {
return amount + this.strategy.calculate(amount);
}
}
const checkout = new Checkout(new CardFee());
console.log(checkout.total(10000)); // 10300
checkout.setStrategy(new BankTransferFee());
console.log(checkout.total(10000)); // 10500
checkout.setStrategy(new PointFee());
console.log(checkout.total(10000)); // 10000TypeScript: 함수형 전략
전략 인터페이스에 메서드가 하나뿐이라면 함수 타입으로 충분하다. 일급 함수(first-class function)를 지원하는 언어에서는 클래스 없이 구현하는 편이 간결하다.
type FeeFn = (amount: number) => number;
const strategies: Record<string, FeeFn> = {
card: (a) => Math.round(a * 0.03),
bank: () => 500,
point: () => 0,
};
function total(method: keyof typeof strategies, amount: number): number {
return amount + strategies[method](amount);
}
console.log(total("card", 10000)); // 10300Python: 정렬 전략 교체
from typing import Callable, Protocol
class SortStrategy(Protocol):
def __call__(self, items: list[int]) -> list[int]: ...
def ascending(items: list[int]) -> list[int]:
return sorted(items)
def descending(items: list[int]) -> list[int]:
return sorted(items, reverse=True)
class Sorter:
def __init__(self, strategy: Callable[[list[int]], list[int]]) -> None:
self._strategy = strategy
def set_strategy(self, strategy: Callable[[list[int]], list[int]]) -> None:
self._strategy = strategy
def sort(self, items: list[int]) -> list[int]:
return self._strategy(items)
sorter = Sorter(ascending)
print(sorter.sort([3, 1, 2])) # [1, 2, 3]
sorter.set_strategy(descending)
print(sorter.sort([3, 1, 2])) # [3, 2, 1]5. 시간 복잡도 또는 성능 특성
전략 패턴 자체는 알고리즘의 시간 복잡도를 바꾸지 않는다. 전체 복잡도는 선택된 구체 전략의 복잡도와 같다.
- 위임 오버헤드: 인터페이스를 통한 메서드 호출(동적 디스패치, dynamic dispatch) 한 번이 추가된다. 호출당 비용은 상수 시간 O(1)이며, 일반적인 애플리케이션 로직에서는 무시할 수 있는 수준이다.
- 객체 생성 비용: 상태가 없는 전략은 하나만 만들어 재사용(싱글턴 또는 모듈 상수)하면 호출마다 객체를 만들 필요가 없다.
- 전략 선택 비용: 키로 전략을 찾는 맵(map) 조회는 평균 O(1)이다. 조건문 분기는 분기 수에 비례하지만 보통 그 수가 작아 성능 차이는 사실상 없다.
따라서 도입 이유는 성능이 아니라 구조적 유연성과 테스트 용이성이다.
6. 실무 사용 사례
- 인증 전략: Passport.js는 로컬, OAuth, JWT 등 인증 방식을 전략 객체로 등록해 쓴다.
- 정렬 비교 함수:
Array.prototype.sort(compareFn), Python의sorted(key=...)처럼 비교·키 추출 로직을 인자로 주입하는 방식이 전략 패턴의 함수형 형태다. - 결제·배송·할인 정책: 수수료, 쿠폰, 배송비 계산을 정책 객체로 분리한다.
- 캐시 교체 정책, 재시도(retry) 정책: 고정 간격, 지수 백오프(exponential backoff) 등을 교체 가능하게 구성한다.
- 의존성 주입(DI) 프레임워크: Spring, NestJS 등에서 인터페이스에 구현체를 주입하는 구조가 전략 패턴과 맞닿아 있다.
7. 주의할 점
- 과도한 적용: 분기가 두세 개이고 앞으로 늘어날 가능성이 낮다면 단순한
if가 더 읽기 쉽다. 변경 가능성이 실제로 보일 때 도입한다. - 클래스 폭증: 전략마다 클래스를 만들면 파일이 급증한다. 언어가 일급 함수를 지원하면 함수로 대체한다.
- 전략 선택 로직의 위치: 어디선가는 결국 "어떤 전략을 쓸지" 고르는 코드가 필요하다. 이 선택을 팩토리나 설정 맵으로 한곳에 모으지 않으면 조건문이 클라이언트로 옮겨갈 뿐이다.
- 인터페이스 설계: 전략마다 필요한 입력이 다르면 인터페이스가 비대해진다. 필요한 데이터를 컨텍스트가 인자로 넘기는 방식과 컨텍스트 참조를 전달하는 방식 사이에서 결합도를 따져 선택한다.
- 상태 패턴(State Pattern)과의 혼동: 구조는 비슷하지만 의도가 다르다. 전략은 클라이언트가 알고리즘을 선택하고, 상태는 객체의 내부 상태 전이에 따라 동작이 바뀐다.
8. 핵심 정리
- 전략 패턴은 교체 가능한 알고리즘을 공통 인터페이스 뒤로 캡슐화하고, 컨텍스트가 이를 합성(composition)으로 보유해 위임한다.
- 조건문 분기를 다형성으로 대체하여 OCP를 지키고, 각 알고리즘을 독립적으로 테스트할 수 있게 한다.
- 알고리즘의 시간 복잡도는 바뀌지 않으며, 추가 비용은 호출당 O(1)의 위임 수준이다.
- 메서드가 하나뿐인 전략은 함수로 표현하는 편이 간결하다.
- 변경 가능성이 낮은 곳에 적용하면 오히려 복잡도만 늘어나므로, 분기가 늘어나는 지점에 선별적으로 쓴다.