행위 패턴(Behavioral Patterns): 객체 간 책임 분배와 통신
Observer, Strategy, Command, Saga, Circuit Breaker, Event Sourcing 등 행위 패턴 총정리
개요
행위 패턴은 객체 간의 책임 분배와 통신 방법을 다루는 디자인 패턴이다. 객체 간의 상호작용을 정의하되, 결합도를 최소화하여 유연한 시스템을 구축하는 것이 핵심이다. 분산 시스템 시대에는 서비스 간 통신과 장애 대응을 위한 아키텍처 패턴으로 확장되었다.
Observer 패턴 (관찰자)
정의
하나의 객체(Subject) 상태가 변할 때 종속된 모든 객체(Observer)에게 자동으로 알리는 일대다(One-to-Many) 의존 관계를 정의하는 패턴이다.
구현 예시
// Subject 인터페이스
public interface EventPublisher<T> {
void subscribe(EventListener<T> listener);
void unsubscribe(EventListener<T> listener);
void notify(T event);
}
// Observer 인터페이스
@FunctionalInterface
public interface EventListener<T> {
void onEvent(T event);
}
// 구현: 주문 이벤트 발행자
public class OrderEventPublisher implements EventPublisher<OrderEvent> {
private final List<EventListener<OrderEvent>> listeners =
new CopyOnWriteArrayList<>(); // 스레드 안전
public void subscribe(EventListener<OrderEvent> listener) {
listeners.add(listener);
}
public void unsubscribe(EventListener<OrderEvent> listener) {
listeners.remove(listener);
}
public void notify(OrderEvent event) {
listeners.forEach(listener -> listener.onEvent(event));
}
}
// 사용
OrderEventPublisher publisher = new OrderEventPublisher();
publisher.subscribe(event -> sendEmail(event)); // 이메일 알림
publisher.subscribe(event -> updateInventory(event)); // 재고 갱신
publisher.subscribe(event -> recordAnalytics(event)); // 분석 기록
publisher.subscribe(event -> notifyWarehouse(event)); // 창고 알림Observer의 현대적 발전
| 진화 | 설명 | 예시 |
|---|---|---|
| 이벤트 리스너 (Event Listener) | DOM 이벤트, 콜백 기반 | addEventListener() |
| Pub/Sub (Publish-Subscribe) | 발행자와 구독자 완전 분리 | Redis Pub/Sub, NATS |
| Reactive Streams | 백프레셔(Backpressure) 지원 | RxJava, Project Reactor, Akka Streams |
| 메시지 브로커 | 분산 시스템 간 비동기 통신 | Apache Kafka, RabbitMQ, AWS SNS/SQS |
Push vs Pull 모델
- Push: Subject가 Observer에게 데이터를 직접 전달 (위 예시)
- Pull: Subject가 변경만 알리고, Observer가 필요한 데이터를 가져감
Strategy 패턴 (전략)
정의
교환 가능한 알고리즘 가족을 정의하고 각각을 캡슐화하여, 클라이언트에 영향을 주지 않고 알고리즘을 교체할 수 있게 하는 패턴이다.
구현 예시: 결제 방법 선택
// 전략 인터페이스
public interface PaymentStrategy {
PaymentResult pay(Money amount, PaymentDetails details);
boolean supports(PaymentType type);
}
// 구체 전략들
public class CreditCardStrategy implements PaymentStrategy {
public PaymentResult pay(Money amount, PaymentDetails details) {
// 신용카드 결제 로직
return callCardGateway(details.getCardNumber(), amount);
}
public boolean supports(PaymentType type) {
return type == PaymentType.CREDIT_CARD;
}
}
public class BankTransferStrategy implements PaymentStrategy {
public PaymentResult pay(Money amount, PaymentDetails details) {
// 계좌이체 로직
return initiateBankTransfer(details.getAccountNumber(), amount);
}
public boolean supports(PaymentType type) {
return type == PaymentType.BANK_TRANSFER;
}
}
public class CryptoStrategy implements PaymentStrategy {
public PaymentResult pay(Money amount, PaymentDetails details) {
// 암호화폐 결제 로직
return sendCryptoTransaction(details.getWalletAddress(), amount);
}
public boolean supports(PaymentType type) {
return type == PaymentType.CRYPTO;
}
}
// 컨텍스트: 전략을 사용하는 클래스
public class PaymentProcessor {
private final List<PaymentStrategy> strategies;
public PaymentProcessor(List<PaymentStrategy> strategies) {
this.strategies = strategies;
}
public PaymentResult process(PaymentType type, Money amount,
PaymentDetails details) {
return strategies.stream()
.filter(s -> s.supports(type))
.findFirst()
.orElseThrow(() -> new UnsupportedPaymentException(type))
.pay(amount, details);
}
}Strategy와 함수형 프로그래밍
함수형 프로그래밍에서는 Strategy 패턴이 **고차 함수(Higher-Order Function)**로 자연스럽게 표현된다:
// 함수형 Strategy
type SortStrategy<T> = (a: T, b: T) => number;
const byPrice: SortStrategy<Product> = (a, b) => a.price - b.price;
const byRating: SortStrategy<Product> = (a, b) => b.rating - a.rating;
const byName: SortStrategy<Product> = (a, b) => a.name.localeCompare(b.name);
// 전략을 함수 인자로 전달
const sortedProducts = products.toSorted(byPrice);Template Method 패턴 (템플릿 메서드)
정의
알고리즘의 골격(Skeleton)을 정의하고, 일부 단계를 서브클래스에 위임하는 패턴이다. 알고리즘의 구조는 변경하지 않으면서 특정 단계를 재정의할 수 있다.
구현 예시: 데이터 처리 파이프라인
public abstract class DataProcessor {
// Template Method: 알고리즘의 골격 (final로 변경 방지)
public final ProcessingResult process(DataSource source) {
RawData raw = extract(source); // 1. 추출
ValidatedData validated = validate(raw); // 2. 검증
TransformedData transformed = transform(validated); // 3. 변환
load(transformed); // 4. 적재
return createResult(transformed); // 5. 결과 생성
}
// 공통 단계: 기본 구현 제공
protected RawData extract(DataSource source) {
return source.read();
}
// 추상 단계: 서브클래스가 반드시 구현
protected abstract ValidatedData validate(RawData raw);
protected abstract TransformedData transform(ValidatedData data);
// Hook 메서드: 선택적으로 오버라이드
protected void load(TransformedData data) {
defaultStorage.save(data);
}
protected ProcessingResult createResult(TransformedData data) {
return new ProcessingResult(data.getRecordCount());
}
}
// CSV 데이터 프로세서
public class CsvDataProcessor extends DataProcessor {
@Override
protected ValidatedData validate(RawData raw) {
// CSV 특화 검증 로직
return csvValidator.validate(raw);
}
@Override
protected TransformedData transform(ValidatedData data) {
// CSV 특화 변환 로직
return csvTransformer.transform(data);
}
}Template Method vs Strategy
| 비교 항목 | Template Method | Strategy |
|---|---|---|
| 변경 메커니즘 | 상속 (Inheritance) | 합성 (Composition) |
| 알고리즘 교체 시점 | 컴파일타임 | 런타임 |
| 제어 흐름 | 부모 클래스가 결정 | 클라이언트가 결정 |
| 유연성 | 낮음 (상속 기반) | 높음 (조합 기반) |
Command 패턴 (명령)
정의
요청을 객체로 캡슐화하여, 요청의 매개변수화, 큐잉, 로깅, 취소(Undo) 등을 가능하게 하는 패턴이다.
구현 예시: 작업 큐 시스템
// Command 인터페이스
public interface Command {
CommandResult execute();
void undo(); // 실행 취소
String describe(); // 로깅용
}
// 구체 Command
public class CreateOrderCommand implements Command {
private final OrderService orderService;
private final OrderRequest request;
private String createdOrderId;
public CreateOrderCommand(OrderService orderService, OrderRequest request) {
this.orderService = orderService;
this.request = request;
}
@Override
public CommandResult execute() {
Order order = orderService.create(request);
this.createdOrderId = order.getId(); // undo를 위해 저장
return CommandResult.success(order);
}
@Override
public void undo() {
if (createdOrderId != null) {
orderService.cancel(createdOrderId);
}
}
@Override
public String describe() {
return "CreateOrder: " + request.getProductId();
}
}
// 명령 실행기 (Invoker)
public class CommandQueue {
private final Queue<Command> pending = new ConcurrentLinkedQueue<>();
private final Deque<Command> executed = new ConcurrentLinkedDeque<>();
public void enqueue(Command command) {
pending.offer(command);
}
public void processAll() {
while (!pending.isEmpty()) {
Command cmd = pending.poll();
try {
cmd.execute();
executed.push(cmd);
} catch (Exception e) {
// 실패 시 이전 명령들 롤백
rollbackAll();
throw e;
}
}
}
public void rollbackAll() {
while (!executed.isEmpty()) {
executed.pop().undo();
}
}
}Command 패턴의 확장: CQRS
Command 패턴은 **CQRS(Command Query Responsibility Segregation)**의 기반이다:
클라이언트 ──→ Command Bus ──→ Command Handler ──→ Write DB
클라이언트 ──→ Query Bus ──→ Query Handler ──→ Read DB- Command: 상태를 변경하는 요청 (Create, Update, Delete)
- Query: 상태를 조회하는 요청 (Read)
- 읽기/쓰기 모델을 분리하여 각각 최적화
Event Sourcing 패턴
정의
애플리케이션 상태의 모든 변경을 이벤트(Event)의 순차적 기록으로 저장하는 패턴이다. 현재 상태는 이벤트를 순서대로 재생(Replay)하여 도출한다.
기존 방식 vs Event Sourcing
[기존 방식: 상태 저장]
Account: { id: 1, balance: 1500 }
→ 어떻게 1500이 되었는지 알 수 없음
[Event Sourcing: 이벤트 저장]
1. AccountCreated { id: 1, initialBalance: 0 }
2. MoneyDeposited { id: 1, amount: 2000 }
3. MoneyWithdrawn { id: 1, amount: 500 }
→ 재생하면 balance = 0 + 2000 - 500 = 1500
→ 모든 변경 이력이 완벽하게 보존됨구현 예시
// 도메인 이벤트
public sealed interface AccountEvent {
record AccountCreated(String accountId, Money initialBalance,
Instant timestamp) implements AccountEvent {}
record MoneyDeposited(String accountId, Money amount,
Instant timestamp) implements AccountEvent {}
record MoneyWithdrawn(String accountId, Money amount,
Instant timestamp) implements AccountEvent {}
}
// 이벤트 저장소
public interface EventStore {
void append(String streamId, List<AccountEvent> events, long expectedVersion);
List<AccountEvent> getEvents(String streamId);
List<AccountEvent> getEvents(String streamId, long fromVersion);
}
// Aggregate: 이벤트를 재생하여 상태 복원
public class Account {
private String id;
private Money balance;
private long version;
// 이벤트 재생으로 상태 복원
public static Account reconstitute(List<AccountEvent> events) {
Account account = new Account();
events.forEach(account::apply);
return account;
}
private void apply(AccountEvent event) {
switch (event) {
case AccountCreated e -> {
this.id = e.accountId();
this.balance = e.initialBalance();
}
case MoneyDeposited e -> {
this.balance = this.balance.add(e.amount());
}
case MoneyWithdrawn e -> {
this.balance = this.balance.subtract(e.amount());
}
}
this.version++;
}
}Event Sourcing의 장단점
| 장점 | 단점 |
|---|---|
| 완전한 감사 추적 (Audit Trail) | 이벤트 스키마 진화(Evolution) 관리 복잡 |
| 시간 여행 디버깅 (Time Travel) | 이벤트 저장소 크기 증가 |
| 이벤트 재생으로 다양한 읽기 모델 생성 | 최종 일관성(Eventual Consistency) |
| 자연스러운 CQRS 결합 | 학습 곡선 높음 |
스냅샷 (Snapshot)
이벤트가 많아지면 재생 시간이 길어진다. 스냅샷으로 성능을 최적화한다:
이벤트 1 → 이벤트 2 → ... → 이벤트 1000 → [스냅샷] → 이벤트 1001 → ...
↑
복원 시 여기부터 재생 시작Saga 패턴
정의
분산 트랜잭션(Distributed Transaction)을 일련의 로컬 트랜잭션으로 분해하여 관리하는 패턴이다. 각 로컬 트랜잭션이 실패하면 **보상 트랜잭션(Compensating Transaction)**을 실행하여 롤백한다.
두 가지 구현 방식
Choreography (안무형)
Order Service ──→ [OrderCreated Event]
│
├──→ Payment Service ──→ [PaymentCompleted Event]
│ │
│ ├──→ Inventory Service ──→ [ItemReserved]
│ │ │
│ │ Shipping Service
│ │
│ [PaymentFailed] → 보상 트랜잭션 실행- 각 서비스가 이벤트를 발행하고 다른 서비스가 반응
- 중앙 조정자 없음
- 단순하지만, 복잡한 플로우에서는 추적이 어려움
Orchestration (오케스트라형)
Saga Orchestrator
┌──────────────┐
│ 1. 주문 생성 │──→ Order Service
│ 2. 결제 처리 │──→ Payment Service
│ 3. 재고 예약 │──→ Inventory Service
│ 4. 배송 요청 │──→ Shipping Service
└──────────────┘
실패 시 역순으로 보상 트랜잭션 실행- 중앙 오케스트레이터가 트랜잭션 순서를 관리
- 복잡한 플로우 관리에 적합
- 오케스트레이터가 단일 장애점(SPOF)이 될 수 있음
Choreography vs Orchestration
| 비교 항목 | Choreography | Orchestration |
|---|---|---|
| 결합도 | 낮음 | 중간 (오케스트레이터 의존) |
| 복잡도 관리 | 서비스 수 증가 시 어려움 | 중앙에서 관리 |
| 가시성 | 분산되어 추적 어려움 | 중앙에서 모니터링 용이 |
| 확장성 | 높음 | 오케스트레이터 병목 가능 |
| 적합한 상황 | 단순한 플로우 (3-4단계) | 복잡한 비즈니스 플로우 |
AWS에서의 Saga 구현
- AWS Step Functions: Orchestration 방식의 Saga를 시각적으로 구성
- Amazon EventBridge: Choreography 방식의 이벤트 라우팅
- Amazon SQS/SNS: 서비스 간 비동기 메시지 전달
Circuit Breaker 패턴 (회로 차단기)
정의
외부 서비스 호출의 연속 실패를 감지하여 호출을 차단하고, 장애가 복구될 때까지 빠르게 실패(Fail Fast)하는 패턴이다.
상태 전이도
성공
┌──────────────┐
▼ │
[CLOSED] ──실패 누적──→ [OPEN] ──타임아웃──→ [HALF-OPEN]
▲ │ │
│ ▼ │
│ 즉시 실패 반환 일부 요청 허용
│ │
└──────────── 성공 ──────────────────────────┘
실패 → [OPEN]으로 복귀상태별 동작
| 상태 | 동작 | 전이 조건 |
|---|---|---|
| CLOSED (정상) | 모든 요청 통과 | 실패율이 임계값 초과 → OPEN |
| OPEN (차단) | 모든 요청 즉시 거부 (Fail Fast) | 타임아웃 경과 → HALF-OPEN |
| HALF-OPEN (시험) | 제한된 요청만 허용 | 성공 → CLOSED, 실패 → OPEN |
구현 예시
public class CircuitBreaker {
private enum State { CLOSED, OPEN, HALF_OPEN }
private volatile State state = State.CLOSED;
private final AtomicInteger failureCount = new AtomicInteger(0);
private final int failureThreshold;
private final Duration timeout;
private volatile Instant lastFailureTime;
public <T> T execute(Supplier<T> operation, Supplier<T> fallback) {
if (state == State.OPEN) {
if (isTimeoutExpired()) {
state = State.HALF_OPEN;
} else {
return fallback.get(); // Fail Fast
}
}
try {
T result = operation.get();
onSuccess();
return result;
} catch (Exception e) {
onFailure();
return fallback.get();
}
}
private void onSuccess() {
failureCount.set(0);
state = State.CLOSED;
}
private void onFailure() {
lastFailureTime = Instant.now();
if (failureCount.incrementAndGet() >= failureThreshold) {
state = State.OPEN;
}
}
private boolean isTimeoutExpired() {
return Duration.between(lastFailureTime, Instant.now()).compareTo(timeout) > 0;
}
}실전 라이브러리
- Resilience4j (Java): 경량 장애 허용 라이브러리
- Polly (.NET): 정책 기반 복원력 라이브러리
- Hystrix (Netflix, 유지보수 종료): Resilience4j로 이주 권장
- Istio/Envoy: 서비스 메시 수준에서 Circuit Breaker 제공
Mediator 패턴 (중재자)
정의
객체 간의 직접적인 통신을 금지하고 중재자 객체를 통해서만 소통하게 하여, 객체 간 결합도를 낮추는 패턴이다.
활용 사례: 채팅방
public interface ChatMediator {
void sendMessage(String message, User sender);
void addUser(User user);
}
public class ChatRoom implements ChatMediator {
private final List<User> users = new ArrayList<>();
public void addUser(User user) {
users.add(user);
}
public void sendMessage(String message, User sender) {
users.stream()
.filter(user -> !user.equals(sender))
.forEach(user -> user.receive(message, sender.getName()));
}
}Mediator의 아키텍처적 적용
- 메시지 브로커 (Message Broker): Kafka, RabbitMQ가 서비스 간 Mediator 역할
- 이벤트 버스 (Event Bus): 마이크로서비스 간 이벤트 기반 통신
- API Gateway: 클라이언트와 서비스 사이의 중재자
Chain of Responsibility 패턴 (책임 연쇄)
정의
요청을 핸들러 체인을 따라 전달하여, 각 핸들러가 요청을 처리하거나 다음 핸들러로 넘기는 패턴이다.
구현 예시: HTTP 미들웨어
public interface RequestHandler {
Response handle(Request request);
}
public abstract class Middleware implements RequestHandler {
private Middleware next;
public Middleware setNext(Middleware next) {
this.next = next;
return next; // Fluent API
}
public Response handle(Request request) {
if (next != null) {
return next.handle(request);
}
return Response.ok();
}
}
public class AuthMiddleware extends Middleware {
public Response handle(Request request) {
if (!isAuthenticated(request)) {
return Response.unauthorized();
}
return super.handle(request); // 다음 핸들러로 전달
}
}
public class RateLimitMiddleware extends Middleware {
public Response handle(Request request) {
if (isRateLimited(request)) {
return Response.tooManyRequests();
}
return super.handle(request);
}
}
public class LoggingMiddleware extends Middleware {
public Response handle(Request request) {
log(request);
Response response = super.handle(request);
log(response);
return response;
}
}
// 체인 구성
Middleware chain = new LoggingMiddleware();
chain.setNext(new AuthMiddleware())
.setNext(new RateLimitMiddleware())
.setNext(new BusinessLogicHandler());활용 사례
- HTTP 미들웨어 체인 (Express.js, Spring Interceptor, ASP.NET Middleware)
- 로깅 레벨 필터 (DEBUG → INFO → WARN → ERROR)
- 승인 워크플로우 (팀장 → 부서장 → 임원)
State 패턴 (상태)
정의
객체의 내부 상태에 따라 행동을 변경하는 패턴이다. 상태 전이를 명시적인 객체로 표현한다.
구현 예시: 주문 상태 머신
public interface OrderState {
OrderState pay(Order order);
OrderState ship(Order order);
OrderState deliver(Order order);
OrderState cancel(Order order);
}
public class PendingState implements OrderState {
public OrderState pay(Order order) {
order.setPaymentDate(Instant.now());
return new PaidState();
}
public OrderState cancel(Order order) {
order.setCancelReason("Cancelled before payment");
return new CancelledState();
}
public OrderState ship(Order order) {
throw new IllegalStateException("Cannot ship unpaid order");
}
public OrderState deliver(Order order) {
throw new IllegalStateException("Cannot deliver unpaid order");
}
}
public class PaidState implements OrderState {
public OrderState ship(Order order) {
order.setShippingDate(Instant.now());
return new ShippedState();
}
public OrderState cancel(Order order) {
order.refund(); // 환불 처리
return new CancelledState();
}
// ...
}Reactor 패턴 (리액터)
정의
이벤트 기반 I/O 처리를 위한 패턴이다. 단일 스레드에서 이벤트 루프(Event Loop)를 사용하여 다수의 동시 I/O 요청을 처리한다.
핵심 구조
클라이언트 요청들
│ │ │
▼ ▼ ▼
[이벤트 디멀티플렉서 (Event Demultiplexer)]
(select, epoll, kqueue, io_uring)
│
▼
[이벤트 디스패처 (Reactor/Event Loop)]
│
┌─────┼─────┐
▼ ▼ ▼
Handler Handler HandlerReactor를 사용하는 시스템
| 시스템 | 구현 방식 |
|---|---|
| Node.js | libuv 기반 이벤트 루프 |
| Nginx | epoll/kqueue 기반 |
| Netty (Java) | NIO 기반 Reactor |
| Tokio (Rust) | io_uring/epoll 기반 async runtime |
| Spring WebFlux | Project Reactor (Netty 기반) |
Proactor 패턴과의 차이
| 비교 | Reactor | Proactor |
|---|---|---|
| I/O 완료 방식 | 준비(Ready) 알림 후 직접 I/O | 완료(Completion) 알림 |
| OS 지원 | epoll, kqueue | IOCP (Windows), io_uring (Linux) |
| 복잡도 | 상대적으로 낮음 | 높음 |
Actor Model (액터 모델)
정의
Actor를 계산의 기본 단위로 사용하는 동시성 모델이다. 각 Actor는 독립적인 상태를 가지며, 메시지 전달(Message Passing)로만 소통한다.
핵심 원칙
- 메시지 수신: Actor는 메시지를 받아 처리
- 새 Actor 생성: 다른 Actor를 생성할 수 있음
- 메시지 전송: 다른 Actor에게 메시지를 보낼 수 있음
- 상태 변경: 자신의 상태를 변경할 수 있음 (외부 접근 불가)
Actor 시스템의 특징
[Actor System]
│
├── /user
│ ├── /user/orderProcessor (Actor)
│ │ ├── /user/orderProcessor/validator
│ │ └── /user/orderProcessor/shipper
│ └── /user/paymentHandler (Actor)
│
└── /system (시스템 Actor)- 위치 투명성 (Location Transparency): Actor가 같은 프로세스, 다른 서버에 있어도 동일하게 메시지 전달
- 장애 격리 (Fault Isolation): 하나의 Actor 실패가 다른 Actor에 영향 없음
- 감독 전략 (Supervision Strategy): 부모 Actor가 자식 Actor의 장애를 관리
주요 구현체
| 구현체 | 언어 | 특징 |
|---|---|---|
| Akka | Scala, Java | 가장 성숙한 Actor 프레임워크 |
| Erlang/OTP | Erlang | Actor 모델의 원조 (BEAM VM) |
| Microsoft Orleans | C# | Virtual Actor (Grain) 모델 |
| Proto.Actor | Go, C#, Kotlin | 크로스 플랫폼 Actor |
| Pekko | Scala, Java | Akka의 Apache 포크 (2026 활발) |
Actor Model + Event Sourcing
Actor 모델은 Event Sourcing과 자연스럽게 결합된다:
- 각 Actor가 수신한 메시지를 이벤트로 저장
- Actor 장애 시 이벤트를 재생하여 상태 복원
- Akka Persistence가 이 패턴을 구현
행위 패턴 비교표
| 패턴 | 핵심 목적 | 적용 규모 | 대표 활용 |
|---|---|---|---|
| Observer | 상태 변경 알림 | 객체/서비스 | 이벤트 시스템, Pub/Sub |
| Strategy | 알고리즘 교체 | 객체 | 결제, 정렬, 할인 정책 |
| Template Method | 알고리즘 골격 | 클래스 | ETL 파이프라인, 프레임워크 |
| Command | 요청 캡슐화 | 객체 | 작업 큐, Undo, CQRS |
| Event Sourcing | 이벤트 기록 | 시스템 | 금융, 감사, 시간 여행 |
| Saga | 분산 트랜잭션 | 서비스 | 주문 처리, 결제 플로우 |
| Circuit Breaker | 장애 격리 | 서비스 | 외부 API 호출 보호 |
| Mediator | 통신 중재 | 객체/서비스 | 메시지 브로커, 이벤트 버스 |
| Chain of Responsibility | 요청 전달 | 객체 | HTTP 미들웨어 |
| State | 상태 기반 행동 | 객체 | 주문 상태, 워크플로우 |
| Reactor | 이벤트 기반 I/O | 시스템 | 웹 서버, 네트워크 |
| Actor Model | 메시지 기반 동시성 | 시스템 | 분산 시스템, 실시간 처리 |