행위 패턴 핵심 정리 — Observer부터 Chain까지

2026-05-03AWS SAA-C03 스터디

디자인 패턴 핵심 정리 시리즈 4편. 행위 패턴 9가지(Memento·Observer·Strategy·Command·Template Method·Iterator·State·Mediator·Chain of Responsibility)를 게임 세이브·구독 알림·결제 선택·자판기 모드·회의 사회자·결재 라인 비유로 풀어가며, 각 패턴의 위반·준수 코드와 자주 빠지는 함정 7가지를 처음 보는 사람도 따라올 수 있게 친절하게 풀어쓴 4편.

📚 디자인 패턴 핵심 정리 · 4편 / 14편 — Observer부터 Chain까지

이 글은 디자인 패턴 핵심 정리 시리즈의 네 번째 편입니다. 1편에서 SOLID 원칙, 2편에서 생성 패턴(객체를 어떻게 만드느냐), 3편에서 구조 패턴(객체를 어떻게 조합하느냐)을 다뤘는데, 마지막 패턴 분류인 행위 패턴은 한 단계 더 안쪽에 들어가서 "객체들이 서로 어떻게 통신하고 책임을 나누느냐" 를 풀어내요.

행위 패턴은 GoF 23가지 중 11가지를 차지하는 가장 큰 무리입니다. 이 글에서는 자주 만나는 Memento · Observer · Strategy · Command · Template Method · Iterator · State · Mediator · Chain of Responsibility 아홉 가지를 정리해요. 게임 세이브 포인트, 유튜브 알림, 결제 방법 선택, 자판기 동전·동작 모드, 회의 사회자 — 모두 행위 패턴의 사촌입니다.

왜 행위 패턴이 처음엔 어렵게 느껴질까요

이유는 네 가지예요.

첫째, 개수가 많아서 한 번에 외우기 어려워요. 11가지 중 9가지를 보는데, 이름이 비슷비슷해 보입니다. Observer·Mediator·Strategy·State — 다 "객체 간 통신"으로 묶이는데 어디서 어떻게 다른지가 잘 안 잡혀요.

둘째, Strategy와 State가 거의 똑같이 생겼습니다. 둘 다 인터페이스에 행위를 위임하고 컨텍스트가 그걸 보관해요. 코드만 보면 차이가 거의 없습니다. 면접 단골 질문이 되는 이유예요.

셋째, Observer와 Mediator도 헷갈려요. 둘 다 객체들이 직접 통신하지 않게 하는 패턴이라 모양이 비슷합니다.

넷째, Memento·Command·Iterator처럼 한 패턴 안에 여러 역할이 등장하는 패턴은 머리에 잘 안 들어와요. Memento만 봐도 Originator·Memento·Caretaker 세 역할이 나오고, Command는 Invoker·Command·Receiver가 나옵니다. 역할이 많을수록 첫 인상이 복잡해요.

해결법은 한 가지예요. 각 패턴을 일상 비유 한 줄로 묶고, 헷갈리는 페어(Strategy vs State, Observer vs Mediator)를 언제 어느 쪽을 쓸지의 차이로만 외우면 갑자기 명확해집니다. 이 글이 그 비유와 차이를 따라 아홉 가지를 차례로 풀어 갑니다.

아홉 가지 행위 패턴을 한 줄로 묶기

본격 설명에 들어가기 전에 아홉 가지 행위 패턴을 일상 비유로 한 줄씩 묶어 두고 시작할게요. 시험 직전에 이 표만 다시 봐도 70%는 떠오릅니다.

패턴비유핵심
Memento"게임 세이브 포인트"상태 스냅샷 저장·복원
Observer"구독·알림"상태 변경 시 자동 통지
Strategy"결제 방법 골라쓰기"알고리즘을 런타임에 교체
Command"명령서 한 장"요청을 객체로 캡슐화
Template Method"정해진 순서, 세부는 자유"알고리즘 골격 + 단계 위임
Iterator"책장 한 칸씩"컬렉션 내부 노출 없이 순회
State"자판기 모드 전환"상태에 따라 행동 변경
Mediator"회의 사회자"객체 간 직접 통신 제거
Chain of Responsibility"결재 라인"처리 책임을 여러 핸들러에

아홉 가지 모두 "객체들 간 통신과 책임 분배"에 관한 답이에요. 자세한 사양은 Refactoring.Guru의 행위 패턴 가이드에서도 같이 보면 좋아요.

Memento — 게임 세이브 포인트

"객체의 내부 상태를 외부에 노출하지 않으면서 캡처하고, 나중에 그 상태로 복원할 수 있게 한다."

회사 비유로 — 게임 세이브 포인트예요. 보스전 직전에 세이브해두고, 죽으면 그 시점으로 다시 돌아와요. 게임 내부 상태(체력·아이템·위치)를 외부가 직접 만지지 않으면서도 스냅샷을 찍어 보관할 수 있게 해주는 패턴.

세 가지 참여자가 있어요.

  • Originator — 상태를 가진 원본 객체 (텍스트 에디터)
  • Memento — 상태 스냅샷을 저장하는 객체
  • Caretaker — 메멘토를 관리하는 보관자

텍스트 에디터 Undo 구현

// Memento — 상태 스냅샷 (불변)
public class EditorMemento {
    private final String content;

    public EditorMemento(String content) {
        this.content = content;
    }

    public String getContent() {
        return content;
    }
}

// Originator — 상태를 가지고 저장·복원할 수 있는 텍스트 에디터
public class TextEditor {
    private String content;

    public TextEditor(String initialContent) {
        this.content = initialContent;
    }

    public void type(String text) {
        this.content += text;
    }

    public String getContent() { return content; }

    // 현재 상태를 메멘토로 저장
    public EditorMemento save() {
        return new EditorMemento(content);
    }

    // 메멘토로부터 상태 복원
    public void restore(EditorMemento memento) {
        this.content = memento.getContent();
    }
}

// Caretaker — 메멘토 보관 (스택으로 히스토리 관리)
// 메멘토 내용에는 직접 접근 안 함 (캡슐화 유지)
public class Caretaker {
    private Stack<EditorMemento> history = new Stack<>();

    public void saveState(TextEditor editor) {
        history.push(editor.save());
    }

    public void undo(TextEditor editor) {
        if (!history.isEmpty()) {
            EditorMemento previousState = history.pop();
            editor.restore(previousState);
        }
    }
}

// 클라이언트
public class EditorApp {
    public static void main(String[] args) {
        TextEditor editor = new TextEditor("Hello");
        Caretaker caretaker = new Caretaker();

        caretaker.saveState(editor);

        editor.type(", World");
        caretaker.saveState(editor);

        editor.type("!!!");
        // 현재: "Hello, World!!!"

        caretaker.undo(editor);
        // 복원: "Hello, World"

        caretaker.undo(editor);
        // 복원: "Hello"
    }
}

여기서 시험 함정이 하나 있어요. 모든 상태를 매번 저장하면 메모리가 무한정 커집니다. 텍스트 에디터에서 키 누를 때마다 저장하면 며칠만 써도 GB 단위가 돼요. 히스토리 크기에 제한을 두거나 저장 빈도를 조절해야 합니다.

// 히스토리 크기 제한
public class LimitedCaretaker {
    private static final int MAX_HISTORY = 10;
    private Deque<EditorMemento> history = new ArrayDeque<>();

    public void saveState(TextEditor editor) {
        if (history.size() >= MAX_HISTORY) {
            history.removeFirst(); // 가장 오래된 것 제거
        }
        history.addLast(editor.save());
    }
}

> 한 줄 정리 — Memento는 "게임 세이브 포인트". Originator·Memento·Caretaker 세 역할로 캡슐화 유지하면서 상태 복원.

Observer — 구독·알림

"하나의 객체(Subject) 상태가 변경되면 그 객체에 의존하는 모든 객체(Observer)들이 자동으로 알림을 받는다."

회사 비유로 — 유튜브 채널 구독·알림이에요. 채널(Subject)이 새 영상을 올리면(상태 변경) 모든 구독자(Observer)가 알림을 받아요. 채널은 구독자 한 명 한 명이 누구인지 신경 쓸 필요 없이 "알림 발송"만 하면 됩니다. 발행-구독(Publish-Subscribe) 모델의 기초.

날씨 스테이션 예시

// Observer 인터페이스
public interface Observer {
    void update(float temperature);
}

// Subject 인터페이스
public interface Subject {
    void addObserver(Observer observer);
    void detach(Observer observer);
    void notifyObservers();
}

// ConcreteSubject — 상태를 가지고 변경 시 옵저버에게 알리는 날씨 스테이션
public class WeatherStation implements Subject {
    private float temperature;
    private List<Observer> observerList = new ArrayList<>();

    @Override
    public void addObserver(Observer observer) {
        observerList.add(observer);
    }

    @Override
    public void detach(Observer observer) {
        observerList.remove(observer);
    }

    @Override
    public void notifyObservers() {
        for (Observer observer : observerList) {
            observer.update(this.temperature);
        }
    }

    public void setTemperature(float temperature) {
        System.out.println("Weather Station: Temperature changed to " + temperature + "°C");
        this.temperature = temperature;
        notifyObservers(); // 자동으로 모든 구독자에게 알림
    }
}

// ConcreteObserver 1 — 온도 디스플레이
public class TemperatureDisplay implements Observer {
    private String displayName;

    public TemperatureDisplay(String displayName) {
        this.displayName = displayName;
    }

    @Override
    public void update(float temperature) {
        System.out.println(displayName + " Display: " + temperature + "°C");
    }
}

// ConcreteObserver 2 — 화씨로 변환하여 표시
public class FahrenheitConverter implements Observer {
    @Override
    public void update(float temperature) {
        float fahrenheit = temperature * 9 / 5 + 32;
        System.out.println("Converter: " + temperature + "°C = " + fahrenheit + "°F");
    }
}

// ConcreteObserver 3 — 경고 시스템
public class AlertSystem implements Observer {
    private float threshold;

    public AlertSystem(float threshold) {
        this.threshold = threshold;
    }

    @Override
    public void update(float temperature) {
        if (temperature > threshold) {
            System.out.println("ALERT: Temperature " + temperature
                              + "°C exceeds threshold " + threshold + "°C!");
        }
    }
}

// 클라이언트
public class WeatherApp {
    public static void main(String[] args) {
        WeatherStation station = new WeatherStation();

        Observer display1 = new TemperatureDisplay("Seoul");
        Observer display2 = new TemperatureDisplay("Busan");
        Observer converter = new FahrenheitConverter();
        Observer alert = new AlertSystem(35.0f);

        station.addObserver(display1);
        station.addObserver(display2);
        station.addObserver(converter);
        station.addObserver(alert);

        // 온도 변경 → 모든 구독자에게 자동 알림
        station.setTemperature(25.0f);
        station.setTemperature(38.0f); // 경고 발동
    }
}

여기서 정말 중요한 시험 함정 — Observer가 Subject를 수정하고 그 수정이 다시 알림을 발송하면 무한 루프가 발생합니다.

// 위험 — Observer 안에서 Subject 상태 변경
public class DangerousObserver implements Observer {
    private WeatherStation station;

    @Override
    public void update(float temperature) {
        if (temperature > 40) {
            // station.setTemperature(40); // 무한 루프!
        }
    }
}

notifyObservers() 도중에 Subject의 상태를 변경하는 건 금지예요.

> 한 줄 정리 — Observer는 "구독·알림". Subject 상태 변경 시 자동 통지. 무한 루프 함정 주의.

Strategy — 결제 방법 골라쓰기

"알고리즘 군(family)을 정의하고 각각을 캡슐화하여 서로 교환 가능하게 만든다."

회사 비유로 — 결제 방법 골라쓰기예요. 같은 "결제"라는 동작에 대해 신용카드·페이팔·암호화폐 같은 여러 알고리즘이 있고, 사용자가 런타임에 선택해요. if-else로 분기하던 코드를 다형성으로 깔끔하게 대체합니다.

결제 시스템 구현

// Strategy 인터페이스
public interface PaymentStrategy {
    void processPayment(double amount);
    String getPaymentType();
}

// ConcreteStrategy 1
public class CreditCardPayment implements PaymentStrategy {
    private String cardNumber;
    private String cardHolder;

    public CreditCardPayment(String cardNumber, String cardHolder) {
        this.cardNumber = cardNumber;
        this.cardHolder = cardHolder;
    }

    @Override
    public void processPayment(double amount) {
        System.out.println("Processing credit card payment of $" + amount);
    }

    @Override
    public String getPaymentType() { return "Credit Card"; }
}

// ConcreteStrategy 2
public class PayPalPayment implements PaymentStrategy {
    private String email;

    public PayPalPayment(String email) { this.email = email; }

    @Override
    public void processPayment(double amount) {
        System.out.println("Processing PayPal payment of $" + amount);
    }

    @Override
    public String getPaymentType() { return "PayPal"; }
}

// ConcreteStrategy 3
public class CryptoPayment implements PaymentStrategy {
    private String walletAddress;

    public CryptoPayment(String walletAddress) { this.walletAddress = walletAddress; }

    @Override
    public void processPayment(double amount) {
        System.out.println("Processing crypto payment of $" + amount);
    }

    @Override
    public String getPaymentType() { return "Cryptocurrency"; }
}

// Context — 전략을 사용하는 결제 서비스
public class PaymentService {
    private PaymentStrategy strategy;

    public void setPaymentStrategy(PaymentStrategy strategy) {
        this.strategy = strategy;
        System.out.println("Payment strategy changed to: " + strategy.getPaymentType());
    }

    public void pay(double amount) {
        if (strategy == null) {
            throw new IllegalStateException("Payment strategy not set!");
        }
        strategy.processPayment(amount);
    }
}

// 클라이언트 — 런타임에 전략 교체
public class ShoppingCart {
    public static void main(String[] args) {
        PaymentService service = new PaymentService();

        service.setPaymentStrategy(new CreditCardPayment("4111-...", "Alice"));
        service.pay(99.99);

        // 런타임에 교체
        service.setPaymentStrategy(new PayPalPayment("alice@example.com"));
        service.pay(49.99);

        service.setPaymentStrategy(new CryptoPayment("0x1234..."));
        service.pay(200.00);
    }
}

Observer vs Strategy

항목ObserverStrategy
목적상태 변경 알림알고리즘 교체
관계1:N1:1
변경 시점Subject 상태 변경 시클라이언트가 명시적으로 교체
통신 방향Subject → Observer (push)Context → Strategy (delegation)

여기서 시험 함정이 하나 있어요. Strategy는 if-else 제거 패턴의 대표 도구입니다. 결제·정렬·압축·할인 정책 — 분기로 알고리즘을 고르는 코드가 보이면 거의 항상 Strategy로 다시 짤 수 있어요.

> 한 줄 정리 — Strategy는 "알고리즘 골라쓰기". if-else 분기를 다형성으로 대체.

Command — 명령서 한 장

"요청(request)을 객체로 캡슐화하여 요청을 매개변수화하고, 요청을 큐에 저장하거나 로그로 기록하고, 되돌릴 수 있게 한다."

회사 비유로 — 결재 요청서 한 장이에요. "이 작업 해주세요"라는 요청을 종이 한 장(객체)으로 만들면, 그 종이를 큐에 쌓아 두거나, 나중에 다시 실행하거나, 취소(undo)할 수 있어요. 발신자(Invoker)와 수신자(Receiver)가 직접 만나지 않고 명령서 한 장을 통해 통신합니다.

텍스트 에디터 버튼 구현

// Command 인터페이스
public interface Command {
    void execute();
    void undo();
}

// Receiver — 실제 작업을 수행
public class TextEditorReceiver {
    private StringBuilder text = new StringBuilder();

    public void makeBold() {
        text.append("<b>");
        System.out.println("Text made bold: " + text);
    }

    public void makeItalic() {
        text.append("<i>");
        System.out.println("Text made italic: " + text);
    }

    public void undoBold() {
        if (text.toString().endsWith("<b>")) {
            text.delete(text.length() - 3, text.length());
        }
    }

    public void undoItalic() {
        if (text.toString().endsWith("<i>")) {
            text.delete(text.length() - 3, text.length());
        }
    }

    public String getText() { return text.toString(); }
}

// ConcreteCommand 1 — Bold
public class BoldCommand implements Command {
    private TextEditorReceiver editor;

    public BoldCommand(TextEditorReceiver editor) {
        this.editor = editor;
    }

    @Override public void execute() { editor.makeBold(); }
    @Override public void undo() { editor.undoBold(); }
}

// ConcreteCommand 2 — Italic
public class ItalicCommand implements Command {
    private TextEditorReceiver editor;

    public ItalicCommand(TextEditorReceiver editor) {
        this.editor = editor;
    }

    @Override public void execute() { editor.makeItalic(); }
    @Override public void undo() { editor.undoItalic(); }
}

// Invoker — 명령을 실행하는 버튼
public class Button {
    private Command command;
    private String name;

    public Button(String name) { this.name = name; }

    public void setCommand(Command command) { this.command = command; }

    public void click() {
        System.out.println(name + " button clicked");
        if (command != null) command.execute();
    }
}

// CommandHistory — Undo를 위한 히스토리
public class CommandHistory {
    private Stack<Command> history = new Stack<>();

    public void execute(Command command) {
        command.execute();
        history.push(command);
    }

    public void undo() {
        if (!history.isEmpty()) {
            Command lastCommand = history.pop();
            lastCommand.undo();
        }
    }
}

// 클라이언트
public class EditorUI {
    public static void main(String[] args) {
        TextEditorReceiver editor = new TextEditorReceiver();
        CommandHistory history = new CommandHistory();

        history.execute(new BoldCommand(editor));
        history.execute(new ItalicCommand(editor));
        history.execute(new BoldCommand(editor));

        // Undo 두 번 — 히스토리 역순으로
        history.undo();
        history.undo();
    }
}

여기서 시험 함정이 하나 있어요. Command 패턴은 Undo·큐잉·로깅·매크로(여러 명령의 시퀀스) 같은 기능이 필요할 때만 진짜 가치가 나옵니다. 단순한 메서드 호출까지 모두 Command 객체로 만들면 클래스 수가 폭발해요. 신호가 보일 때만 꺼내 쓰는 패턴.

> 한 줄 정리 — Command는 "요청을 객체로 캡슐화". Undo·큐잉·로깅·매크로 신호가 있을 때.

Template Method — 정해진 순서, 세부는 자유

"알고리즘의 골격(template)을 상위 클래스에서 정의하고, 알고리즘의 일부 단계를 서브클래스에서 구현한다."

회사 비유로 — 공통 양식이 있는 보고서 작성이에요. 회사 전체 보고서 양식은 똑같은데(개요·본론·결론) 각 부서마다 본론 내용이 달라요. 양식은 상위 클래스가 강제하고, 부서별 본론은 서브클래스에서 채우는 식.

핵심 원칙은 헐리우드 원칙(Hollywood Principle) — "Don't call us, we'll call you" (우리한테 전화하지 마, 우리가 너한테 전화할 테니까). 프레임워크가 코드를 호출하는 구조.

데이터 파서 예시

// 추상 클래스 — 알고리즘 골격
public abstract class DataParser {
    // 템플릿 메서드 — 알고리즘 흐름 정의 (final로 오버라이드 차단)
    public final void parse() {
        openFile();     // 공통
        parseData();    // 가변 (서브클래스 구현)
        closeFile();    // 공통
    }

    // 공통 구현 (훅 메서드) — 필요시 오버라이드 가능
    protected void openFile() {
        System.out.println("Opening file...");
    }

    protected void closeFile() {
        System.out.println("Closing file...");
    }

    // 추상 메서드 — 반드시 서브클래스에서 구현
    protected abstract void parseData();
}

// ConcreteClass 1
public class CSVParser extends DataParser {
    @Override
    protected void parseData() {
        System.out.println("Parsing CSV data: splitting by commas");
    }
}

// ConcreteClass 2 — openFile 오버라이드로 추가 처리
public class XMLParser extends DataParser {
    @Override
    protected void parseData() {
        System.out.println("Parsing XML data: reading tags");
    }

    @Override
    protected void openFile() {
        super.openFile(); // 부모 호출
        System.out.println("Validating XML schema...");
    }
}

// ConcreteClass 3
public class PDFParser extends DataParser {
    @Override
    protected void parseData() {
        System.out.println("Parsing PDF data: extracting text and images");
    }

    @Override
    protected void closeFile() {
        System.out.println("Releasing PDF resources...");
        super.closeFile();
    }
}

// 클라이언트
public class ParserApp {
    public static void main(String[] args) {
        new CSVParser().parse();
        new XMLParser().parse();
        new PDFParser().parse();

        // 모두 openFile → parseData → closeFile 순서를 따름
        // 하지만 parseData()의 동작은 각자 다름
    }
}

Template Method vs Strategy

항목Template MethodStrategy
메커니즘상속(Inheritance)컴포지션(Composition)
변형 위치알고리즘의 일부 단계전체 알고리즘
교체 시점컴파일 타임 (상속 구조)런타임 (객체 주입)
관계IS-AHAS-A

여기서 시험 함정이 하나 있어요. Template Method는 상속을 사용하므로 자바의 단일 상속 제약에 걸려요. 여러 알고리즘 변형이 필요하다면 Strategy 패턴이 더 유연합니다. 또 추상 메서드는 반드시 구현해야 하므로 강제성도 큽니다.

> 한 줄 정리 — Template Method는 "정해진 순서, 세부는 자유". 헐리우드 원칙으로 프레임워크가 코드를 호출.

Iterator — 책장 한 칸씩

"컬렉션의 내부 구조를 노출하지 않고 요소들을 순차적으로 접근할 수 있는 방법을 제공한다."

회사 비유로 — 책장 한 칸씩 꺼내 읽기예요. 책장이 어떤 구조인지(배열·리스트·트리) 몰라도, "다음 책 주세요"·"더 있어요?"라고 물으면 한 권씩 꺼내 줘요. 클라이언트는 컬렉션의 내부 구조에 의존하지 않습니다.

Java의 IteratorIterable 인터페이스가 이 패턴의 표준 구현이에요. for-each 루프가 그 위에서 돌아갑니다.

책 컬렉션 예시

// Iterator 인터페이스
public interface Iterator<T> {
    boolean hasNext();
    T next();
}

// Iterable 인터페이스 — 이터레이터를 만들 수 있는 컬렉션
public interface Iterable<T> {
    Iterator<T> createIterator();
}

// 도메인 클래스
public class Book {
    private String title;
    private String author;
    private String isbn;

    public Book(String title, String author, String isbn) {
        this.title = title;
        this.author = author;
        this.isbn = isbn;
    }

    public String getTitle() { return title; }
    public String getAuthor() { return author; }
}

// ConcreteIterator
public class BookIterator implements Iterator<Book> {
    private List<Book> books;
    private int position = 0;

    public BookIterator(List<Book> books) {
        this.books = books;
    }

    @Override
    public boolean hasNext() {
        return position < books.size();
    }

    @Override
    public Book next() {
        if (!hasNext()) {
            throw new NoSuchElementException("No more books");
        }
        Book book = books.get(position);
        position++;
        return book;
    }
}

// ConcreteCollection
public class BookCollection implements Iterable<Book> {
    private List<Book> books = new ArrayList<>();

    public void addBook(Book book) {
        books.add(book);
    }

    @Override
    public Iterator<Book> createIterator() {
        return new BookIterator(books);
    }
}

// 클라이언트
public class LibraryApp {
    public static void main(String[] args) {
        BookCollection collection = new BookCollection();
        collection.addBook(new Book("Clean Code", "Robert C. Martin", "ISBN-001"));
        collection.addBook(new Book("Design Patterns", "Gang of Four", "ISBN-002"));
        collection.addBook(new Book("Refactoring", "Martin Fowler", "ISBN-003"));

        Iterator<Book> iterator = collection.createIterator();
        while (iterator.hasNext()) {
            Book book = iterator.next();
            // book 처리
        }
    }
}

여기서 정말 중요한 시험 함정 — Iterator로 순회하는 도중 컬렉션을 수정하면 ConcurrentModificationException이 발생합니다.

// 잘못된 코드 — 순회 중 수정
for (Book book : books) {
    if (book.getTitle().equals("Old Book")) {
        books.remove(book); // ConcurrentModificationException!
    }
}

// 올바른 코드 — Iterator.remove() 사용
Iterator<Book> it = books.iterator();
while (it.hasNext()) {
    Book book = it.next();
    if (book.getTitle().equals("Old Book")) {
        it.remove(); // 안전
    }
}

> 한 줄 정리 — Iterator는 "내부 구조 노출 없이 순회". Java for-each가 이 패턴 위에 돌아간다. 순회 중 수정 금지.

State — 자판기 모드 전환

"객체의 내부 상태에 따라 행동이 달라진다. 마치 객체의 클래스가 바뀐 것처럼 동작한다."

회사 비유로 — 자판기의 동전·동작 모드예요. 자판기는 "동전 없음"·"동전 있음"·"상품 선택됨"·"배출 중" 같은 여러 상태를 거치는데, 같은 버튼을 눌러도 현재 상태에 따라 동작이 달라요. 동전 없을 때 상품 버튼을 누르면 무시되고, 동전 있을 때 같은 버튼을 누르면 상품이 나오죠.

if-else로 상태 분기하는 코드를 다형성으로 대체합니다.

네비게이션 교통수단 모드 예시

// State 인터페이스
public interface TransportationMode {
    int calculateEta(int distanceKm);
    String getDirection(String destination);
    String getModeName();
}

// ConcreteState 1 — 도보
public class Walking implements TransportationMode {
    @Override
    public int calculateEta(int distanceKm) {
        return (int) (distanceKm / 5.0 * 60); // 시속 5km
    }

    @Override
    public String getDirection(String destination) {
        return "Walk to " + destination + " via pedestrian path";
    }

    @Override
    public String getModeName() { return "Walking"; }
}

// ConcreteState 2 — 자전거
public class Cycling implements TransportationMode {
    @Override
    public int calculateEta(int distanceKm) {
        return (int) (distanceKm / 15.0 * 60); // 시속 15km
    }

    @Override
    public String getDirection(String destination) {
        return "Cycle to " + destination + " via bike lane";
    }

    @Override
    public String getModeName() { return "Cycling"; }
}

// ConcreteState 3 — 차량
public class Driving implements TransportationMode {
    @Override
    public int calculateEta(int distanceKm) {
        return (int) (distanceKm / 40.0 * 60); // 시속 40km
    }

    @Override
    public String getDirection(String destination) {
        return "Drive to " + destination + " via main road";
    }

    @Override
    public String getModeName() { return "Driving"; }
}

// Context — 현재 상태에 행동 위임
public class DirectionService {
    private TransportationMode transportationMode;

    public DirectionService() {
        this.transportationMode = new Walking();
    }

    public void setTransportationMode(TransportationMode mode) {
        this.transportationMode = mode;
    }

    public int getETA(int distanceKm) {
        return transportationMode.calculateEta(distanceKm); // 위임
    }

    public String getDirection(String destination) {
        return transportationMode.getDirection(destination);
    }
}

// 클라이언트
public class NavigationApp {
    public static void main(String[] args) {
        DirectionService nav = new DirectionService();

        nav.getDirection("Park"); // Walking
        nav.setTransportationMode(new Cycling());
        nav.getDirection("Park"); // Cycling
        nav.setTransportationMode(new Driving());
        nav.getDirection("Park"); // Driving
    }
}

Strategy vs State — 가장 헷갈리는 페어

코드만 보면 거의 똑같이 생겼어요. 차이점은 의도에 있습니다.

항목StrategyState
목적알고리즘을 교체 가능하게상태에 따라 행동 변경
전환 주체클라이언트가 명시적으로 교체상태 전환 로직이 내부에 있을 수 있음
상태 보유Context는 상태 인식 안 함Context가 현재 상태를 의식
비유결제 방법 선택신호등·자판기

여기서 정말 중요한 시험 함정 — Strategy는 "선택"의 문제, State는 "전환"의 문제예요. 결제 방식은 사용자가 선택하지 시간 흐름에 따라 자동 전환되지 않아요. 반면 자판기 모드는 동전 투입·상품 선택이라는 이벤트로 자동 전환됩니다.

> 한 줄 정리 — State는 "자판기 모드 전환". Strategy와 코드는 비슷, 의도가 다름.

Mediator — 회의 사회자

"객체들이 서로 직접 통신하는 대신 중재자(Mediator)를 통해 통신하도록 한다."

회사 비유로 — 회의 사회자예요. 8명이 회의실에 있는데 모두가 동시에 모두에게 말하면 28개의 통신선이 생겨요. 사회자 한 명이 가운데 서서 발언을 정리하면 8개 선만 있으면 됩니다. 다대다(N×N)가 일대다(N)로 줄어드는 효과.

채팅방, 항공 관제 시스템, 이벤트 버스 — 모두 Mediator의 모양입니다.

채팅방 예시

// Mediator 인터페이스
public interface ChatMediator {
    void sendMessage(String message, ChatUser sender);
    void addUser(ChatUser user);
}

// ConcreteMediator
public class ChatRoom implements ChatMediator {
    private List<ChatUser> users = new ArrayList<>();

    @Override
    public void addUser(ChatUser user) {
        users.add(user);
    }

    @Override
    public void sendMessage(String message, ChatUser sender) {
        for (ChatUser user : users) {
            if (user != sender) {
                user.receiveMessage(message, sender);
            }
        }
    }
}

// Colleague — 채팅 참여자
public class ChatUser {
    private String name;
    private ChatMediator chatMediator;

    public ChatUser(String name, ChatMediator mediator) {
        this.name = name;
        this.chatMediator = mediator;
    }

    public String getName() { return name; }

    // 메시지 발송 — 중재자에게 위임 (다른 사용자 직접 참조 X)
    public void sendMessage(String message) {
        chatMediator.sendMessage(message, this);
    }

    public void receiveMessage(String message, ChatUser sender) {
        System.out.println("[" + name + " from " + sender.getName() + "]: " + message);
    }
}

// 클라이언트
public class ChatApp {
    public static void main(String[] args) {
        ChatRoom room = new ChatRoom();

        ChatUser alice = new ChatUser("Alice", room);
        ChatUser bob = new ChatUser("Bob", room);
        ChatUser charlie = new ChatUser("Charlie", room);

        room.addUser(alice);
        room.addUser(bob);
        room.addUser(charlie);

        alice.sendMessage("Hello everyone!"); // Bob과 Charlie가 받음
        bob.sendMessage("Hi Alice!");          // Alice와 Charlie가 받음

        // 객체들이 서로를 직접 참조하지 않음 → 느슨한 결합
    }
}

Mediator vs Observer

항목MediatorObserver
목적다대다 통신을 일대다로 단순화상태 변경 자동 알림
통신양방향 (사용자 → 중재자 → 사용자)단방향 (Subject → Observer)
중심중재자가 모든 통신 라우팅발행-구독 모델
비유회의 사회자·항공 관제유튜브 채널 구독

여기서 시험 함정이 하나 있어요. 모든 통신이 Mediator를 통하면 Mediator가 모든 것을 알고 모든 것을 하는 "God Object"가 됩니다. Mediator는 단순히 메시지를 라우팅하는 역할만 해야 하고, 비즈니스 로직은 각 Colleague가 가져야 해요. Facade에서 본 함정이 그대로 재현됩니다.

> 한 줄 정리 — Mediator는 "회의 사회자". 다대다 통신을 일대다로. God Object 함정 주의.

Chain of Responsibility — 결재 라인

"요청을 처리할 수 있는 객체들의 체인을 만들어, 각 객체가 처리할 수 있으면 처리하고 아니면 다음 객체로 넘긴다."

회사 비유로 — 결재 라인이에요. 대리가 처리할 수 있으면 처리하고, 아니면 과장에게, 과장도 안 되면 부장에게, 부장도 안 되면 임원에게. 각 단계가 자기 권한 안에서만 처리하고, 안 되면 다음 단계로 넘기는 구조.

스프링의 서블릿 필터 체인, Java 예외 처리(try/catch), 미들웨어 — 모두 Chain of Responsibility의 모양입니다.

휴가 결재 예시

// Handler 인터페이스
public abstract class LeaveApprover {
    protected LeaveApprover next;

    public void setNext(LeaveApprover next) {
        this.next = next;
    }

    // 템플릿 메서드 — 처리 가능하면 처리, 아니면 다음으로
    public void processRequest(int days) {
        if (canApprove(days)) {
            approve(days);
        } else if (next != null) {
            next.processRequest(days);
        } else {
            System.out.println("No approver can handle " + days + " days");
        }
    }

    protected abstract boolean canApprove(int days);
    protected abstract void approve(int days);
}

// ConcreteHandler 1 — 대리 (3일 이하)
public class TeamLeader extends LeaveApprover {
    @Override
    protected boolean canApprove(int days) { return days <= 3; }

    @Override
    protected void approve(int days) {
        System.out.println("Team Leader approved " + days + " days");
    }
}

// ConcreteHandler 2 — 과장 (7일 이하)
public class Manager extends LeaveApprover {
    @Override
    protected boolean canApprove(int days) { return days <= 7; }

    @Override
    protected void approve(int days) {
        System.out.println("Manager approved " + days + " days");
    }
}

// ConcreteHandler 3 — 부장 (15일 이하)
public class Director extends LeaveApprover {
    @Override
    protected boolean canApprove(int days) { return days <= 15; }

    @Override
    protected void approve(int days) {
        System.out.println("Director approved " + days + " days");
    }
}

// 클라이언트
public class LeaveApp {
    public static void main(String[] args) {
        // 체인 구성
        LeaveApprover leader = new TeamLeader();
        LeaveApprover manager = new Manager();
        LeaveApprover director = new Director();

        leader.setNext(manager);
        manager.setNext(director);

        // 요청 — 체인의 첫 노드에 던지면 알아서 흘러감
        leader.processRequest(2);   // Team Leader approved
        leader.processRequest(5);   // Manager approved
        leader.processRequest(10);  // Director approved
        leader.processRequest(30);  // No approver can handle
    }
}

여기서 시험 함정이 하나 있어요. 체인 끝까지 가도 처리할 수 없는 경우의 처리를 빼먹기 쉽습니다. 위 예시처럼 "처리할 수 없음" 메시지를 명시적으로 출력하거나, 예외를 던지거나, 기본 처리자(default handler)를 두는 식으로 마무리해야 해요.

또 다른 함정 — 체인이 너무 길어지면 디버깅이 어려워져요. 어느 노드에서 처리됐는지 추적이 힘들 수 있으니 로깅을 충분히 박아둬야 합니다.

> 한 줄 정리 — Chain of Responsibility는 "결재 라인". 처리 가능한 노드 만날 때까지 체인 따라 흘러간다.

아홉 가지 행위 패턴 종합 비교표

아홉 가지를 한 표로 정리하면 이래요. 시험 직전·코드 리뷰 직전에 이 표만 다시 봐도 머리에서 정리됩니다.

패턴목적핵심 참여자대표 예시관련 패턴
Memento상태 저장·복원Originator·Memento·Caretaker게임 세이브·UndoCommand
Observer이벤트 자동 알림Subject·Observer유튜브 구독Mediator
Strategy알고리즘 교체Context·Strategy결제 방식State
Command요청을 객체로 캡슐화Invoker·Command·Receiver에디터 버튼Memento
Template Method알고리즘 골격 + 단계 위임Abstract·Concrete Class데이터 파서Strategy
Iterator컬렉션 순회Iterator·Iterable책 컬렉션Composite
State상태에 따른 행동 변경Context·State자판기·신호등Strategy
Mediator객체 간 직접 통신 제거Mediator·Colleague채팅방Observer
Chain of Responsibility처리 가능한 핸들러까지 흐름Handler·ConcreteHandler결재 라인·필터 체인Command

헷갈리는 페어 정리

아홉 가지 중에서 가장 헷갈리는 페어 세 가지를 한 번 더 짚을게요.

Strategy vs State — 코드는 비슷, 의도가 다름. 결제 방식 선택 = Strategy, 자판기 모드 전환 = State

Observer vs Mediator — 둘 다 객체 간 직접 통신 제거. Observer는 1:N 단방향(구독), Mediator는 N:N을 라우팅(채팅방)

Command vs Memento — 둘 다 Undo에 쓸 수 있음. Command는 "동작 자체"를 저장(역으로 실행), Memento는 "상태 스냅샷"을 저장(직접 복원)

> 한 줄 정리 — 행위 패턴 9가지는 "객체 간 통신과 책임 분배"의 답. 헷갈리는 페어는 의도 차이로 구분.

행위 패턴에서 자주 빠지는 함정 7가지

1. Observer 순환 참조

Observer 안에서 Subject 상태를 변경하면 무한 루프. notifyObservers() 도중 Subject 수정 금지.

2. Memento의 메모리 누수

모든 상태 저장하면 히스토리 폭발. 크기 제한 또는 저장 빈도 조절.

3. Command 패턴의 클래스 폭발

단순한 메서드 호출까지 Command로 만들면 클래스 수 폭발. Undo·큐잉·로깅·매크로 신호가 있을 때만 적용.

4. Template Method의 단일 상속 제약

자바는 단일 상속만 지원. 여러 알고리즘 변형 필요하면 Strategy로.

5. Mediator God Object

모든 비즈니스 로직이 Mediator로 모이면 안티패턴. Mediator는 라우팅만, 비즈니스 로직은 Colleague에.

6. Iterator 동시 수정

순회 중 컬렉션 수정 = ConcurrentModificationException. Iterator.remove() 사용 또는 별도 리스트로 수집 후 일괄 처리.

7. Strategy vs State 혼동

코드는 같아도 의도가 다름. 선택의 문제 = Strategy, 전환의 문제 = State.

행위 패턴 코드 리뷰 체크리스트 — 실전용

Observer 신호

  • 한 객체의 상태 변경이 여러 다른 객체에 영향을 주는가?
  • 발행-구독 모델이 자연스러운 도메인인가?
  • 알림 받을 객체가 동적으로 추가·제거되는가?

Strategy 신호

  • if-else·switch로 알고리즘을 분기하는가?
  • 런타임에 알고리즘을 교체해야 하는가?
  • 각 알고리즘이 독립적으로 캡슐화되는가?

State 신호

  • 객체의 행동이 내부 상태에 따라 달라지는가?
  • 상태 전환 로직이 if-else로 흩어져 있는가?
  • 상태가 자판기·신호등처럼 명확한 사이클을 갖는가?

Command 신호

  • Undo·Redo 기능이 필요한가?
  • 작업을 큐에 쌓거나 로깅·매크로로 만들어야 하는가?
  • 발신자와 수신자를 분리하고 싶은가?

Mediator 신호

  • 객체들이 서로 다대다로 직접 참조하는가?
  • 한 객체 변경이 다른 여러 객체에 영향을 주는가?
  • 통신 라우팅이 분산되어 있어 추적이 어려운가?

Chain of Responsibility 신호

  • 처리 책임이 여러 단계에 분산되어 있는가? (대리·과장·부장)
  • 요청 종류에 따라 다른 핸들러가 처리해야 하는가?
  • 핸들러를 동적으로 추가·제거할 수 있어야 하는가?

시험 직전 한 번 더 — 자주 헷갈리는 함정 모음

여기까지가 행위 패턴 4편의 핵심입니다. 시험 직전·코드 리뷰 직전에 다시 펼쳐 볼 수 있게 압축 노트로 마무리할게요.

  • 행위 패턴 9가지 — Memento·Observer·Strategy·Command·Template Method·Iterator·State·Mediator·Chain of Responsibility
  • 모두 "객체들 간 통신과 책임 분배"에 관한 답
  • Memento — 상태 스냅샷 저장·복원. 게임 세이브 비유. Originator·Memento·Caretaker 세 역할
  • Memento 함정 — 모든 상태 저장 시 메모리 폭발. 히스토리 크기 제한 필요
  • Observer — 상태 변경 자동 알림. 유튜브 구독 비유
  • Observer 함정 — notifyObservers() 도중 Subject 수정 = 무한 루프
  • Strategy — 알고리즘 골라쓰기. 결제 방식 비유. if-else 분기를 다형성으로
  • Command — 요청을 객체로 캡슐화. 명령서 비유. Undo·큐잉·로깅·매크로 신호일 때
  • Template Method — 알고리즘 골격 + 세부 위임. 헐리우드 원칙 ("우리가 너한테 전화할게")
  • Template Method 한계 — 단일 상속 제약. 여러 알고리즘 필요하면 Strategy
  • Iterator — 컬렉션 내부 노출 없이 순회. Java for-each가 이 위에서 돌아감
  • Iterator 함정 — 순회 중 수정 = ConcurrentModificationException. Iterator.remove() 사용
  • State — 상태에 따라 행동 변경. 자판기·신호등 비유
  • Strategy vs State — 코드는 비슷, 의도가 다름. 선택 vs 전환
  • Mediator — 다대다 통신을 중재자로 일대다. 회의 사회자 비유
  • Mediator 함정 — God Object 안티패턴. 라우팅만, 비즈니스 로직은 Colleague에
  • Chain of Responsibility — 처리 가능한 핸들러까지 흐름. 결재 라인 비유
  • Chain 함정 — 끝까지 처리 못 했을 때 처리 빼먹기. 기본 처리자 또는 명시적 실패 처리
  • Observer vs Mediator — 1:N 단방향 vs N:N 라우팅
  • Command vs Memento — 동작 저장 vs 상태 저장

시리즈 다른 편

같은 시리즈의 다른 글들도 같은 친절 톤으로 묶어 정리되어 있어요.

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!