전략 패턴으로 조건 분기를 객체로 바꾸기
전략 패턴은 서로 바꿔 쓸 알고리즘을 객체로 캡슐화해 실행 중에 교체하는 GoF 행동 패턴이다. 늘어나는 조건 분기를 걷어내는 구조와 자바 Comparator 예제, 상태 패턴과의 차이, 언제 쓰고 언제 과한지 판단 기준을 담았다.
전략 패턴(Strategy Pattern)은 서로 바꿔 쓸 수 있는 여러 알고리즘을 각각 독립된 객체로 캡슐화해, 실행 중에 원하는 것으로 갈아 끼우게 하는 행동(behavioral) 디자인 패턴이다. 결제 수단, 할인 정책, 정렬 기준처럼 같은 자리에서 동작만 달라지는 코드를 if-else나 switch 분기 대신 객체 교체로 다룰 때 쓴다. 1994년 나온 GoF(Gang of Four)의 책 『Design Patterns』가 정리한 23개 패턴 중 하나로, 핵심 의도는 알고리즘 집단을 정의하고 각각을 캡슐화해 서로 교체 가능하게 만드는 것이다.
이 글은 특정 언어나 프레임워크가 아니라 객체지향 언어 전반에서 통하는 전략 패턴의 구조와 판단 기준을 다룬다. 예제 코드는 자바로 적지만 파이썬·코틀린·스위프트에도 그대로 옮겨진다. 대신 함수형 문법으로 더 가볍게 푸는 경우와, 상태 패턴처럼 헷갈리는 사촌 패턴과의 경계까지 짚는다.
늘어나는 조건 분기가 문제다
결제 모듈을 예로 들어 보자. 처음엔 카드 결제 하나였다가 계좌이체가 붙고, 간편결제가 추가되고, 포인트 결제까지 들어온다. 결제를 처리하는 메서드 안에는 if (type == CARD) ... else if (type == BANK) ...가 계속 자라난다. 결제 수단이 하나 늘 때마다 같은 메서드를 열어 분기를 덧붙여야 하고, 테스트도 그 비대해진 메서드를 통째로 다시 돌려야 한다. 한 곳을 고치려고 여러 군데를 헤집는 이른바 산탄총 수술(shotgun surgery)의 전형이다.
이건 개방-폐쇄 원칙(OCP, 기능 확장에는 열려 있고 기존 코드 수정에는 닫혀 있어야 한다는 설계 원칙)을 정면으로 어긴다. 전략 패턴은 이 분기 덩어리를 결제 방식이라는 공통 인터페이스 뒤로 밀어 넣는다. 새 결제 수단은 새 클래스 하나로 추가되고, 기존 코드는 건드리지 않는다. 조건문으로 고르던 것을 객체 교체로 바꾸는 셈이다.
구조: 세 조각으로 나눈다
전략 패턴은 역할이 셋으로 갈린다. 이름은 GoF 책의 용어를 따른다.
- 전략 인터페이스(Strategy): 갈아 끼울 동작의 공통 시그니처만 선언한다. 예를 들면
apply(price)하나. - 구체 전략(ConcreteStrategy): 인터페이스를 구현한 실제 알고리즘이다. 정률 할인, 정액 할인처럼 방식마다 클래스를 하나씩 둔다.
- 컨텍스트(Context): 전략을 필드로 들고 있다가 호출을 위임한다. 자기가 든 게 어떤 구체 전략인지 모른 채 인터페이스로만 부른다.
여기서 핵심은 컨텍스트가 전략을 상속하지 않고 가지고 있다는 점이다. GoF가 강조한 상속보다 합성(composition)의 대표 사례다. 상속으로 풀면 방식 조합마다 서브클래스가 폭발하지만, 합성이면 전략 객체만 바꿔 끼우면 된다.
코드로 보는 전략 패턴
할인 정책을 전략으로 뽑아 보자. 주문 객체는 할인을 어떻게 계산하는지 직접 알지 않고, 주입받은 정책 객체에 맡긴다.
interface DiscountPolicy {
int apply(int price);
}
class NoDiscount implements DiscountPolicy {
public int apply(int price) { return price; }
}
class RateDiscount implements DiscountPolicy {
private final double rate; // 0.1 = 10% 할인
RateDiscount(double rate) { this.rate = rate; }
public int apply(int price) {
return (int) (price * (1 - rate));
}
}
class Order {
private DiscountPolicy policy;
Order(DiscountPolicy policy) { this.policy = policy; }
void changePolicy(DiscountPolicy policy) { this.policy = policy; }
int finalPrice(int price) { return policy.apply(price); }
}새 할인 방식이 생기면 DiscountPolicy를 구현한 클래스를 하나 추가하고, 주문을 만들 때 골라 넣으면 끝이다. Order는 다시 컴파일할 필요도 없다. 런타임에 changePolicy로 갈아 끼울 수 있어서, 이벤트 기간에만 정률 할인을 적용했다가 평소엔 NoDiscount로 돌리는 전환도 자연스럽다.
표준 라이브러리에도 이미 이 패턴이 박혀 있다. 대표가 자바의 java.util.Comparator다. 오라클의 자바 API 문서에 따르면 Comparator는 객체의 정렬 순서를 정하는 비교 함수이며, Collections.sort나 List.sort에 넘겨 정렬 기준을 제어한다. 정렬 알고리즘 자체는 그대로 두고 무엇을 기준으로 비교할지라는 전략만 바꿔 끼우는 것이라, 전략 패턴의 교과서적 사례다. 정렬 알고리즘이 내부에서 어떻게 도는지는 버블·선택·삽입 정렬을 비교한 글에서 따로 다뤘다.
List<Item> items = new ArrayList<>();
// 가격 오름차순 전략을 람다로 주입
items.sort(Comparator.comparingInt(Item::price));
// 이름 기준으로 갈아 끼우기
items.sort(Comparator.comparing(Item::name));Comparator는 추상 메서드가 하나뿐인 함수형 인터페이스라 람다로 축약된다. 구체 전략을 클래스로 따로 만들 필요 없이, 비교 규칙을 그 자리에서 함수로 넘기는 것이다.

전략 패턴은 언제 써야 하나
세 조건이 겹칠 때 값어치가 있다. 같은 목적을 이루는 알고리즘이 여럿이고(보통 셋 이상), 앞으로 더 늘 게 분명하며, 실행 중에 어떤 걸 쓸지 바뀔 수 있을 때다. 결제 수단, 요금 정책, 압축 방식, 정렬 기준, 인증 방식이 전형적이다.
반대로 과하게 쓰기도 쉽다. 분기가 두 개뿐이고 앞으로 바뀔 일도 없다면 그냥 if-else가 낫다. 전략 패턴은 클래스 수를 늘리고 호출 흐름을 한 단계 더 감추기 때문에, 실체가 단순한 코드에 얹으면 읽기만 어려워진다. 지금 필요하지도 않은 유연성을 미리 깔지 말라는 YAGNI 원칙이 여기서 제동을 건다.
언어에 따라 형태도 달라진다. 파이썬, 자바스크립트, 코틀린처럼 함수를 값으로 다루는 언어에서는 인터페이스와 구현 클래스를 만드는 대신 함수 자체를 넘기는 게 훨씬 가볍다. 전략 패턴의 본질은 동작을 값처럼 주고받는 것이고, 클래스는 그걸 표현하는 한 가지 수단일 뿐이다. 위의 자바 Comparator 람다가 딱 그 중간 형태다.
상태 패턴과 무엇이 다른가
구조가 거의 같아서 자주 헷갈린다. 둘 다 컨텍스트가 인터페이스 하나를 들고 구현체에 위임한다. 갈리는 지점은 누가, 왜 구현체를 바꾸느냐다. 전략 패턴은 클라이언트(바깥)가 목적에 맞는 알고리즘을 골라 넣고 한번 정해지면 대개 그대로 간다. 상태 패턴은 객체가 스스로 자기 상태를 다음 상태로 넘기며, 전환 규칙이 내부에 있다.
| 구분 | 전략 패턴 | 상태 패턴 |
|---|---|---|
| 바꾸는 목적 | 알고리즘 선택 | 상태 전이 |
| 교체 주체 | 바깥 클라이언트 | 객체 자신 |
| 구현체끼리 관계 | 서로 모름, 대등 | 다음 상태를 알고 넘김 |
| 전환 빈도 | 보통 한 번 주입 | 동작 중 수시로 |
판단이 애매하면 이렇게 물어보면 된다. 구현체를 고르는 조건이 이 요청은 무엇을 원하나이면 전략이고, 지금 객체가 어떤 단계에 있나이면 상태에 가깝다. 결제 수단 선택은 전략, 주문의 접수·배송·완료 흐름은 상태다.
도입할 때 흔히 겪는 문제
전략을 고르는 로직을 어디에 둘지가 첫 고민이다. 컨텍스트 안에서 if로 전략을 고르면 결국 분기를 한 겹 옮긴 것뿐이라 의미가 없다. 보통은 팩토리나 의존성 주입(DI) 컨테이너가 키에 맞는 전략을 찾아 주입하게 만들어, 선택 로직 자체를 한곳에 모은다. 스프링 같은 프레임워크는 같은 인터페이스를 구현한 빈들을 맵으로 주입받아 키로 꺼내 쓰는 방식이 흔하다.
전략끼리 상태를 공유해야 할 때도 조심한다. 전략 객체를 하나만 만들어 재사용하면서 내부에 가변 필드를 두면, 동시 요청이 그 필드를 덮어써 버그가 난다. 전략은 되도록 상태 없이(stateless) 짜고, 매 호출에 필요한 값은 인자로 넘기는 편이 안전하다. 위 RateDiscount가 rate를 final로 둔 것도 같은 맥락이다.
마지막으로, 전략이 단 두 종류고 늘어날 계획이 없다면 패턴을 걷어내는 것도 리팩터링이다. 설계는 지금 코드가 감당할 변화량에 맞추는 일이지, 패턴 개수를 늘리는 대회가 아니다.





