전략, 옵저버, 커맨드 패턴
1. 행동 패턴이 필요한 이유
1-1. 생성과 구조만으로는 부족하다
객체지향 설계에는 생성(Creational), 구조(Structural), 행동(Behavioral)이라는 세 가지 패턴 분류가 있다. 대부분의 초보 설계자들은 생성과 구조 패턴에 집중하는 경향이 있다. 하지만 객체가 생성되고 관계가 설정된 후, 실제 동작 방식의 유연성을 확보하는 것이 진짜 설계의 완성이다.
행동 패턴은 객체 간의 협력과 메시지 흐름을 설계하는 데 초점을 맞춘다. 이는 단순한 로직 분리 수준을 넘어, 시간에 따라 변화하거나 전략적으로 대체 가능한 행동을 추상화하는 방식으로 시스템의 복잡성을 줄인다. 특히 사용자 인터페이스, 이벤트 처리, 명령 실행 등에서 매우 유용하게 활용된다.
행동 패턴이 없다면 모든 행동은 if-else, switch-case, 플래그, 콜백 등의 복잡한 구조로 엉켜 버린다. 결과적으로 시스템은 점점 테스트하기 어렵고, 유지보수가 힘든 코드를 양산하게 된다. 전략, 옵저버, 커맨드 패턴은 이런 복잡성을 해소하는 대표적인 행동 패턴이다.
1-2. 변화에 강한 행동 설계란?
변화가 많은 기능일수록, 변경을 외부로 분리해두는 것이 바람직하다. 예를 들어 결제 방식, UI 동작, 알림 처리, 사용자 커맨드 등은 언제든 바뀔 수 있는 기능이다. 이러한 동작을 코드 내부에 하드코딩하면, 매번 코드 수정과 테스트가 반복된다.
행동 패턴은 이 같은 변화를 객체로 분리하고, 필요 시 전략을 바꾸거나, 새로운 행동을 구독하게 하거나, 명령을 저장해 재실행하는 방식으로 설계의 유연성을 확보한다. 이러한 설계 방식은 OCP(개방-폐쇄 원칙), DIP(의존성 역전 원칙)과도 자연스럽게 연결된다.
이러한 패턴을 통해 우리는 "행동은 바뀌지만, 구조는 그대로"인 이상적인 아키텍처를 만들 수 있다. 행동 패턴은 결국 변화에 강한 시스템을 만드는 가장 효과적인 무기다.
1-3. 전략, 옵저버, 커맨드의 차별점
전략(Strategy)은 다양한 알고리즘을 교체 가능하게 만드는 패턴이다. 알고리즘의 선택을 런타임에 유연하게 전환할 수 있다. 반면 옵저버(Observer)는 객체 상태 변화에 따라 자동으로 반응하는 이벤트 중심 구조를 만들고, 커맨드(Command)는 동작 자체를 하나의 객체로 캡슐화하여 실행, 취소, 재실행 등을 가능하게 한다.
이 세 가지는 공통적으로 "행동을 객체화한다"는 목적을 가지지만, 선택 기준은 동작의 특성과 흐름 제어 방식에 있다. 전략은 선택적 분기, 옵저버는 상태 반응, 커맨드는 행동 기록 및 제어에 적합하다. 현대의 UI 프레임워크, 리액티브 시스템, 메시지 기반 시스템 등 거의 모든 복잡한 시스템 내부에는 이 세 가지 패턴 중 적어도 하나가 숨어 있다. 설계자가 이들의 차이와 용도를 정확히 이해하는 것이 중요하다.
1-4. 전략-옵저버-커맨드는 행동 설계의 3대 축이다
행동 패턴은 많지만, 전략, 옵저버, 커맨드만큼 범용성과 실전 적용 빈도가 높은 패턴은 드물다. 이 세 가지는 단독으로도 사용되지만, 조합되었을 때 더욱 강력한 구조를 만든다. 예를 들어 전략을 커맨드로 감싸고, 그 커맨드의 실행을 옵저버가 트리거하는 구조는 이벤트 기반 시스템에서 흔히 볼 수 있다. 이 패턴들은 단순히 "디자인 패턴 목록에 있는 기술"이 아니라, 소프트웨어의 행동 구조를 설계하는 철학적인 원칙이다. 상황에 맞는 행동 구조를 설계하지 않으면, 결국 객체지향의 이점은 사라지고, 함수의 나열이나 조건 분기 코드만 남는다.
2. 전략 패턴 완전정복
2-1. 전략 패턴이란 무엇인가?
전략(Strategy) 패턴은 행동(알고리즘)을 추상화하고, 실행 시점에 교체할 수 있도록 하는 설계 방식이다. 즉, 실행할 동작(전략)을 객체로 분리하고, 클라이언트는 이 전략들을 동적으로 교체하거나 선택만 하면 되는 구조다. 핵심은 "어떤 일을 어떻게 할지"를 객체 외부에서 결정하게 만드는 것이다.
가장 전형적인 예시는 결제 시스템이다. 사용자가 신용카드, 카카오페이, 네이버페이 등 여러 결제 수단 중 하나를 선택했을 때, 전략 패턴을 이용하면 PaymentProcessor라는 추상 인터페이스 아래에 CardPayment, KakaoPayPayment, NaverPayPayment 같은 다양한 구현체를 두고, 실행 시점에 전략만 바꾸면 된다. 클라이언트 코드에서는 결제 방식이 바뀌더라도, payment.pay(order) 같은 형태로 일관된 호출이 가능해진다.
전략 패턴은 객체지향 5원칙 중 OCP(개방-폐쇄 원칙)와 DIP(의존 역전 원칙)를 동시에 만족시키는 대표적인 예다. 알고리즘이 바뀌더라도 기존 코드는 닫혀 있고, 전략 인터페이스를 통해 느슨하게 결합되어 있다. 따라서 변경에 유연하고, 확장에 강하며, 테스트도 훨씬 쉬운 구조를 만들어 준다.
2-2. 전략 패턴의 구조와 코드 예제
전략 패턴은 크게 세 가지 요소로 구성된다:
- Context: 전략을 사용하는 클라이언트 역할 (예: OrderService)
- Strategy 인터페이스: 전략을 정의하는 추상화 계층 (예: PaymentStrategy)
- Concrete Strategy: 실제 전략을 구현한 클래스들 (예: CardPayment, KakaoPayPayment)
Java 기준의 예제를 보자면 다음과 같다.
public interface PaymentStrategy {
void pay(Order order);
}
public class CardPayment implements PaymentStrategy {
public void pay(Order order) {
System.out.println("카드 결제 완료");
}
}
public class OrderService {
private final PaymentStrategy strategy;
public OrderService(PaymentStrategy strategy) {
this.strategy = strategy;
}
public void checkout(Order order) {
strategy.pay(order);
}
}
이 구조에서 OrderService는 PaymentStrategy에만 의존하고, 구체적인 결제 방식은 외부에서 주입된다. 사용자는 필요에 따라 전략 객체만 바꿔주면 전혀 다른 방식으로 동작하는 OrderService를 재활용할 수 있다. 이 방식은 단순히 코드 분리의 차원이 아니라, 비즈니스 정책을 객체 단위로 모듈화해서 관리하는 설계 전략이다.
2-3. 전략 패턴의 강점과 적용 포인트
전략 패턴의 가장 큰 장점은 변경과 확장을 설계 레벨에서 허용한다는 점이다. 조건문이나 switch문으로 알고리즘 분기를 관리하는 대신, 각 알고리즘을 독립된 객체로 분리하면 새로운 전략을 추가하거나 수정할 때 기존 코드는 일절 건드릴 필요가 없다. 이 패턴은 특히 다음과 같은 상황에 적합하다.
- 정책이 자주 바뀌는 비즈니스 로직 (결제, 배송, 수수료 계산 등)
- 사용자에 따라 알고리즘이 달라지는 경우 (유저 레벨에 따른 추천 방식)
- 테스트 시 특정 전략만 분리해서 검증하고 싶은 경우
또한 전략 패턴은 기능 전환, A/B 테스트, 다국어 로직 교체 등에도 유용하게 활용된다. 핵심은 전략을 객체로 취급함으로써, 코드가 아니라 구성(configuration) 으로 동작을 제어할 수 있게 된다는 점이다.
2-4. 실무 프레임워크에서의 활용 방식
Spring Framework에서도 전략 패턴은 널리 사용된다. 특히 DI 컨테이너와 함께 사용되면, 전략 패턴의 유연성은 극대화된다. 예를 들어 Bean 이름으로 전략을 맵핑하거나, @Qualifier로 주입 받을 전략을 선택할 수 있고, 특정 프로파일이나 설정값에 따라 전략을 바꿔치기할 수 있다.
예를 들어 다음과 같이 조건에 따라 다른 구현체를 주입할 수 있다.
@Autowired
@Qualifier("kakaoPay")
private PaymentStrategy paymentStrategy;
또한 Spring Boot에서는 전략 목록을 전부 주입 받아, 맵 형태로 관리하면서 이름으로 선택하는 방식도 가능하다.
@Autowired
private Map<String, PaymentStrategy> strategies;
이 방식은 서비스 구조가 점점 더 복잡해질수록 유용하다. 전략의 수가 늘어나더라도 DI 컨테이너가 알아서 관리해주고, 코드는 전략을 선택하고 위임만 하면 된다. 이는 전략 패턴이 프레임워크의 구성철학과 가장 자연스럽게 결합되는 행동 패턴임을 보여준다.
3. 옵저버 패턴의 원리와 적용
3-1. 옵저버 패턴이란 무엇인가?
옵저버(Observer) 패턴은 어떤 객체의 상태가 변했을 때, 그 변화를 감지하고 자동으로 반응하는 객체들을 구조적으로 연결하는 방식이다. 즉, 한 객체(주제 또는 Subject)가 변경되면, 그것을 관찰(Observe)하고 있는 여러 옵저버 객체들이 자동으로 알림을 받고 동작을 수행하게 되는 이벤트 기반 구조다.
예를 들어 게시판에 새로운 게시물이 등록되었을 때, 해당 게시판을 구독하고 있는 사용자에게 알림이 전송되는 구조를 생각해보자. 이때 게시판이 Subject이고, 각 알림 수신자가 Observer 역할을 한다. 게시판은 단지 “게시물이 등록되었다”는 사실만 통보하며, 알림을 어떻게 처리할지는 각 옵저버가 개별적으로 정의한다.
옵저버 패턴은 이벤트 브로드캐스트, 실시간 데이터 전파, UI 갱신, 도메인 이벤트 처리 등 “변화에 반응해야 하는 구조”에서 매우 자주 사용된다. 무엇보다 핵심은, Subject가 자신의 변경 사실을 Observer들에게 명시적으로 호출하지 않고도 자동으로 반영되게 한다는 점에 있다.
3-2. 핵심 구성 요소와 동작 원리
옵저버 패턴은 네 가지 구성 요소로 이루어진다.
- Subject: 상태를 갖고 있는 핵심 객체. 옵저버를 등록/해지하고 변경 시 알림을 보낸다.
- Observer 인터페이스: 상태 변화에 반응하는 공통 인터페이스를 정의한다.
- ConcreteObserver: 실제 알림을 받아 처리하는 클래스들.
- Event 또는 데이터: 전달되는 정보(선택적 요소).
기본 흐름은 이렇다: 옵저버는 Subject에 등록(subscribe)되고, Subject에서 상태가 바뀌면 등록된 옵저버에게 update() 호출로 알림이 간다. 각 옵저버는 받은 데이터를 바탕으로 스스로 알아서 반응한다. Java에서는 Observer와 Observable이 내장 클래스로 존재했지만, 이후 deprecated 되었고, 현재는 사용자 정의 구조나 리액티브 라이브러리(RxJava, Reactor 등)를 통해 구현하는 경우가 많다. JS에서는 EventEmitter, C#에서는 IObservable<T> 등 언어별로 다양한 구현이 존재한다.
3-3. 리액티브 프로그래밍과의 연결
최근에는 옵저버 패턴이 리액티브 프로그래밍의 핵심 철학으로 다시 부각되고 있다. RxJava, Project Reactor, RxJS 등은 옵저버 패턴을 기반으로 “데이터 흐름” 자체를 추상화하는 모델을 제공한다. 이 구조는 단순히 한 번의 변경 알림이 아니라, 데이터의 스트림(stream)을 기반으로 하는 지속적인 관찰 및 반응을 가능하게 만든다.
리액티브 스트림에서 Observable은 일종의 Subject 역할을 하고, Subscriber나 Observer는 이벤트를 받아서 처리한다. 예를 들어 사용자 클릭 이벤트, 웹소켓 메시지 수신, 백엔드 API 스트림 응답 등을 옵저버로 처리하면, 비동기/비차단/이벤트 중심 시스템을 효율적으로 구현할 수 있다.
즉, 옵저버 패턴은 단순한 알림 전달을 넘어, 현대 리액티브 시스템의 핵심 설계 철학으로 확장되었다. 리액티브 프로그래밍을 설계적으로 이해하려면 옵저버 패턴의 개념을 정확히 이해하는 것이 필수적이다.
3-4. 실무 적용: Pub/Sub과의 차이점
옵저버 패턴은 종종 메시지 브로커 기반의 Pub/Sub 구조와 혼동된다. 둘은 비슷하지만 약간의 차이가 있다. 옵저버 패턴은 코드 안에서 객체 간 직접 연결로 구성되는 이벤트 구조이며, Pub/Sub은 시스템 레벨에서 메시지를 비동기적으로 주고받는 분산 구조다.
예를 들어 옵저버 패턴은 한 객체가 메모리 내에서 다른 객체들에게 직접 이벤트를 푸시하는 구조이며, 주로 하나의 프로세스 내에서 동작한다. 반면 Pub/Sub 시스템(예: Kafka, RabbitMQ)은 발행자와 구독자가 네트워크를 통해 메시지를 주고받으며, 서로 존재를 몰라도 메시지로 연결된다.
그러므로 옵저버 패턴은 구조적으로 tight coupling이지만 단순한 시스템, Pub/Sub은 loose coupling이지만 복잡한 시스템에 적합하다. 실무에서는 UI 갱신, 메모리 내 캐시 동기화 등은 옵저버 패턴이, 서비스 간 이벤트 전파는 Pub/Sub 구조가 알맞다.
4. 커맨드 패턴의 구조와 확장성
4-1. 커맨드 패턴이란 무엇인가?
커맨드(Command) 패턴은 요청(Request)을 객체로 캡슐화하여, 실행 시점과 실행 주체를 분리시키는 설계 패턴이다. 즉, 어떤 동작을 실행하는 것이 아니라, “무엇을 할 것인가”라는 명령 자체를 객체로 만들어두고 나중에 실행하는 구조다. 이 패턴의 핵심은 행동(메서드 호출)을 데이터처럼 다루는 것이다. 예를 들어 사용자가 버튼을 클릭하면 바로 함수를 실행하는 것이 아니라, 그 클릭 자체를 명령 객체로 만들어 execute()라는 메서드를 호출한다. 이 명령은 필요하면 큐에 저장하거나, 실행 순서를 바꾸거나, 나중에 취소(undo)할 수도 있다.
커맨드 패턴은 실행 시점이 유동적이거나, 동작 자체를 저장·관리해야 하는 경우에 매우 유용하다. 대표적으로 UI 버튼 액션, 매크로 실행, 실행 취소 기능, 작업 큐, 스케줄링 시스템 등에서 커맨드 패턴이 효과적으로 활용된다. 즉, 행동의 구조화가 필요한 영역에서 가장 강력한 해결책이 된다.
4-2. 커맨드 패턴의 구성 요소와 흐름
커맨드 패턴은 보통 다음과 같은 구성 요소를 가진다.
- Command 인터페이스: execute()라는 단일 메서드를 정의하며, 모든 명령이 이 인터페이스를 구현한다.
- ConcreteCommand: 실제 명령을 정의하며, 수행할 대상 객체(Receiver)를 포함하고 있다.
- Receiver: 명령을 수행하는 실제 로직을 가진 객체.
- Invoker: 명령을 호출하는 객체. 주로 UI나 외부 시스템 이벤트가 여기에 해당한다.
- Client: 명령 객체를 생성하고 설정하는 코드.
실제 흐름은 이렇다. 클라이언트가 명령 객체를 만들고, 이를 호출자에게 넘긴다. 호출자는 그 명령을 실행하거나 저장해두고 나중에 실행한다. Receiver는 커맨드 내부에 포함되어 실제 작업을 수행한다. 이로 인해 요청 발신자(Invoker)와 수신자(Receiver)가 완전히 분리된다.
예를 들어 Light 클래스를 Receiver로 두고, TurnOnCommand는 이 Light를 켜는 명령을 정의하며, 버튼 클릭(Invoker)은 command.execute()만 호출하면 된다. 버튼은 명령의 구체 내용을 몰라도 되고, Light는 누가 호출했는지를 알 필요가 없다.
4-3. 커맨드 패턴의 강점: 취소와 매크로
커맨드 패턴의 가장 강력한 기능 중 하나는 실행 취소(Undo) 기능을 자연스럽게 구현할 수 있다는 점이다. 각각의 Command 객체는 execute() 뿐만 아니라 undo()를 함께 제공할 수 있으며, 실행된 명령을 스택에 쌓아두었다가 undo()를 호출하면 된다. 이 구조는 복잡한 상태 롤백 없이도 강력한 UX 경험을 만들 수 있게 한다.
또한 커맨드 패턴은 매크로(MacroCommand) 구성도 쉽게 만든다. 여러 개의 명령을 하나의 커맨드로 묶어 연속 실행할 수 있기 때문이다. 예를 들어 ‘게임 시작’ 버튼을 누르면 캐릭터 생성, 맵 로딩, UI 초기화 등을 순차적으로 실행해야 할 때, 이들을 하나의 매크로 명령으로 구성하면 된다.
커맨드는 기본적으로 명령 큐, 스케줄링 시스템, 워크플로우 제어 등에서 지연 실행과 순차 실행을 자연스럽게 지원한다. 이는 시스템이 점점 복잡해지고 이벤트 기반 비동기 로직이 많아질수록 더욱 큰 장점으로 부각된다.
4-4. 실무에서 커맨드 패턴이 필요한 순간들
커맨드 패턴은 실제 다양한 영역에서 사용된다. 대표적으로는 GUI 프레임워크의 버튼 액션 처리, 게임의 키 입력 처리, 복잡한 서버 사이드 작업 큐, 워크플로우 엔진의 작업 추적 등이 있다. 특히 이벤트가 순차적으로 처리되거나, 상태를 롤백하거나, 복잡한 조합을 다뤄야 할 때 매우 유용하다.
예를 들어 도메인 이벤트 기반 아키텍처에서는 명령(Command)와 이벤트(Event)를 분리하여 각각 책임을 나누는데, 이때 Command 객체는 특정한 의도를 가진 행동으로 기록되고, 그 실행 여부나 결과를 명확히 추적할 수 있다. 이는 CQRS나 Event Sourcing 기반 설계에서 매우 자주 등장하는 구조다.
또한 테스트 측면에서도 커맨드 패턴은 큰 이점이 있다. 각각의 Command 객체는 명확한 입력과 결과를 가지므로 단위 테스트가 매우 수월하며, 여러 명령을 조합해 테스트 시나리오를 쉽게 구성할 수 있다. 이는 복잡한 업무 로직이 존재하는 시스템에서 테스트 안정성을 확보하는 데 매우 효과적이다.
5. 전략 vs 옵저버 vs 커맨드 비교
5-1. 세 패턴의 역할과 책임 차이
전략, 옵저버, 커맨드 패턴은 모두 행동을 객체화한다는 공통점이 있지만, 그 역할과 책임은 명확히 다르다. 전략 패턴은 “여러 알고리즘 중 하나를 선택”하는 구조이며, 옵저버 패턴은 “상태 변화에 반응”하는 구조, 그리고 커맨드 패턴은 “행동 자체를 저장·전달”하는 구조다.
전략은 동작을 추상화하고 교체하는 데 목적이 있다. 알고리즘이나 정책이 자주 바뀌거나, 다양한 동작을 런타임에 선택해야 하는 경우에 적합하다. 반면 옵저버는 어떤 객체의 상태가 변할 때 여러 객체에 자동으로 알림을 보내야 할 때 사용되며, 주로 이벤트 중심 설계에 강점을 가진다. 커맨드는 사용자 요청이나 시스템 명령을 저장하고, 실행, 취소, 재실행을 가능하게 만든다.
이처럼 세 패턴은 모두 “행동”을 다루지만, 각각 전략적 분기, 이벤트 전파, 행위의 캡슐화와 제어라는 서로 다른 관점을 제공한다. 올바른 패턴을 선택하려면 이 차이를 명확히 인식하고, 설계 목적에 맞게 적용해야 한다.
5-2. 트리거 방식과 객체 간 관계 구조
이 세 패턴은 트리거(trigger)의 방식과 객체 간 관계에서도 차이를 보인다. 전략 패턴은 호출자가 명시적으로 특정 전략을 주입받아 사용한다. 즉, 주체가 전략을 직접 선택해서 호출하는 방식이다. 이로 인해 전략 패턴은 호출자가 행동의 주도권을 갖는다.
반면 옵저버 패턴은 트리거가 내부 상태 변화다. 객체의 내부 변화가 발생하면 외부 객체들이 자동으로 반응하는 구조다. 즉, Subject는 자신이 알림을 줄 필요는 있지만, 각 옵저버들이 어떤 행동을 하는지는 알지 못한다. 이는 객체 간의 의존성을 낮추는 장점이 있다.
커맨드 패턴은 트리거가 주로 사용자 이벤트(버튼 클릭, 명령 호출)이며, 행동 자체가 하나의 독립 객체로 추상화된다. 트리거가 발생하면 Invoker는 단순히 command.execute()만 호출하고, 커맨드는 Receiver를 통해 실제 작업을 수행한다. 따라서 호출자와 수행자 간의 결합도가 낮아지고, 행동 자체를 큐잉하거나 조합할 수 있는 유연성이 생긴다.
5-3. 패턴 적용 시 고려해야 할 조건
세 패턴을 적용할 때는 다음과 같은 기준을 고려해야 한다:
- 동작 선택의 유연성이 필요하다면 전략 패턴이 적합하다. 정책이 잦은 경우, 사용자 옵션이 다양할 경우에 특히 유용하다.
- 상태 변경에 따른 반응이 필요하면 옵저버 패턴이 적합하다. 주로 UI 바인딩, 알림 전파, 리액티브 설계 등에 자주 쓰인다.
- 행동을 기록하거나 재실행, 취소할 필요가 있다면 커맨드 패턴을 써야 한다. 특히 사용자 요청이나 시스템 명령을 구조적으로 다뤄야 할 때 적합하다.
이 기준은 단순히 상황에 맞는 패턴을 골라야 한다는 의미를 넘어서, 시스템의 흐름 제어 방식 자체를 정의하는 설계 기준이 된다. 행동이 반복되는지, 지연 가능한지, 동적으로 연결되는지 등도 패턴 선택에 영향을 미친다.
5-4. 조합과 혼합 사용의 가능성
현실의 시스템에서는 하나의 패턴만 사용하는 경우보다, 여러 패턴을 혼합 사용하는 경우가 훨씬 더 많다. 예를 들어 커맨드 패턴으로 사용자 요청을 객체로 캡슐화한 뒤, 전략 패턴으로 명령 수행 방식(예: 빠르게, 안전하게)을 분리할 수 있다. 또한 실행된 커맨드가 내부적으로 이벤트를 발생시키고, 옵저버들이 이를 구독하여 반응하는 구조도 가능하다. 특히 이벤트 기반 시스템에서는 옵저버와 커맨드가 함께 사용되는 경우가 많다. 이벤트가 발생하면 커맨드 객체를 만들어 큐에 넣거나, 명령을 재처리하는 구조는 많은 UI 프레임워크에서 기본 동작이기도 하다. 전략 패턴도 동적으로 커맨드를 교체하거나, 커맨드를 구성할 때 전략을 내부 구성 요소로 활용할 수 있다.
이처럼 행동 패턴들은 상호 보완적이며, 하나의 기능을 세 가지 방식으로 나눠볼 수 있다는 점에서 매우 유연하다. 중요한 건 각각의 패턴이 가진 핵심 의도와 트리거 방식, 책임 분리를 설계자의 관점에서 제대로 이해하고 활용하는 것이다.