Strategy 패턴: 알고리즘을 호출부에서 떼어내 갈아 끼우는 법
Strategy 패턴으로 알고리즘 선택 책임을 조건문에서 구성 단계로 옮기는 설계와, 결제 수수료·AB 테스트 등 실무 적용 사례, 운영 트레이드오프를 정리한다.
2026-08-13 · 최초 발행 2025-10-14
결제 수수료율이 카드사마다, 국가마다 다르다면 그 규칙을 어디에 둘 것인가가 문제가 된다. if-else로 하나씩 처리하면 규칙이 하나 늘 때마다 그 조건문 전체를 다시 읽고 고쳐야 한다. Strategy 패턴은 이 알고리즘 계열을 공통 인터페이스로 뽑아내고, 콘텍스트(Context)가 런타임에 구체 전략을 주입받아 실행하게 만든다. 알고리즘을 고르는 책임이 호출부의 조건문에서 구성(주입) 단계로 옮겨가는 것이고, 이는 OCP(Open-Closed Principle)를 지키는 방식이기도 하다. 런타임 전략 교체, 기능 플래그·AB 테스트, 리전별 정책 차등에 두루 적용된다.
인터페이스, 구현체, 그리고 콘텍스트
Strategy 인터페이스는 알고리즘 계약을 정의한다 — 입력·출력 타입, 예외 계약, 성능 특성을 명시해두면 테스트 더블로 손쉽게 대체할 수 있다. ConcreteStrategy는 이 계약의 구체 구현이고, 성능·정확도·부작용 특성을 기록해두고 메트릭 로깅을 심어두는 것이 좋다. Context는 비즈니스 흐름을 주도하며 전략을 호출하고 결과를 후처리하는 쪽인데, 상태는 최소로 갖고 전략은 가능한 한 무상태로 두는 편이 낫다. 전략을 어떻게 골라 넣을지는 DI 컨테이너, 팩토리, 매핑 테이블 중에서 정하고, 피처 플래그·환경 변수·요청 메타데이터 기반으로 조건을 평가한다.
Context가 Strategy를 주입받는 구조
조건문, Strategy, Template Method 비교
| 접근 | 성능 | 확장성 | 일관성 | 안정성 | 운영 편의 |
|---|---|---|---|---|---|
| 다중 if-else/switch | 낮음(분기 누적 시) | 낮음(개방 폐쇄 위반) | 낮음(산재한 분기) | 중간(단순 구조) | 낮음(수정 영향 범위 큼) |
| Strategy 패턴 | 중간~높음(적절한 캐시/주입) | 높음(신규 전략 추가 용이) | 높음(계약 기반) | 높음(테스트 격리 용이) | 높음(DI/피처 플래그 연계) |
| Template Method | 중간 | 중간(상속 제약) | 중간 | 중간 | 중간(훅 메서드 관리 필요) |
정책이 자주 바뀌는 곳에서
- 결제 수수료/환율 정책 — 입력: 금액, 카드사/국가 코드, 시각 / 처리: 리전별·제휴사별 수수료 전략과 환율 전략 조합 / 출력: 최종 청구 금액, 수수료 내역
- 추천/랭킹 알고리즘 AB 테스트 — 입력: 사용자 세그먼트, 트래픽 버킷 / 처리: 버킷→전략 매핑, 성능 로그 집계 / 출력: 노출 리스트, 지표 이벤트
- 데이터 압축/암호화 파이프라인 — 입력: 페이로드, 민감도, 크기 / 처리: LZ4/ZSTD 선택, AES/GCM 여부 결정 / 출력: 압축·암호화된 바이트 스트림
- 정렬·검색 정책 — 입력: 정렬 키, 데이터 크기, 메모리 제한 / 처리: 퀵/머지/튜닝된 외부 정렬 전략 선택 / 출력: 성능·자원 제약을 만족하는 정렬 결과
가격 정책으로 보는 Strategy 구현
Java 17 이상, Gradle 또는 Maven 환경을 전제로 한다.
// Strategy 계약
public interface PricingStrategy {
long apply(long rawPrice, Customer ctx);
}
// 구체 전략들
public class RateDiscountStrategy implements PricingStrategy {
private final int rate; // 퍼센트
public RateDiscountStrategy(int rate) { this.rate = rate; }
public long apply(long rawPrice, Customer ctx) {
if (rate < 0 || rate > 100) throw new IllegalArgumentException("invalid rate");
return rawPrice - (rawPrice * rate / 100);
}
}
public class TieredDiscountStrategy implements PricingStrategy {
public long apply(long rawPrice, Customer ctx) {
int rate = switch (ctx.tier()) { case "GOLD" -> 20; case "SILVER" -> 10; default -> 0; };
return rawPrice - (rawPrice * rate / 100);
}
}
// 콘텍스트
public class PriceCalculator {
private PricingStrategy strategy;
public PriceCalculator(PricingStrategy strategy) { this.strategy = strategy; }
public void setStrategy(PricingStrategy strategy) { this.strategy = strategy; }
public long calculate(long rawPrice, Customer ctx) { return strategy.apply(rawPrice, ctx); }
}
public record Customer(String id, String tier) {}
// 전략 선택기 (간단 팩토리/레지스트리)
import java.util.Map;
public class StrategyRegistry {
private final Map<String, PricingStrategy> map;
public StrategyRegistry(Map<String, PricingStrategy> map) { this.map = map; }
public PricingStrategy select(String key) {
PricingStrategy s = map.get(key);
if (s == null) throw new IllegalArgumentException("unknown strategy: " + key);
return s;
}
}
// 실행
import java.util.Map;
public class Main {
public static void main(String[] args) {
StrategyRegistry registry = new StrategyRegistry(Map.of(
"rate10", new RateDiscountStrategy(10),
"tiered", new TieredDiscountStrategy()
));
Customer gold = new Customer("u1", "GOLD");
PriceCalculator calc = new PriceCalculator(registry.select("rate10"));
long p1 = calc.calculate(100_00, gold); // 100원 단위
calc.setStrategy(registry.select("tiered"));
long p2 = calc.calculate(100_00, gold);
System.out.printf("rate10=%d, tiered=%d%n", p1, p2);
}
}
컴파일·실행은 javac *.java && java Main이다. 전략을 추가할 때는 PricingStrategy 구현체를 만들고 레지스트리에 등록한 뒤 구성만 바꾸면 즉시 반영된다. 선택이 실패하면 폴백 전략을 적용하거나 요청을 차단하고, 입력 검증을 먼저 거친 뒤 메트릭·로그로 전략별 성능을 모니터링하는 것을 권장한다.
운영에서 신경 쓸 것들
전략은 무상태로 설계해 재사용 인스턴스를 싱글턴화하고 스레드 안전성을 확보하는 것이 기본이다. DI 컨테이너를 쓰면서 전략을 자동 탐지(@Component)하고 Qualifier로 이름 기반 주입을 하면 관리가 쉬워지고, 피처 플래그·AB 테스트 플랫폼과 엮어 전략 키 매핑을 설정이나 DB로 외부화하면 무중단으로 전략을 교체할 수 있다.
다만 전략이 늘어날수록 클래스 수와 구조 복잡도가 올라가므로 작은 문제에는 과설계가 될 수 있고, 전략을 지나치게 잘게 쪼개면 오히려 중복 코드가 늘어 공통 전처리·후처리를 데코레이터나 파이프라인으로 따로 빼야 할 수도 있다. 런타임에 전략을 고르는 비용도 존재하므로 핫패스에는 캐시나 프리바인딩, JIT 친화적 구현을 고려해야 한다.
원본 사례가 제시하는 정량 수치로는 조건 분기 수 4070% 감소, 신규 정책 리드타임 3050% 단축(배포 없이 구성 교체 시), 회귀 결함률 20~35% 감소(전략 단위 테스트 격리 기준)가 있다. 다만 이 수치는 유사 규모 조직 도입 사례와 보수적 가정을 혼합한 추정치이며, 실제 효과는 도메인과 엔지니어링 성숙도에 따라 달라질 수 있다는 단서가 붙어 있다. 그 외에 변경 영향 범위가 국소화되어 가독성·유지보수성이 오르고, AB 테스트나 그레이드 릴리스와 엮인 실험 문화가 촉진되며, 표준 인터페이스를 통해 팀 간 계약이 명확해지는 정성적 효과도 함께 보고된다. 소규모 영역에 먼저 시범 도입하고, 성능·운영 지표를 보면서 적용 범위를 넓혀가는 방식이 무난하다.