조건에 따라 다른 알고리즘을 골라야 하는 코드는 흔히 if/switch 분기로 자란다. 결제 수단마다, 정렬 기준마다, 압축 방식마다 분기가 하나씩 늘어난다. 새 알고리즘이 생길 때마다 이 분기를 열어 고쳐야 한다. Strategy 는 각 알고리즘을 객체로 캡슐화해 교체 가능하게 만든다.
공통 의도
Strategy 의 의도는 세 가지다.
- 알고리즘 캡슐화 — 각 알고리즘을 같은 인터페이스 뒤에 둔다. 서로 교체 가능한 하나의 알고리즘군이 된다.
- 런타임 교체 — 컨텍스트가 어떤 전략을 쓸지 런타임에 주입받는다. 코드를 고치지 않고 동작을 바꾼다.
- 추가로 확장 — 새 알고리즘은 분기 수정이 아니라 새 전략을 더하는 것으로 들어온다. 기존 코드는 그대로 둔 채 확장한다.
GoF 원형
GoF 는 세 역할로 정의한다. Context, Strategy, ConcreteStrategy.
Context 는 전략을 참조로 들고 실행을 위임한다. Strategy 는 알고리즘의 공통 인터페이스이고, ConcreteStrategy 가 각 알고리즘을 구현한다. Context 는 어떤 ConcreteStrategy 를 받았는지 모른 채 인터페이스로만 호출한다.
flowchart LR
Context --> S["Strategy"]
S --> A["RateDiscount"]
S --> B["AmountDiscount"]
S --> C["NoDiscount"]
interface DiscountStrategy {
int apply(int price);
}
class RateDiscount implements DiscountStrategy {
public int apply(int price) { return price * 9 / 10; }
}
class Checkout {
private final DiscountStrategy discount;
Checkout(DiscountStrategy discount) { this.discount = discount; }
int total(int price) { return discount.apply(price); }
}
new Checkout(new RateDiscount()) 처럼 전략을 주입한다. 새 할인 방식이 생기면 DiscountStrategy 를 구현한 클래스를 하나 더할 뿐, Checkout 은 건드리지 않는다.
언어별 구현
Java 의 Comparator 가 대표적인 Strategy 다. list.sort(comparator) 에서 정렬 알고리즘은 고정이고, 비교 기준만 전략으로 주입된다. 새 정렬 기준은 새 Comparator 로 표현된다.
Python 은 전략을 함수로 그냥 전달한다. 일급 함수가 있으므로 별도의 Strategy 클래스가 필요 없다.
from typing import Callable
def checkout(price: int, discount: Callable[[int], int]) -> int:
return discount(price)
checkout(10000, lambda p: p * 9 // 10)
sorted(data, key=func) 도 같은 형태다. 비교 전략을 함수로 넘긴다. 클래스 계층 대신 함수 하나가 ConcreteStrategy 역할을 한다.
TypeScript 도 함수 타입으로 표현한다. (price: number) => number 같은 타입을 받으면 전략을 함수로 주입할 수 있다. 인터페이스와 클래스로도 가능하지만 함수 쪽이 더 간결하다.
State 와의 구분
Strategy 와 State 는 구조가 거의 같다. 컨텍스트가 인터페이스에 위임하는 형태다. 하지만 의도가 다르다.
- Strategy — 외부가 알고리즘을 선택한다. 전략들은 서로를 모른다.
- State — 상태가 스스로 다음 상태로 전이한다. 상태들은 전이 관계를 안다.
같은 위임 구조를 쓰지만, 누가 교체를 결정하는가가 다르다.
언어 기능이 흡수하는 경우
Strategy 의 핵심은 “교체 가능한 동작을 주입한다” 이다. 일급 함수가 있는 언어에서는 이 동작이 함수 하나로 표현되므로, 별도의 Strategy 인터페이스와 ConcreteStrategy 클래스 계층이 사라진다.
strategies = {
"rate": lambda p: p * 9 // 10,
"amount": lambda p: p - 2000,
}
discount = strategies["rate"]
클래스 세 개가 함수 세 개로 줄어든다. Builder 가 named/default 매개변수에 흡수되듯, Strategy 는 일급 함수에 흡수된다. 패턴이 사라지는 게 아니라, 그 패턴이 풀던 문제가 언어 차원에서 해결된다.
결론
Strategy 는 알고리즘군을 같은 인터페이스로 캡슐화해 런타임에 교체한다. 분기를 고치는 대신 전략을 더해 확장한다. 선택은 두 질문으로 좁혀진다.
- 조건마다 다른 알고리즘이 있고 런타임에 교체되는가. 그렇다면 분기보다 Strategy.
- 사용 언어에 일급 함수가 있는가. 그렇다면 Strategy 클래스 계층은 함수 주입으로 줄어든다.
Comparator, sorted(key=...), 함수 주입이 모두 같은 의도의 변주다. 구조가 닮은 State 와는 교체를 누가 결정하는가로 갈린다.
참고
- Observer — 행위 패턴 짝, 객체 간 통지
- Dependency Injection — DIP, IoC, DI의 위계 — 전략을 주입하는 메커니즘
- GoF — Design Patterns: Elements of Reusable Object-Oriented Software (1994)