행위 패턴은 객체 간의 책임 분배와 커뮤니케이션 방식에 초점을 맞춘 패턴이다. 객체들이 어떤 방식으로 상호작용하며 특정 행위를 어떤 객체가 책임질지를 정리하는 데에 중점을 둔다. 이 패턴들을 통해 시스템의 유연성, 확장성, 유지보수성이 높아지며 복잡한 조건문들을 제거하고 역할에 따라 책임을 위임할 수 있게 된다.
총 11가지 행위 패턴이 있으며 아래는 각 패턴의 목적과 대표 키워드를 정리한 표이다.
패턴명 | 설명 | 주요 키워드 |
---|---|---|
Strategy | 알고리즘을 런타임에 교체 | 알고리즘 캡슐화, 전략 객체 |
Observer | 상태 변화 감지 및 알림 | 이벤트, 구독, 발행 |
Template Method | 알고리즘의 골격 정의, 세부 구현은 하위 클래스에 위임 | 후킹 메서드, 상속 기반 |
Command | 요청을 객체로 캡슐화 | 요청, 실행 취소, 큐잉 |
State | 상태에 따라 행동 변경 | 상태 객체, 상태 전이 |
Chain of Responsibility | 요청을 처리할 수 있는 객체에게 책임을 넘김 | 체인 구성, 처리자 분리 |
Iterator | 컬렉션 순회 방법 캡슐화 | next() , hasNext() , 순회 인터페이스 |
Mediator | 객체 간 복잡한 상호작용을 중앙에서 조율 | 중재자, 의존성 감소 |
Memento | 객체의 이전 상태 저장 및 복원 | 스냅샷, undo 기능 |
Visitor | 객체 구조 변경 없이 새로운 기능 추가 | 방문자, double dispatch |
Interpreter | 언어 문법과 해석을 위한 구조 제공 | 문법 트리, 해석기 |
특정 기능을 수행하는 알고리즘을 인터페이스를 통해 통째로 바꾸며 사용하는 패턴이다. 알고리즘을 캡슐화하여 서로 교체 가능하게 만들고 런타임에 전략을 바꿀 수 있다. 복잡한 if-else 분기문을 제거할 수 있고 행위의 변화가 필요한 경우에도 유연하게 대처 가능하다.
// 전략 인터페이스
public interface PaymentStrategy {
void pay(int amount);
}
// 전략 구현체들
public class CreditCardStrategy implements PaymentStrategy {
public void pay(int amount) {
System.out.println("신용카드로 " + amount + "원 결제했습니다.");
}
}
public class KakaoPayStrategy implements PaymentStrategy {
public void pay(int amount) {
System.out.println("카카오페이로 " + amount + "원 결제했습니다.");
}
}
// Context
public class ShoppingCart {
private PaymentStrategy paymentStrategy;
public ShoppingCart(PaymentStrategy strategy) {
this.paymentStrategy = strategy;
}
public void checkout(int amount) {
paymentStrategy.pay(amount);
}
}
전자상거래 시스템에서 다양한 결제 수단(CreditCard, PayPal, KakaoPay 등)을 유연하게 교체할 수 있어야 할 때 유용하다. 또한 게임 AI에서 적의 전략을 교체하거나 경로 탐색 알고리즘을 유동적으로 바꿀 필요가 있는 경우에도 Strategy 패턴을 적용하면 복잡한 조건문 없이 유지보수와 확장이 쉬워진다.
어떤 객체의 상태 변화가 있을 때 그에 의존하는 다른 객체들이 자동으로 통보를 받는 패턴이다. 주로 이벤트 기반 시스템이나 데이터 바인딩 등에서 널리 사용된다. 주체(Subject)와 옵저버(Observer)의 관계를 설정하고 상태가 변경되면 자동으로 모든 옵저버에게 알린다.
// 옵저버 인터페이스
public interface Observer {
void update(String message);
}
// 주체 인터페이스
public interface Subject {
void attach(Observer o);
void detach(Observer o);
void notifyObservers(String message);
}
// 구체적인 주체
public class NewsAgency implements Subject {
private List<Observer> observers = new ArrayList<>();
public void attach(Observer o) {
observers.add(o);
}
public void detach(Observer o) {
observers.remove(o);
}
public void notifyObservers(String message) {
for (Observer o : observers) {
o.update(message);
}
}
}
// 구체적인 옵저버
public class Subscriber implements Observer {
private String name;
public Subscriber(String name) {
this.name = name;
}
public void update(String message) {
System.out.println(name + "님 새로운 뉴스: " + message);
}
}
GUI 이벤트 처리(버튼 클릭 시 리스너에게 알림), 메시지 브로드캐스트 시스템, 스프링의 ApplicationEventPublisher
를 통한 이벤트 발행 등에서 광범위하게 쓰인다. 데이터의 변경이 여러 컴포넌트에 영향을 미칠 때 효과적이다.
전체 알고리즘의 구조를 정의하고 일부 단계를 서브 클래스에서 구현하도록 위임하는 패턴이다. 공통 로직은 상위 클래스에 두되 변하는 부분만 하위 클래스에서 정의하여 중복을 제거하고 코드의 일관성을 유지할 수 있다.
public abstract class DataParser {
// 템플릿 메서드
public final void parseData() {
readData();
processData();
writeData();
}
protected abstract void readData();
protected abstract void processData();
protected abstract void writeData();
}
public class CSVParser extends DataParser {
protected void readData() {
System.out.println("CSV 파일 읽기");
}
protected void processData() {
System.out.println("CSV 데이터 처리");
}
protected void writeData() {
System.out.println("CSV 결과 저장");
}
}
JUnit의 테스트 실행 구조(@Before
, @Test
, @After
)가 대표적인 템플릿 메서드 구현이다. 또한 HTTP 요청 처리 파이프라인, 알고리즘 설계 시 공통된 프로세스를 템플릿으로 정의하고 특수 케이스를 하위 클래스에서 처리할 때 유용하다.
요청(명령)과 실행을 객체로 분리(캡슐화)해서 관리하는 패턴이다. 요청을 여러 단계로 구성하고 중심에서 실행할 수 있기 때문에 가장 큰 이점은 undo
및 redo
가 가능하다는 점이다.
// Command 인터페이스
public interface Command {
void execute();
}
// Receiver
public class Light {
public void turnOn() {
System.out.println("손전금 켜기");
}
public void turnOff() {
System.out.println("손전금 공기");
}
}
// ConcreteCommand
public class LightOnCommand implements Command {
private Light light;
public LightOnCommand(Light light) {
this.light = light;
}
public void execute() {
light.turnOn();
}
}
// Invoker
public class RemoteControl {
private Command command;
public void setCommand(Command command) {
this.command = command;
}
public void pressButton() {
command.execute();
}
}
UI 보통 앱에서 "버튼" 통을 통해 실행을 바꾸고자 할 때 유용하며 테스트 프로그램의 undo/redo, macro 시계를 구현할 때 코드 수행화를 최대화 할 수 있다.
객체의 상태에 따라 동일한 동작이 다르게 실행되도록 하는 패턴이다. 객체는 상태를 내부적으로 표현하는 객체를 포함하고 있으며 상태 변경에 따라 행위가 달라진다. 조건문을 통한 상태 분기 로직을 상태 객체로 캡슐화하여 관리하면 코드의 유연성과 가독성을 높일 수 있다.
// 상태를 표현하는 인터페이스
interface State {
void doAction(Context context);
}
// 시작 상태 구현
class StartState implements State {
public void doAction(Context context) {
System.out.println("시작 상태로 전이합니다.");
context.setState(this);
}
}
// 정지 상태 구현
class StopState implements State {
public void doAction(Context context) {
System.out.println("정지 상태로 전이합니다.");
context.setState(this);
}
}
// 상태를 가지는 컨텍스트
class Context {
private State state;
public void setState(State state) {
this.state = state;
}
public void applyState() {
state.doAction(this);
}
}
엘리베이터 시스템, TCP 연결 상태, 게임 캐릭터의 상태 전이(걷기, 뛰기, 공격 등) 같은 상황에서 사용하면 각 상태에 따른 행동을 분리하여 유지보수를 쉽게 할 수 있다.
요청을 처리할 수 있는 객체를 체인 형태로 연결하고 순차적으로 요청을 전달하여 처리하는 패턴이다. 각 처리 객체는 자신이 처리할 수 있는지 여부를 판단하고 아니면 다음 객체로 요청을 넘긴다. 이 구조는 복잡한 조건문을 제거하고 처리 책임을 분산시킬 수 있다.
// 핸들러 추상 클래스
abstract class Handler {
protected Handler next; // 다음 처리자
public void setNext(Handler next) {
this.next = next;
}
public abstract void handle(String request);
}
// 인증 요청 처리자
class AuthHandler extends Handler {
public void handle(String request) {
if (request.equals("AUTH")) {
System.out.println("인증 처리 완료");
} else if (next != null) {
next.handle(request);
}
}
}
// 로그 요청 처리자
class LogHandler extends Handler {
public void handle(String request) {
if (request.equals("LOG")) {
System.out.println("로그 처리 완료");
} else if (next != null) {
next.handle(request);
}
}
}
Spring Security의 필터 체인, 서블릿 필터 구조와 유사하며 요청 처리 전후에 여러 책임을 순차적으로 수행해야 할 때 유용하다. 또한 예외 처리, 접근 제어, 로깅 등의 관심사를 분리하는 데 효과적이다.
컬렉션 내부 구조를 노출하지 않고 요소를 순차적으로 접근할 수 있도록 하는 패턴이다. 반복 로직을 외부에 분리하여 컬렉션과 반복을 분리하고 다양한 반복 방법을 유연하게 지원한다.
// 반복자 인터페이스
interface Iterator<T> {
boolean hasNext(); // 다음 요소 존재 여부
T next(); // 다음 요소 반환
}
// 문자열 배열 반복자 구현
class NameIterator implements Iterator<String> {
private String[] names;
private int index = 0;
public NameIterator(String[] names) {
this.names = names;
}
public boolean hasNext() {
return index < names.length;
}
public String next() {
return names[index++];
}
}
Java의 Iterator
, Iterable
인터페이스가 대표적인 예이며 컬렉션 프레임워크의 for-each
문에서도 내부적으로 사용된다. 내부 데이터 구조를 숨기고 다양한 순회 방식(역방향, 필터링 등)을 쉽게 구현할 수 있다.
객체 간의 복잡한 상호작용을 중재자 객체가 대신 조율하여 의존성을 줄이는 패턴이다. 각 객체는 중재자에게만 의존하며 직접 다른 객체와 통신하지 않음으로써 결합도를 낮출 수 있다.
// 중재자 인터페이스
interface Mediator {
void sendMessage(String message, Colleague sender);
}
// 동료 객체 추상 클래스
abstract class Colleague {
protected Mediator mediator;
public Colleague(Mediator mediator) {
this.mediator = mediator;
}
}
// 사용자 클래스
class User extends Colleague {
private String name;
public User(String name, Mediator mediator) {
super(mediator);
this.name = name;
}
public void send(String message) {
mediator.sendMessage(message, this);
}
public void receive(String message) {
System.out.println(name + " received: " + message);
}
}
// 채팅 중재자 구현
class ChatMediator implements Mediator {
private List<User> users = new ArrayList<>();
public void addUser(User user) {
users.add(user);
}
public void sendMessage(String message, Colleague sender) {
for (User user : users) {
if (user != sender) {
user.receive(message);
}
}
}
}
채팅 시스템, 항공 교통 관제 시스템, UI 컴포넌트 간 상호작용 등 복잡한 객체 간 관계를 단순화할 때 효과적이다. MVC 패턴에서 Controller
가 Mediator
역할을 하기도 한다.
객체의 이전 상태를 저장하고 복원할 수 있도록 하는 패턴이다. 캡슐화를 유지하면서 객체 상태를 스냅샷처럼 보관하고 필요 시 복구할 수 있게 해준다. undo/redo 기능 구현에 자주 사용된다.
// 상태 저장 객체
class Memento {
private String state;
public Memento(String state) {
this.state = state;
}
public String getState() {
return state;
}
}
// 원본 객체
class Originator {
private String state;
public void setState(String state) {
this.state = state;
}
public Memento save() {
return new Memento(state); // 현재 상태 저장
}
public void restore(Memento memento) {
this.state = memento.getState(); // 이전 상태 복원
}
}
// 상태 저장 관리자
class Caretaker {
private Stack<Memento> history = new Stack<>();
public void save(Memento m) {
history.push(m); // 상태 저장
}
public Memento undo() {
return history.pop(); // 이전 상태 반환
}
}
텍스트 편집기의 undo 기능, 게임의 저장/로드, 이미지 편집 도구의 작업 이력 저장 등에 활용된다. 내부 상태를 외부에 노출하지 않고도 상태 복원이 가능한 것이 장점이다.
객체 구조를 변경하지 않고 새로운 기능을 추가할 수 있도록 하는 패턴이다. 방문자 객체는 요소를 방문하며 요소의 타입에 따라 다른 연산을 수행한다. double dispatch를 활용해 다양한 연산을 유연하게 지원할 수 있다.
// 방문자 인터페이스
interface Visitor {
void visit(Book book);
void visit(Fruit fruit);
}
// 방문 가능한 요소 인터페이스
interface ItemElement {
void accept(Visitor visitor);
}
// 책 요소 구현
class Book implements ItemElement {
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
// 과일 요소 구현
class Fruit implements ItemElement {
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
// 방문자 구현체
class ShoppingCartVisitor implements Visitor {
public void visit(Book book) {
System.out.println("책 계산");
}
public void visit(Fruit fruit) {
System.out.println("과일 계산");
}
}
컴파일러의 AST(구문 트리) 분석, 파일 시스템 탐색, 세금 계산 등 다양한 요소를 순회하며 타입별로 다른 처리를 할 때 유용하다. 요소 객체의 수정 없이 기능을 추가할 수 있다는 점에서 확장성 측면에서 유리하다.
언어의 문법 규칙을 클래스로 표현하고 해석 기능을 제공하는 패턴이다. 간단한 언어나 명령어를 해석하고 실행하는 구조를 만들 때 사용된다. 문법 규칙을 트리 구조로 표현하여 문장을 해석한다.
// 표현식 인터페이스
interface Expression {
boolean interpret(String context);
}
// 단말 표현식 (기본 해석 단위)
class TerminalExpression implements Expression {
private String data;
public TerminalExpression(String data) {
this.data = data;
}
public boolean interpret(String context) {
return context.contains(data);
}
}
// OR 연산 표현식
class OrExpression implements Expression {
private Expression expr1;
private Expression expr2;
public OrExpression(Expression expr1, Expression expr2) {
this.expr1 = expr1;
this.expr2 = expr2;
}
public boolean interpret(String context) {
return expr1.interpret(context) || expr2.interpret(context);
}
}
간단한 도메인 전용 언어(DSL), 수식 계산기, SQL 파서 정규식 해석기 등에 활용된다. 문법 트리를 객체 구조로 표현하여 다양한 해석 방법을 구현할 수 있다는 것이 특징이다.
행위 패턴은 객체 간 협력과 책임 분산에 초점을 맞춘 패턴군으로 조건문이 복잡하게 얽히는 문제를 해결하고 유연하고 확장 가능한 구조를 설계할 수 있게 해준다. 특히 Strategy, State, Command 패턴처럼 런타임에서 행위를 캡슐화하거나 교체할 수 있는 구조는 유지보수성과 확장성을 높이는 데 매우 효과적이었다. Observer나 Mediator 같은 패턴은 객체 간의 결합도를 낮추고 이벤트 기반 설계나 중재 구조에 활용할 수 있어 실무에도 자주 적용된다. Visitor나 Interpreter는 사용 빈도는 낮지만 복잡한 구조나 DSL 해석에서 명확한 역할 분리로 강력한 확장 포인트를 제공한다는 점이 인상적이었다. 각 패턴은 단순히 구조적 형태가 아니라 상황에 따른 '변화 대응 전략'이라는 점에서 이해하는 것이 중요하다고 느꼈다.
참고