학습목표

  • 명령하다!!
  • 행위 객체화(Encapsulate Request)
    • 요청(명령)을 객체로 캡슐화하여 호출자와 수신자를 분리.
  • 호출자-수신자 결합도 낮추기
    • 요청을 수행하는 객체와 요청을 호출하는 객체를 독립적으로 설계 가능.
  • Undo/Redo, 큐잉, 예약 실행 구현
    • 요청 객체를 저장하고, 실행 취소나 재실행이 가능하도록 구조화.
  • 유연한 요청 관리
    • 명령의 실행 순서, 스케줄링, 로깅 등을 중앙 집중적으로 관리 가능.

활용상황

  • GUI 버튼, 메뉴, 액션 처리
    • 버튼 클릭, 메뉴 선택 등 이벤트를 명령 객체로 변환하여 처리.
    • 예: “저장”, “취소”, “복사” 버튼 이벤트 처리.
  • Undo/Redo 기능 구현
    • 명령을 객체로 캡슐화하여 실행 내역 저장 → 취소/재실행 가능.
    • 예: 텍스트 편집기, 그림판.
  • 작업 큐/스케줄링
    • 명령 객체를 큐에 저장하고 순차적으로 실행.
    • 예: 서버 배치 작업, 비동기 작업 처리.
  • 트랜잭션 및 로그 관리
    • 명령 객체를 로그로 남기고, 시스템 복구 시 재실행.
    • 예: 은행 계좌 입출금 트랜잭션 기록.

예제

클라이언트
   │
   ▼
Invoker (RemoteControl) ----> Command (LightOnCommand) ----> Receiver (Light)
                                execute()                        on()
  • Command
public interface Command {
    void execute();
}
  • Receiver
public class Light {

    public void on() {
        System.out.println("Light is ON");
    }

    public void off() {
        System.out.println("Light is OFF");
    }
}
  • ConcreteCommand
public class LightOnCommand implements Command {

    private Light light;

    public LightOnCommand(Light light) {
        this.light = light;
    }

    @Override
    public void execute() {
        light.on();
    }
}

public class LightOffCommand implements Command {

    private Light light;

    public LightOffCommand(Light light) {
        this.light = light;
    }

    @Override
    public void execute() {
        light.off();
    }
}
  • Invoker
public class RemoteControl {
    private Command command;

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

    public void pressButton() {
        command.execute();
    }
}
  • 실행
public class Main {

    public static void main(String[] args) {
        Light livingRoomLight = new Light();

        Command lightOn = new LightOnCommand(livingRoomLight);
        Command lightOff = new LightOffCommand(livingRoomLight);

        RemoteControl remote = new RemoteControl();

        remote.setCommand(lightOn);
        remote.pressButton(); // Light is ON

        remote.setCommand(lightOff);
        remote.pressButton(); // Light is OFF
    }
}

 

  • 클라이언트가 Invoker에 LightOnCommand를 설정.
  • Invoker가 pressButton() → command.execute() 호출.
  • ConcreteCommand(LightOnCommand)가 Receiver(Light). on() 호출.
  • Light가 실제로 켜짐.

마무리

객체를 받아서 명령만하는 구조 같다. 

학습목표

  • 객체 접근 제어 이해
    • 실제 객체(real subject)에 대한 접근을 제어하고, 접근 전에 추가 로직(권한 체크, 로깅 등)을 삽입하는 방법을 학습.
  • 간접 접근(Indirection) 구현 능력
    • 클라이언트가 직접 실제 객체에 접근하지 않고, 프락시를 통해 접근하도록 설계하는 구조 이해.
  • 객체 생성 비용 최적화
    • 실제 객체 생성 비용이 크거나 리소스가 많이 필요할 때, 필요할 때만 생성(lazy initialization)하는 방법 학습.
  • 책임 분리와 캡슐화
    • 접근 제어, 캐싱, 로깅, 트랜잭션 등 부가 기능을 실제 객체와 분리하여 관리.

활용상황

  • 보안 관리(Security Proxy)
    • 접근 권한을 확인하고, 권한이 없는 경우 실제 객체에 접근을 막음.
    • 예: 관리자 전용 API 접근 제한, 파일 접근 권한 제어
  • 지연 로딩(Lazy Initialization / Virtual Proxy)
    • 객체 생성 비용이 큰 경우, 실제 객체 생성은 필요할 때만 수행.
    • 예: 대형 이미지, 데이터베이스 연결 객체, 원격 리소스
  • 로깅/모니터링(Logging Proxy)
    • 메서드 호출 기록, 성능 모니터링, 트래픽 통계 등을 수집.
    • 예: 서비스 호출 시간 측정, API 요청 로그 기록
  • 캐싱(Caching Proxy)
    • 이전에 계산한 결과를 저장하여 성능 최적화.
    • 예: 웹 페이지 캐시, 계산 결과 캐싱
  • 원격 객체 접근(Remote Proxy)
    • 원격 서버의 객체를 로컬 프록시를 통해 접근.
    • 예: RMI(Remote Method Invocation), gRPC 클라이언트

예제

더보기

서비스에 로깅을 프록시 패턴을 이용하여 남기는 기능

  • Service
public interface Service {
    void performTask();
}

 

  • RealService ( 실제 로직 )
public class RealService implements Service {
    @Override
    public void performTask() {
        System.out.println("Task performed.");
    }
}

 

  • LogginServiceProxy ( Proxy )
public class LoggingServiceProxy implements Service {
    private RealService realService;

    public LoggingServiceProxy(RealService realService) {
        this.realService = realService;
    }

    @Override
    public void performTask() {
        long start = System.currentTimeMillis();
        System.out.println("[LOG] Task started at " + start);

        realService.performTask();

        long end = System.currentTimeMillis();
        System.out.println("[LOG] Task finished at " + end + ", duration: " + (end - start) + "ms");
    }
}

 

  • 실행
public class Main {
    public static void main(String[] args) {
        Service service = new LoggingServiceProxy(new RealService());
        service.performTask();
    }
}

- 실제 구현체를 받아서 Logging은 Proxy에서 해당 로그를 남기는 방법

 

마무리

여러 상황에 많이 쓰고 알게 모르게 자주 쓰이는 패턴인 것 같습니다.

학습목표

  • 공유 가능한 객체를 재사용하여 메모리 사용 최적화 원리 이해
  • 내부 상태(공유) / 외부 상태(비공유)의 개념 구분
  • 객체가 대량으로 생성될 때 플라이웨이트가 왜 필요한지 파악
  • 팩토리(캐싱) 방식으로 객체를 관리하는 구조 습득

활용방법

  • 문서에 같은 글자 수천 개 → 폰트 공유
  • 수십만 개 미니언 모델 → Texture/Mesh 공유
  • 건물 아이콘, 마커 공유
  • DB Connection, 상수 문자열 재사용

⇒ Java String Pool, JDBC connection Pool

 

예제

더보기

문서 내 동일한 Font 스타일이 반복되는 경우 → Font 객체를 공유

  • Font ( Flyweight 객체 (내부 상태) )
public class Font {
    private final String fontName;
    private final int size;

    public Font(String fontName, int size) {
        this.fontName = fontName;
        this.size = size;
    }

    public String getFontName() { return fontName; }
    public int getSize() { return size; }
}
  • FontFactory ( Flyweight Factory (캐싱 저장) )
public class FontFactory {
    private static final Map<String, Font> fontPool = new ConcurrentHashMap<>();

    public static Font getFont(String name, int size) {
        String key = name + size;
        fontPool.putIfAbsent(key, new Font(name, size));
        return fontPool.get(key);
    }

    public static int getPoolSize() {
        return fontPool.size();
    }
}
  • CharacterGlyph ( 외부 상태는 글자 위치로 전달 )
public class CharacterGlyph {
    private final char value;
    private final Font font; // 공유
    private final int x;     // 비공유
    private final int y;     // 비공유

    public CharacterGlyph(char value, Font font, int x, int y) {
        this.value = value;
        this.font = font;
        this.x = x;
        this.y = y;
    }

    public void draw() {
        System.out.println("Draw '" + value + "' at (" + x + "," + y +
                ") with font: " + font.getFontName() + "-" + font.getSize());
    }
}

 

  • 실행
public class Main {
    public static void main(String[] args) {
        Font fontArial12 = FontFactory.getFont("Arial", 12);
        Font fontArial12Again = FontFactory.getFont("Arial", 12);

        System.out.println(fontArial12 == fontArial12Again); // true (공유 객체)

        new CharacterGlyph('A', fontArial12, 10, 10).draw();
        new CharacterGlyph('B', fontArial12, 20, 10).draw();

        System.out.println(FontFactory.getPoolSize()); // 1
    }
}

- FontFactory에서 Font를 불러올 때 이름과 사이즈만 같으면 재생성하지 않고 값을 공유해서 쓰이는 점 메모리 효율을 높입니다.

 

마무리

위에 예제 처럼 값 자체가 바뀌지 않는 다면 메모리가 공유되어서 효율이 올라가는 장점이 있습니다.

'Java > 패턴' 카테고리의 다른 글

커맨드 패턴 ( Command Pattern )  (1) 2025.12.11
프록시 패턴 ( Proxy Pattern )  (0) 2025.12.10
메멘토 패턴 ( Memento Pattern )  (0) 2025.12.08
상태 패턴 ( State Pattern )  (0) 2025.12.05
중재자 패턴 ( Mediator Pattern )  (0) 2025.12.04

학습목표

  1. 객체 상태를 외부에 노출하지 않고 저장/복원할 수 있는 방법 이해
  2. 실행 취소(Undo), 체크포인트, 히스토리 관리 등에 적용하는 방식 학습
  3. Originator / Memento / Caretaker 역할 분리를 이해하고 구현 능력 확보

활용방법

  • 텍스트 에디터 작성 내용 되돌리기
  • 체크포인트 저장 후 재시작
  • 사용자가 취소 -> 최근 상태 복원
  • 실패 시 이전 트랜잭션 상태로 롤백

예제

더보기

텍스트 편집 + Undo/Redo + Diff 저장 방식

  • Snapshot ( Memento )
public class Snapshot {
    private final String state;

    public Snapshot(String state) {
        this.state = state;
    }

    public String getState() {
        return state;
    }
}
  • TextEditor ( Originator )
public class TextEditor {
    private String text = "";

    public void type(String words) {
        text += words;
    }

    public Snapshot save() {
        return new Snapshot(text);
    }

    public void restore(Snapshot snapshot) {
        if (snapshot != null) {
            this.text = snapshot.getState();
        }
    }

    public String getText() {
        return text;
    }
}
  • HistoryManager ( Caretaker )
public class HistoryManager {

    private final Stack<Snapshot> undoStack = new Stack<>();
    private final Stack<Snapshot> redoStack = new Stack<>();

    public void save(Snapshot snapshot) {
        undoStack.push(snapshot);
        redoStack.clear();
    }

    public Snapshot undo() {
        if (undoStack.isEmpty()) return null;
        Snapshot snapshot = undoStack.pop();
        redoStack.push(snapshot);
        return undoStack.isEmpty() ? null : undoStack.peek();
    }

    public Snapshot redo() {
        if (redoStack.isEmpty()) return null;
        Snapshot snapshot = redoStack.pop();
        undoStack.push(snapshot);
        return snapshot;
    }
}
  • 실행
public class Main {
    public static void main(String[] args) {
        TextEditor editor = new TextEditor();
        HistoryManager history = new HistoryManager();

        editor.type("Hello ");
        history.save(editor.save());

        editor.type("World!");
        history.save(editor.save());

        System.out.println(editor.getText()); // Hello World!

        editor.restore(history.undo());
        System.out.println(editor.getText()); // Hello

        editor.restore(history.redo());
        System.out.println(editor.getText()); // Hello World!
    }
}

마무리

예제를 보면 글자를 자동 저장하면서 되돌리기 앞으로 가기 같은 기능을 만들 때 사용하는 예제로도 사용할 수 있고, 적당한 상황에 쓰기에는 나쁘지 않은 방법 같습니다.

학습 목표

  • 객체의 상태에 따라 동작이 달라지는 구조를 설계하는 방법 학습
  • if-else/Switch 지옥 제거 → 상태 확장이 쉬운 구조 만들기
  • 객체지향 원칙(SOLID) 중 OCP(개방-폐쇄 원칙) 실천

상황 예제

  • “문(door)” 상태 제어

예제

더보기

문에 열리는 상태에 따라 이전 상태를 가지고 변환하는 로직

  • State 인터페이스
public interface DoorState {
    void open(Door door);
    void close(Door door);
    void lock(Door door);
}
  • Context — Door
public class Door {
    private DoorState state;

    public Door() {
        this.state = new ClosedState(); // 초기 상태
    }

    public void setState(DoorState state) {
        this.state = state;
    }

    public void open() {
        state.open(this);
    }

    public void close() {
        state.close(this);
    }

    public void lock() {
        state.lock(this);
    }
}
  • 구체적인 상태 객체
public class ClosedState implements DoorState {

    @Override
    public void open(Door door) {
        System.out.println("문이 열렸습니다.");
        door.setState(new OpenedState());
    }

    @Override
    public void close(Door door) {
        System.out.println("이미 닫혀 있습니다.");
    }

    @Override
    public void lock(Door door) {
        System.out.println("문을 잠궜습니다.");
        door.setState(new LockedState());
    }
}


public class LockedState implements DoorState {

    @Override
    public void open(Door door) {
        System.out.println("문이 잠겨 있어 열 수 없습니다!");
    }

    @Override
    public void close(Door door) {
        System.out.println("이미 닫혀 있습니다.");
    }

    @Override
    public void lock(Door door) {
        System.out.println("이미 잠긴 상태입니다.");
    }
}

public class OpenedState implements DoorState {

    @Override
    public void open(Door door) {
        System.out.println("이미 열려 있습니다.");
    }

    @Override
    public void close(Door door) {
        System.out.println("문을 닫았습니다.");
        door.setState(new ClosedState());
    }

    @Override
    public void lock(Door door) {
        System.out.println("문이 열려 있어 잠글 수 없습니다!");
    }
}
  • 실행
public class StatePatternExample {
    public static void main(String[] args) {
        Door door = new Door();

        door.lock();
        door.open();
        door.close();
        door.lock();
        door.open();
    }
}

- 생성자에서 처음 초기 값을 ClosedState 상태로 초기화한 상태에서 문을 잠그면, LockedState 상태로 변경되고 해당 상태에서는 열고, 닫고, 잠그는 행동을 하지 못하는 것을 볼 수 있다. 전략 패턴에서 함수를 바꾸었다면 해당 예제는 상태 자체를 바꾸는 느낌에 패턴

 

마무리

SOLID에서 OCP예제 중에서 대표적인 예제가 아닌가 생각이 듭니다. if / else 지옥으로 만들었을 문제를 현제 상태를 가지고 현재 내가 할 수 있는 행동을 만들 때 쓰인 개념 같습니다.

학습 목표

  • 복잡한 관계를 간단한 관계로 구현한다.
  • 객체 간 직접적인 의존도를 줄이는 것이 why 중요한지 이해한다
  • 미디에이터 패턴이 언제 필요한지 판단할 수 있다
  • 실서비스에서 적용할 수 있도록 예제 흐름 + 코드를 이해한다
  • 기존 구조와 비교해 장단점을 설명할 수 있다

상황 예제

  • 컴포넌트들이 서로 직접 참조하고 메시지를 주고받음
  • UI 컴포넌트가 서로 이벤트를 주고받음
  • 채팅방 기능 개발
    ⇒ N:N → N:1 관계로 개선

예제

더보기

유저를 연결해서 다른 유저한테 전체 메시지를 보내는 방법

  • Mediator (인터페이스) — 메시지 전달 규약 정의
public interface Mediator {
    void send(String message, Colleague colleague);
}
  • Colleague (추상 클래스)
public abstract class Colleague {
    protected Mediator mediator;

    public Colleague(Mediator mediator) {
        this.mediator = mediator;
    }

    public void send(String message) {
        mediator.send(message, this);
    }

    public abstract void receive(String message);
}
  • Concrete Mediator ( 실제 구현 클래스 )
public class ConcreteMediator implements Mediator{

    private List<Colleague> colleagues = new ArrayList<>();

    public void register(Colleague colleague) {
        colleagues.add(colleague);
    }

    @Override
    public void send(String message, Colleague sender) {
        for (Colleague c : colleagues) {
            if (c != sender) {
                c.receive(message);
            }
        }
    }
}
  • Concrete Colleague ( 구독 유저 정보 )
public class User extends Colleague {

    private String name;

    public User(Mediator mediator, String name) {
        super(mediator);
        this.name = name;
    }

    @Override
    public void receive(String message) {
        System.out.println(name + " received: " + message);
    }
}
  • 실행
public class Main {
    public static void main(String[] args) {
        ConcreteMediator mediator = new ConcreteMediator();

        User user1 = new User(mediator, "Alice");
        User user2 = new User(mediator, "Bob");
        User user3 = new User(mediator, "Charlie");

        mediator.register(user1);
        mediator.register(user2);
        mediator.register(user3);

        user1.send("Hello everyone!");
    }
}

- 채팅방처럼 하나의 그룹으로 묶어서 메시지를 보내는 로직

 

마무리

상당히 쓰기 어려운 조건으로 만들어지는 것 같습니다. 그러나 동시에 1:N or N:N을 연결해서 서로 통신을 해야 하는 상황에서 많은 도움이 될 것 같아요.

학습 목표

옵서버 패턴을 통해 이벤트 발생 후 객체 외부에서 처리할 수 있다.

사용 상황

이벤트 발생 → 여러 객체가 반응해야 하는 상황 같을 때 사용

어떤 이벤트가 발생하면 연관된 여러 모듈이 동시에 반응해야 하는 경우.

예시:

  • 새 글이 게시됨 →
    • 알림 서비스가 알림 보내기
    • 이메일 발송
    • 통계 카운트 증가
    • 검색 인덱스 업데이트

이 모든 걸 한 클래스에 묶어두면 결합도가 높아지니까 옵저버 패턴으로 분리함.

예제

  • Observer 클래스
public interface Observer {
    void update(String news);
}
  • Subject 클래스
public interface Subject {
    void registerObserver(Observer observer);
    void removeObserver(Observer observer);
    void notifyObservers();
}
  • ConcreteSubject - 뉴스 발행 주체
import java.util.ArrayList;
import java.util.List;

public class NewsAgency implements Subject {

    private List<Observer> observers = new ArrayList<>();
    private String latestNews;

    @Override
    public void registerObserver(Observer observer) {
        observers.add(observer);
    }

    @Override
    public void removeObserver(Observer observer) {
        observers.remove(observer);
    }

    public void setNews(String news) {
        this.latestNews = news;
        notifyObservers(); // 뉴스가 바뀌면 자동 알림
    }

    @Override
    public void notifyObservers() {
        for (Observer observer : observers) {
            observer.update(latestNews);
        }
    }
}
  • ConcreteObserver - 구독자
public class User implements Observer {

    private String name;

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

    @Override
    public void update(String news) {
        System.out.println(name + "님이 새 뉴스 알림을 받았습니다: " + news);
    }
}
  • 실행
public class Main {
    public static void main(String[] args) {
        NewsAgency agency = new NewsAgency();

        User user1 = new User("덕환");
        User user2 = new User("철수");

        agency.registerObserver(user1);
        agency.registerObserver(user2);

        agency.setNews("속보: 옵저버 패턴 이해가 쉬워졌다!");
        
        agency.removeObserver(user2);

        agency.setNews("2차 속보: 이제 더 많은 예제를 요청하세요!");
    }
}

- NewsAgency가 Subject를 상복 받아서 Observer를 추가하는 상속받아서 각 User가 해당 News를 발행받는 구조

 

마무리

하나의 알림 신호를 여러가지에 방향으로 처리할 때 사용 될 거 같고, Spring에 기초적인 기능으로 Spring ApplicantEventPublisher 객체가 대표적인 옵서버 패턴입니다.

학습 목표

복잡한 과정을 간단하게 표현하는 퍼사드 패턴

사용 상황

여러 복잡한 하위 시스템을 단일 API로 묶고 싶을 때

가장 대표적인 사용 이유.

예시:

  • 결제 처리
    • PG 연동
    • 카드사 인증
    • 포인트 차감
    • 쿠폰 사용
    • 결제 로그 기록

이걸 각 서비스마다 따로 호출하면 복잡해짐 → PaymentFacade로 단일 메서드 제공

예제

더보기

helpSystem01 , helpSystem02, helpSystem03을 순서대로 하나에 process에서 처리하는 로직

  • 각각의 System
class HelpSystem01 {

    public HelpSystem01() {
        System.out.println("Call COnstructor : " + getClass().getSimpleName());
    }

    public void process() {
        System.out.println("Call Process : " + getClass().getSimpleName());
    }

}

class HelpSystem02 {

    public HelpSystem02() {
        System.out.println("Call COnstructor : " + getClass().getSimpleName());
    }

    public void process() {
        System.out.println("Call Process : " + getClass().getSimpleName());
    }

}

class HelpSystem03 {

    public HelpSystem03() {
        System.out.println("Call COnstructor : " + getClass().getSimpleName());
    }

    public void process() {
        System.out.println("Call Process : " + getClass().getSimpleName());
    }

}
  • Facade
public class Facade {

    private HelpSystem01 helpSystem01;
    private HelpSystem02 helpSystem02;
    private HelpSystem03 helpSystem03;

    public Facade() {
        helpSystem01 = new HelpSystem01();
        helpSystem02 = new HelpSystem02();
        helpSystem03 = new HelpSystem03();
    }

    public void process() {
        helpSystem01.process();
        helpSystem02.process();
        helpSystem03.process();
    }
}
  • 실행
public class Main {

    public static void main(String[] args) {
        Facade facade = new Facade();
        facade.process();
    }
}

- Facade 안에 여러가지 서비스 시스템을 실행하는 과정

 

마무리

보통 Web 을 개발하다 보면 Service에서 풀려고 하다 보면 비즈니스 로직들이 합쳐지면 DDD가 깨지는 상황이 옵니다. 그럴 때 Service 앞에 두어서 쓰기 좋은 것 같습니다.

학습 목표

  • 요청을 처리하는 객체들을 체인(연결 구조)으로 구성하는 방법 이해
    • 요청을 보낸 쪽과 처리하는 객체 간의 결합도를 낮출 수 있음.
  • 하나의 요청을 여러 처리자가 순차적으로 검사하고 처리하는 구조 습득
  • 객체 간 강한 의존성을 줄여 유연하고 확장 가능한 구조 설계
  • 분기문(if/switch) 남발을 줄여 코드 유지보수를 쉽게 하는 방법 이해
  • 핸들러 추가/변경 시 기존 코드 수정 최소화하기

상황 예제

  • 회사 결재 시스템
    • 직원이 물품 구매 요청을 한다.
    • 처리자는 아래 순서로 사슬처럼 연결:
      1. 팀장 (최대 50만 원 승인 가능)
      2. 부장 (최대 200만 원 승인 가능)
      3. 이사 (200만 원 이상 승인)
    요청 금액에 따라 어떤 결재자가 승인할지 자동으로 결정됨.
  • 웹 서버 요청 필터링
    1. 인증 처리
    2. 권한 확인
    3. 속도 제한 검사
    4. 요청 처리
    각 단계는 “내가 처리할 수 있으면 처리하고, 아니면 다음으로 넘김”.
  • HTTP 요청이 들어오면 다음 순서로 처리:
  • 고객센터 문의 처리가 순서로 해결 가능 여부를 판단하며 넘어감.
  • 고객 질문 → 챗봇 → 1차 상담 → 전문 상담 → 관리자

예제

더보기
  • 회사 결재 시스템
    • 직원이 물품 구매 요청을 한다.
    • 처리자는 아래 순서로 사슬처럼 연결:
      1. 팀장 (최대 50만 원 승인 가능)
      2. 부장 (최대 200만 원 승인 가능)
      3. 이사 (200만 원 이상 승인)
    요청 금액에 따라 어떤 결재자가 승인할지 자동으로 결정됨.
  • handle
public abstract class Handler {
    protected Handler next;

    public Handler setNext(Handler next) {
        this.next = next;
        return next;
    }

    public abstract void handleRequest(int amount);
}
  • 구현체 책임
public class Manager extends Handler {
    @Override
    public void handleRequest(int amount) {
        if (amount <= 500000) {
            System.out.println("팀장 승인: " + amount + "원");
        } else if (next != null) {
            next.handleRequest(amount);
        }
    }
}

public class Director extends Handler {
    @Override
    public void handleRequest(int amount) {
        if (amount <= 2000000) {
            System.out.println("부장 승인: " + amount + "원");
        } else if (next != null) {
            next.handleRequest(amount);
        }
    }
}

public class Executive extends Handler {
    @Override
    public void handleRequest(int amount) {
        System.out.println("이사 최종 승인: " + amount + "원");
    }
}
  • 실행
public class Main {

    public static void main(String[] args) {
        Handler manager = new Manager();
        Handler director = new Director();
        Handler exec = new Executive();

        manager.setNext(director).setNext(exec);

        manager.handleRequest(300000);   // 팀장 처리
        manager.handleRequest(1500000);  // 부장 처리
        manager.handleRequest(5000000);  // 이사 처리
    }
}

- 금액에 따라 자동으로 결제를 해야 하는 대상이 골라지는 로직입니다.

 

마무리

좀 더 보기 좋게 한 줄로 표기되면서 가시성이 높아질 것 같고, 실제 예제로 생각나는 장면은 예전 결제 로직에서 특정 금액에 따라 할인율이 달라지는 로직이 있었는데 해당 로직 같은걸 해결이 가능했을 것 같습니다.

학습목표

  • 객체 구조(데이터)와 기능(연산)을 분리하여 설계하는 방법 이해
  • 새로운 기능을 객체 구조 변경 없이 확장하는 법을 학습
  • 타입별 다른 처리가 필요한 다형성 구조 만들기
  • 복잡한 객체 그래프(트리/컴포지트) 순회 방법 이해

활용 예제

  • 복잡한 Composite 구조에 다양한 기능을 붙여야 할 때
  • 객체 타입별 서로 다른 로직을 실행해야 할 때
  • 기존 객체 구조는 건드리기 어렵거나 변경 위험이 있을 때
  • 데이터 구조는 공통인데 ‘결과물만 다른 연산’을 추가하고 싶을 때
    • 통계 계산
    • 보고서 생성

예제

더보기

도메인 예: 문서(Document) 안에 여러 요소(Element)가 있고, 요소마다 “렌더링 방식이 다르게 동작”해야 하는 상황

  • 방문받는 객체(Element) 인터페이스
interface DocumentElement {
    void accept(DocumentVisitor visitor);
}
  • 구체 요소들 (Paragraph, Image…)
class Paragraph implements DocumentElement {
    private String text;

    public Paragraph(String text) {
        this.text = text;
    }

    public String getText() {
        return text;
    }

    @Override
    public void accept(DocumentVisitor visitor) {
        visitor.visit(this);
    }
}

class Image implements DocumentElement {
    private String url;

    public Image(String url) {
        this.url = url;
    }

    public String getUrl() {
        return url;
    }

    @Override
    public void accept(DocumentVisitor visitor) {
        visitor.visit(this);
    }
}
  • Visitor 인터페이스
interface DocumentVisitor {
    void visit(Paragraph paragraph);
    void visit(Image image);
}
  • HTML 렌더러 , 텍스트 추출기
class HtmlRenderVisitor implements DocumentVisitor {

    @Override
    public void visit(Paragraph paragraph) {
        System.out.println("<p>" + paragraph.getText() + "</p>");
    }

    @Override
    public void visit(Image image) {
        System.out.println("<img src='" + image.getUrl() + "' />");
    }
}

class TextExtractorVisitor implements DocumentVisitor {

    @Override
    public void visit(Paragraph paragraph) {
        System.out.println(paragraph.getText());
    }

    @Override
    public void visit(Image image) {
        System.out.println("[Image: " + image.getUrl() + "]");
    }
}
  • 구현
public class Main {
    public static void main(String[] args) {
        List<DocumentElement> elements = List.of(
                new Paragraph("Hello World"),
                new Image("https://example.com/image.png")
        );

        DocumentVisitor htmlVisitor = new HtmlRenderVisitor();
        DocumentVisitor textVisitor = new TextExtractorVisitor();

        System.out.println("=== HTML Render ===");
        elements.forEach(e -> e.accept(htmlVisitor));

        System.out.println("\n=== Text Extract ===");
        elements.forEach(e -> e.accept(textVisitor));
    }
}

 

- Paragraph, Image는 둘 다 같이 쓰고 방문자 구현 클래스에 따라 HTML이나 text로 표현하는 방식

 

마무리

리포트 같은 곳에서 같은 값들을 가지고 여러가지 방식으로 표현할 때 사용하기 좋아 보입니다.

+ Recent posts