Strategy 패턴: 알고리즘을 호출부에서 떼어내 갈아 끼우는 법

Strategy 패턴으로 알고리즘 선택 책임을 조건문에서 구성 단계로 옮기는 설계와, 결제 수수료·AB 테스트 등 실무 적용 사례, 운영 트레이드오프를 정리한다.

2026-08-13 · 최초 발행 2025-10-14

결제 수수료율이 카드사마다, 국가마다 다르다면 그 규칙을 어디에 둘 것인가가 문제가 된다. if-else로 하나씩 처리하면 규칙이 하나 늘 때마다 그 조건문 전체를 다시 읽고 고쳐야 한다. Strategy 패턴은 이 알고리즘 계열을 공통 인터페이스로 뽑아내고, 콘텍스트(Context)가 런타임에 구체 전략을 주입받아 실행하게 만든다. 알고리즘을 고르는 책임이 호출부의 조건문에서 구성(주입) 단계로 옮겨가는 것이고, 이는 OCP(Open-Closed Principle)를 지키는 방식이기도 하다. 런타임 전략 교체, 기능 플래그·AB 테스트, 리전별 정책 차등에 두루 적용된다.

인터페이스, 구현체, 그리고 콘텍스트

Strategy 인터페이스는 알고리즘 계약을 정의한다 — 입력·출력 타입, 예외 계약, 성능 특성을 명시해두면 테스트 더블로 손쉽게 대체할 수 있다. ConcreteStrategy는 이 계약의 구체 구현이고, 성능·정확도·부작용 특성을 기록해두고 메트릭 로깅을 심어두는 것이 좋다. Context는 비즈니스 흐름을 주도하며 전략을 호출하고 결과를 후처리하는 쪽인데, 상태는 최소로 갖고 전략은 가능한 한 무상태로 두는 편이 낫다. 전략을 어떻게 골라 넣을지는 DI 컨테이너, 팩토리, 매핑 테이블 중에서 정하고, 피처 플래그·환경 변수·요청 메타데이터 기반으로 조건을 평가한다.

Context가 Strategy를 주입받는 구조

Context-Strategy strategy+setStrategy(Strategy)+execute(input) : output«interface»Strategy+apply(input) : outputConcreteStrategyA+apply(input) : outputConcreteStrategyB+apply(input) : output

조건문, 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 테스트나 그레이드 릴리스와 엮인 실험 문화가 촉진되며, 표준 인터페이스를 통해 팀 간 계약이 명확해지는 정성적 효과도 함께 보고된다. 소규모 영역에 먼저 시범 도입하고, 성능·운영 지표를 보면서 적용 범위를 넓혀가는 방식이 무난하다.

Strategy 패턴디자인 패턴OCP알고리즘 교체의존성 주입