서론: 비슷해 보이지만 본질이 다른 두 디자인 패턴
소프트웨어 설계에서 상태 패턴(State Pattern)과 전략 패턴(Strategy Pattern)은 종종 혼동된다. 두 패턴 모두 인터페이스를 기반으로 다형성을 활용하며, UML 클래스 다이어그램 상으로는 거의 동일한 구조를 가진다. 그러나 이 둘은 추구하는 목적과 적용되는 시나리오가 근본적으로 다르다. 어떤 패턴을 선택하느냐는 단순히 코딩 스타일의 차이가 아니라, 도메인을 바라보는 관점의 차이를 반영한다.
전략 패턴: 알고리즘의 교체
전략 패턴은 다양한 알고리즘을 캡슐화하고, 필요에 따라 서로 교체할 수 있도록 하는 패턴이다. 주로 조건문(if-else 또는 switch)으로 인해 복잡해지는 로직을 정리할 때 사용된다.
핵심 개념
- 목적: 유사한 기능을 수행하지만 방식이 다른 알고리즘들을 하나의 인터페이스로 통합하고, 실행 시점에 선택 가능하게 함.
- 장점: 새로운 전략 추가 시 기존 코드 수정 없이 확장 가능 (OCP 원칙 준수).
- 사용 시기: 동일한 문제를 해결하기 위한 여러 접근 방법이 존재하고, 클라이언트가 직접 전략을 선택하거나 주입해야 할 때.
코드 예제
public interface PaymentMethod {
void pay(double amount);
}
public class CreditCardPayment implements PaymentMethod {
public void pay(double amount) {
System.out.println("신용카드로 " + amount + "원 결제");
}
}
public class KakaoPayPayment implements PaymentMethod {
public void pay(double amount) {
System.out.println("카카오페이로 " + amount + "원 결제");
}
}
public class PaymentProcessor {
private PaymentMethod method;
public void setPaymentMethod(PaymentMethod method) {
this.method = method;
}
public void execute(double amount) {
if (method == null) throw new IllegalStateException("결제 수단 미지정");
method.pay(amount);
}
}
클라이언트는 필요에 따라 CreditCardPayment나 KakaoPayPayment를 선택하여 PaymentProcessor에 주입할 수 있다. 이 선택은 외부 결정이며, 전략 간에는 자연스러운 전환 흐름이 없다.
상태 패턴: 상태 기반 행동 변화
상태 패턴은 객체의 내부 상태가 변경됨에 따라 그 행동이 달라져야 할 때 사용된다. 객체가 특정 상태에 있을 때는 특정한 방식으로 동작해야 하며, 상태 전이는 객체 스스로 또는 외부 트리거에 의해 발생한다.
핵심 개념
- 목적: 상태 변화에 따른 행동 변화를 명시적으로 모델링하고, 조건문을 제거함.
- 특징: 상태 간 전이(transition)가 핵심이며, 전이는 일반적으로 객체 내부 로직이나 메서드 호출 결과로 발생한다.
- 사용 시기: 객체의 행동이 현재 상태에 강하게 의존하며, 상태 전이 규칙이 복잡할 때.
개선된 코드 예제
public interface DeviceState {
void powerPress(DeviceContext context);
void volumeUp(DeviceContext context);
}
public class OnState implements DeviceState {
public void powerPress(DeviceContext context) {
System.out.println("전원 끔");
context.setState(new OffState());
}
public void volumeUp(DeviceContext context) {
System.out.println("볼륨 증가");
}
}
public class OffState implements DeviceState {
public void powerPress(DeviceContext context) {
System.out.println("전원 켬");
context.setState(new OnState());
}
public void volumeUp(DeviceContext context) {
System.out.println("전원 꺼짐 - 무시");
}
}
public class DeviceContext {
private DeviceState state = new OffState();
private void setState(DeviceState state) {
this.state = state;
}
public void pressPower() {
state.powerPress(this);
}
public void increaseVolume() {
state.volumeUp(this);
}
}
여기서 중요한 점은 상태 전이가 DeviceState 구현체 내부에서 context.setState(...)를 통해 제어된다는 것이다. 클라이언트는 단순히 pressPower()를 호출할 뿐이며, 어떤 상태로 전이될지는 상태 객체 자체가 결정한다.
핵심 차이점 요약
| 항목 | 전략 패턴 | 상태 패턴 |
|---|---|---|
| 의도 | 알고리즘 교체 | 상태 기반 행동 변화 |
| 전이 제어 | 클라이언트가 전략 선택 | 내부 상태 또는 컨텍스트가 제어 |
| 의존성 | 전략은 컨텍스트를 참조하지 않음 | 상태는 컨텍스트를 참조 가능 (전이 위해) |
| 확장성 | 새 전략 추가 쉬움 | 새 상태 및 전이 규칙 추가 필요 |
| 예시 | 정렬 알고리즘, 결제 수단 | 자동차 변속기, 문서 편집기 상태 |
어떻게 선택해야 할까?
패턴 선택은 다음과 같은 질문을 통해 결정할 수 있다:
- 행동의 변화가 사용자의 선택인지, 아니면 시스템의 자연스러운 흐름인가?
→ 사용자가 선택하면 전략, 시스템 상태 변화면 상태 패턴. - 객체가 스스로 상태를 전이시키는가?
→ 그렇다면 상태 패턴이 적합하다. - 각 구현체 간에 평등한 관계인가, 계층/흐름이 있는가?
→ 평등하면 전략, 순차적 흐름 있으면 상태.
예를 들어, "미국에서는 영어를, 한국에서는 한국어를 사용한다"라는 요구사항을 생각해보자. 만약 사용자가 국가별 언어를 수동으로 선택한다면 LanguageStrategy가 적절하다. 하지만 사용자가 국경을 넘을 때 언어가 자동으로 전환된다면, 위치를 나타내는 LocationState로 모델링하는 것이 더 자연스럽다.
마무리
전략 패턴과 상태 패턴은 구조적으로 유사하지만, 의미 체계와 적용 맥락은 다르다. 전략은 "어떻게 할 것인가"에 대한 선택이고, 상태는 "지금 어떤 상황인가"에 따른 반응이다. 올바른 패턴을 선택하면 코드의 가독성과 유지보수성이 크게 향상되며, 도메인의 본질을 더 잘 드러낼 수 있다.