본문 바로가기
TYLER SONGBlog
← Blog index
CS학습5 min read

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. 왜 사용하는가

결제 수단별 수수료 계산을 조건문으로 작성한 코드를 보자.

typescript
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. 동작 원리

  1. 전략 인터페이스를 정의한다. (예: FeePolicy.calculate(amount))
  2. 각 알고리즘을 구체 전략으로 구현한다.
  3. 컨텍스트는 전략 참조를 필드로 가지며, 작업을 전략에 위임한다.
  4. 클라이언트(또는 설정/팩토리)가 어떤 전략을 주입할지 결정한다. 전략은 생성 시점에 주입하거나 세터로 런타임에 교체할 수 있다.
text
Client ──선택/주입──▶ Context ──위임──▶ «interface» Strategy
                                             ▲        ▲
                                       StrategyA   StrategyB

상속으로 알고리즘을 바꾸는 방식(템플릿 메서드 패턴 등)은 컴파일 시점에 구조가 고정된다. 반면 전략 패턴은 합성(composition) 을 쓰므로 실행 중에도 교체할 수 있다.

4. 코드 예제

TypeScript: 결제 수수료 전략

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)); // 10000

TypeScript: 함수형 전략

전략 인터페이스에 메서드가 하나뿐이라면 함수 타입으로 충분하다. 일급 함수(first-class function)를 지원하는 언어에서는 클래스 없이 구현하는 편이 간결하다.

typescript
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)); // 10300

Python: 정렬 전략 교체

python
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)의 위임 수준이다.
  • 메서드가 하나뿐인 전략은 함수로 표현하는 편이 간결하다.
  • 변경 가능성이 낮은 곳에 적용하면 오히려 복잡도만 늘어나므로, 분기가 늘어나는 지점에 선별적으로 쓴다.