Featured image of post [Design Patterns] 커맨드와 체인 오브 리스폰시빌리티: 요청 처리의 예술

[Design Patterns] 커맨드와 체인 오브 리스폰시빌리티: 요청 처리의 예술

요청을 객체로 캡슐화하는 Command 패턴과 요청 처리자들을 체인으로 연결하는 Chain of Responsibility 패턴을 심도 있게 분석합니다. Undo/Redo 시스템, Macro 명령, 요청 파이프라인, 미들웨어 아키텍처 등 실무에서 활용되는 고급 요청 처리 기법을 학습합니다.

Command와 Chain of Responsibility 패턴을 통해 요청 처리의 우아한 설계를 탐구합니다. 요청 객체화와 책임의 연쇄로 유연한 시스템을 구축합니다.

서론: 요청을 객체로, 책임을 체인으로

“좋은 설계는 ‘무엇을 할 것인가’와 ‘누가 할 것인가’를 분리한다. Command 패턴은 전자를, Chain of Responsibility는 후자를 해결한다.”

현대 소프트웨어에서 **“요청(Request)”**은 단순한 메서드 호출을 넘어 복잡한 워크플로우의 시작점입니다. 사용자의 클릭, API 호출, 시스템 이벤트… 이 모든 요청들을 어떻게 우아하게 처리할 수 있을까요?

Command 패턴은 **“요청을 객체로 캡슐화”**하여 실행 지연, 큐잉, 로깅, Undo/Redo를 가능하게 합니다. Chain of Responsibility 패턴은 **“처리 책임을 체인으로 연결”**하여 요청을 적절한 처리자에게 전달합니다.

이 두 패턴은 **“요청 처리의 완전한 아키텍처”**를 제공합니다:

  • Command: 요청의 캡슐화재사용성
  • Chain of Responsibility: 처리자의 분리확장성

Command 패턴 - 요청의 객체화

Command 패턴의 핵심 철학

Command 패턴의 핵심은 **“Do, Undo, Redo”**입니다. 요청을 객체로 만들면 다음이 가능해집니다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 전통적인 방식의 한계
class BadTextEditor {
    private StringBuilder content = new StringBuilder("Hello World");
    
    public void insertText(String text, int position) {
        content.insert(position, text);
        // 😱 실행 후 되돌릴 방법이 없음
        // 😱 실행 전에 검증할 방법이 없음
        // 😱 나중에 실행할 방법이 없음
        // 😱 여러 번 실행할 방법이 없음
    }
}

Command 패턴으로 혁신적 해결

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
// Command 패턴의 강력함
interface Command {
    void execute();
    void undo();
    boolean canExecute();
    String getDescription();
    LocalDateTime getTimestamp();
}

// Document 클래스 (Receiver)
class Document {
    private StringBuilder content;
    private final List<DocumentListener> listeners;
    
    public Document(String initialContent) {
        this.content = new StringBuilder(initialContent);
        this.listeners = new ArrayList<>();
    }
    
    public void insertText(String text, int position) {
        validatePosition(position);
        content.insert(position, text);
        notifyListeners("INSERT", text, position);
    }
    
    public String deleteText(int start, int length) {
        validateRange(start, length);
        String deleted = content.substring(start, start + length);
        content.delete(start, start + length);
        notifyListeners("DELETE", deleted, start);
        return deleted;
    }
    
    public String getContent() {
        return content.toString();
    }
    
    public int getLength() {
        return content.length();
    }
    
    private void validatePosition(int position) {
        if (position < 0 || position > content.length()) {
            throw new IllegalArgumentException("Invalid position: " + position);
        }
    }
    
    private void validateRange(int start, int length) {
        if (start < 0 || length < 0 || start + length > content.length()) {
            throw new IllegalArgumentException("Invalid range: " + start + ", " + length);
        }
    }
    
    private void notifyListeners(String operation, String text, int position) {
        for (DocumentListener listener : listeners) {
            listener.onDocumentChanged(operation, text, position);
        }
    }
    
    public void addListener(DocumentListener listener) {
        listeners.add(listener);
    }
}

interface DocumentListener {
    void onDocumentChanged(String operation, String text, int position);
}

// ConcreteCommand 구현체들
class InsertCommand implements Command {
    private final Document document;
    private final String text;
    private final int position;
    private final LocalDateTime timestamp;
    
    public InsertCommand(Document document, String text, int position) {
        this.document = document;
        this.text = text;
        this.position = position;
        this.timestamp = LocalDateTime.now();
    }
    
    @Override
    public void execute() {
        if (!canExecute()) {
            throw new IllegalStateException("Command cannot be executed");
        }
        document.insertText(text, position);
    }
    
    @Override
    public void undo() {
        document.deleteText(position, text.length());
    }
    
    @Override
    public boolean canExecute() {
        return position >= 0 && position <= document.getLength() && text != null;
    }
    
    @Override
    public String getDescription() {
        return String.format("Insert '%s' at position %d", text, position);
    }
    
    @Override
    public LocalDateTime getTimestamp() {
        return timestamp;
    }
}

// 매크로 명령
class MacroCommand implements Command {
    private final List<Command> commands;
    private final String description;
    private final LocalDateTime timestamp;
    
    public MacroCommand(String description) {
        this.commands = new ArrayList<>();
        this.description = description;
        this.timestamp = LocalDateTime.now();
    }
    
    public void addCommand(Command command) {
        commands.add(command);
    }
    
    @Override
    public void execute() {
        for (Command command : commands) {
            if (command.canExecute()) {
                command.execute();
            } else {
                throw new IllegalStateException("Macro contains invalid command");
            }
        }
    }
    
    @Override
    public void undo() {
        // 역순으로 undo 실행
        for (int i = commands.size() - 1; i >= 0; i--) {
            commands.get(i).undo();
        }
    }
    
    @Override
    public boolean canExecute() {
        return commands.stream().allMatch(Command::canExecute);
    }
    
    @Override
    public String getDescription() {
        return String.format("%s (%d commands)", description, commands.size());
    }
    
    @Override
    public LocalDateTime getTimestamp() {
        return timestamp;
    }
}

// CommandManager (Invoker)
class CommandManager {
    private final Deque<Command> undoStack;
    private final Deque<Command> redoStack;
    private final int maxHistorySize;
    
    public CommandManager(int maxHistorySize) {
        this.undoStack = new ArrayDeque<>();
        this.redoStack = new ArrayDeque<>();
        this.maxHistorySize = maxHistorySize;
    }
    
    public void executeCommand(Command command) {
        if (!command.canExecute()) {
            throw new IllegalArgumentException("Command cannot be executed");
        }
        
        command.execute();
        undoStack.addLast(command);
        redoStack.clear();
        
        while (undoStack.size() > maxHistorySize) {
            undoStack.removeFirst();
        }
    }
    
    public boolean canUndo() {
        return !undoStack.isEmpty();
    }
    
    public boolean canRedo() {
        return !redoStack.isEmpty();
    }
    
    public void undo() {
        if (canUndo()) {
            Command command = undoStack.removeLast();
            command.undo();
            redoStack.addLast(command);
        }
    }
    
    public void redo() {
        if (canRedo()) {
            Command command = redoStack.removeLast();
            command.execute();
            undoStack.addLast(command);
        }
    }
}

Chain of Responsibility - 책임의 연쇄

Chain of Responsibility의 핵심 철학

Chain of Responsibility 패턴은 **“요청을 처리할 수 있는 객체들의 체인을 구성”**하여 요청을 적절한 처리자에게 전달합니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 전통적인 방식의 한계
class BadSupportSystem {
    public void handleRequest(String requestType, String description) {
        // 😱 모든 처리 로직이 한 곳에 집중
        if (requestType.equals("PASSWORD_RESET")) {
            if (description.contains("forgot")) {
                // Level 1 처리
            } else if (description.contains("locked")) {
                // Level 2 처리
            } else {
                // Level 3 처리
            }
        } else if (requestType.equals("BILLING")) {
            // 또 다른 복잡한 조건문들...
        }
        // 😱 새로운 요청 타입 추가 시 이 메서드 수정 필요
    }
}

Chain of Responsibility로 우아하게 해결

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
// Chain of Responsibility 패턴의 우아함
abstract class RequestHandler {
    protected RequestHandler nextHandler;
    protected final String handlerName;
    protected final Set<String> supportedTypes;
    
    public RequestHandler(String handlerName, String... supportedTypes) {
        this.handlerName = handlerName;
        this.supportedTypes = Set.of(supportedTypes);
    }
    
    public RequestHandler setNext(RequestHandler handler) {
        this.nextHandler = handler;
        return handler;
    }
    
    public final void handleRequest(Request request) {
        if (canHandle(request)) {
            long startTime = System.nanoTime();
            RequestResult result = doHandle(request);
            long endTime = System.nanoTime();
            
            result.setProcessingTime(endTime - startTime);
            result.setHandlerName(handlerName);
            request.setResult(result);
            
            if (result.isSuccess()) {
                System.out.printf("[OK] %s handled: %s\n", handlerName, request.getId());
                return;
            }
        }
        
        if (nextHandler != null) {
            nextHandler.handleRequest(request);
        } else {
            handleUnprocessableRequest(request);
        }
    }
    
    protected abstract boolean canHandle(Request request);
    protected abstract RequestResult doHandle(Request request);
    
    protected void handleUnprocessableRequest(Request request) {
        System.out.printf("[Error] No handler found for: %s\n", request.getId());
        request.setResult(RequestResult.failed("No suitable handler found"));
    }
}

// Request와 Result 클래스들
class Request {
    private final String id;
    private final String type;
    private final String description;
    private final Priority priority;
    private final LocalDateTime timestamp;
    private RequestResult result;
    
    public Request(String type, String description, Priority priority) {
        this.id = UUID.randomUUID().toString();
        this.type = type;
        this.description = description;
        this.priority = priority;
        this.timestamp = LocalDateTime.now();
    }
    
    // getters and setters
    public String getId() { return id; }
    public String getType() { return type; }
    public String getDescription() { return description; }
    public Priority getPriority() { return priority; }
    public LocalDateTime getTimestamp() { return timestamp; }
    public RequestResult getResult() { return result; }
    public void setResult(RequestResult result) { this.result = result; }
}

class RequestResult {
    private final boolean success;
    private final String message;
    private String handlerName;
    private long processingTime;
    
    private RequestResult(boolean success, String message) {
        this.success = success;
        this.message = message;
    }
    
    public static RequestResult success(String message) {
        return new RequestResult(true, message);
    }
    
    public static RequestResult failed(String message) {
        return new RequestResult(false, message);
    }
    
    // getters and setters
    public boolean isSuccess() { return success; }
    public String getMessage() { return message; }
    public String getHandlerName() { return handlerName; }
    public void setHandlerName(String handlerName) { this.handlerName = handlerName; }
    public long getProcessingTime() { return processingTime; }
    public void setProcessingTime(long processingTime) { this.processingTime = processingTime; }
}

enum Priority {
    LOW(1), MEDIUM(2), HIGH(3), CRITICAL(4);
    
    private final int level;
    Priority(int level) { this.level = level; }
    public int getLevel() { return level; }
}

// ConcreteHandler 구현체들
class Level1SupportHandler extends RequestHandler {
    public Level1SupportHandler() {
        super("Level 1 Support", "PASSWORD_RESET", "ACCOUNT_QUESTION");
    }
    
    @Override
    protected boolean canHandle(Request request) {
        return supportedTypes.contains(request.getType()) && 
               request.getPriority().getLevel() <= 2;
    }
    
    @Override
    protected RequestResult doHandle(Request request) {
        System.out.println("🔐 Processing basic support request...");
        return RequestResult.success("Basic support provided");
    }
}

class Level2TechnicalHandler extends RequestHandler {
    public Level2TechnicalHandler() {
        super("Level 2 Technical", "TECHNICAL_ISSUE", "BILLING_PROBLEM");
    }
    
    @Override
    protected boolean canHandle(Request request) {
        return supportedTypes.contains(request.getType()) && 
               request.getPriority().getLevel() <= 3;
    }
    
    @Override
    protected RequestResult doHandle(Request request) {
        System.out.println("🔧 Processing technical issue...");
        return RequestResult.success("Technical issue resolved");
    }
}

class Level3SpecialistHandler extends RequestHandler {
    public Level3SpecialistHandler() {
        super("Level 3 Specialist", "CRITICAL_ISSUE", "SECURITY_BREACH");
    }
    
    @Override
    protected boolean canHandle(Request request) {
        return supportedTypes.contains(request.getType()) || 
               request.getPriority() == Priority.CRITICAL;
    }
    
    @Override
    protected RequestResult doHandle(Request request) {
        System.out.println("🚨 Specialist handling critical request...");
        return RequestResult.success("Critical issue resolved by specialist");
    }
}

// Chain Builder
class SupportChainBuilder {
    public static RequestHandler buildSupportChain() {
        RequestHandler level1 = new Level1SupportHandler();
        RequestHandler level2 = new Level2TechnicalHandler();
        RequestHandler level3 = new Level3SpecialistHandler();
        
        level1.setNext(level2).setNext(level3);
        return level1;
    }
}

미들웨어 패턴으로의 진화

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// 웹 미들웨어 스타일 구현
interface Middleware {
    void handle(HttpRequest request, HttpResponse response, MiddlewareChain chain);
}

class MiddlewareChain {
    private final List<Middleware> middlewares;
    private int currentIndex = 0;
    
    public MiddlewareChain(List<Middleware> middlewares) {
        this.middlewares = new ArrayList<>(middlewares);
    }
    
    public void proceed(HttpRequest request, HttpResponse response) {
        if (currentIndex < middlewares.size()) {
            Middleware currentMiddleware = middlewares.get(currentIndex++);
            currentMiddleware.handle(request, response, this);
        }
    }
}

// 인증 미들웨어
class AuthenticationMiddleware implements Middleware {
    @Override
    public void handle(HttpRequest request, HttpResponse response, MiddlewareChain chain) {
        String token = request.getHeader("Authorization");
        
        if (token == null || !validateToken(token)) {
            response.setStatus(401);
            response.setBody("Unauthorized");
            return;
        }
        
        chain.proceed(request, response);
    }
    
    private boolean validateToken(String token) {
        return token.startsWith("Bearer ") && token.length() > 10;
    }
}

// 로깅 미들웨어
class LoggingMiddleware implements Middleware {
    @Override
    public void handle(HttpRequest request, HttpResponse response, MiddlewareChain chain) {
        long startTime = System.currentTimeMillis();
        
        System.out.printf("→ %s %s\n", request.getMethod(), request.getPath());
        
        chain.proceed(request, response);
        
        long endTime = System.currentTimeMillis();
        System.out.printf("← %d (%dms)\n", response.getStatus(), endTime - startTime);
    }
}

// HTTP 관련 클래스들
class HttpRequest {
    private final String method;
    private final String path;
    private final Map<String, String> headers;
    
    public HttpRequest(String method, String path) {
        this.method = method;
        this.path = path;
        this.headers = new HashMap<>();
    }
    
    public String getMethod() { return method; }
    public String getPath() { return path; }
    public String getHeader(String name) { return headers.get(name); }
    public void setHeader(String name, String value) { headers.put(name, value); }
}

class HttpResponse {
    private int status = 200;
    private String body = "";
    
    public int getStatus() { return status; }
    public void setStatus(int status) { this.status = status; }
    public String getBody() { return body; }
    public void setBody(String body) { this.body = body; }
}

한눈에 보는 Command & Chain of Responsibility 패턴

Command vs Chain of Responsibility 핵심 비교

비교 항목Command 패턴Chain of Responsibility 패턴
핵심 목적요청을 객체로 캡슐화요청 처리 기회를 여러 객체에 부여
구조단일 핸들러 지정핸들러 체인
처리자 결정호출 시점에 명확런타임에 동적 결정
Undo/Redo지원 용이지원 어려움
결합도Invoker-Receiver 분리핸들러 간 느슨한 연결
확장성새 Command 추가체인에 핸들러 추가/제거

Command 패턴 핵심 참여자

참여자역할책임
Command인터페이스execute(), undo() 정의
ConcreteCommand구체 명령Receiver 호출, 상태 저장
Invoker요청자Command 실행 트리거
Receiver수신자실제 작업 수행
Client조립자Command-Receiver 연결

Chain of Responsibility 처리 방식

처리 방식설명예시
단일 처리하나의 핸들러만 처리권한 검증 체인
다중 처리여러 핸들러가 순차 처리미들웨어 체인
선택적 처리조건에 따라 처리/스킵로깅 필터
변환 처리요청을 변환하며 전달파이프라인

적용 시나리오 비교

시나리오CommandChain of Responsibility
Undo/Redo 기능OX
매크로 기록OX
트랜잭션OX
요청 큐잉OX
권한 검증 체인XO
HTTP 미들웨어XO
로깅 필터XO
이벤트 버블링XO

현대적 활용 비교

프레임워크/도구Command 활용CoR 활용
Spring@TransactionalFilter, Interceptor
Java Servlet-Filter Chain
Express.js-Middleware
ReduxAction (Command류)Middleware
GUI Framework버튼 클릭 핸들링이벤트 버블링

장단점 비교

패턴장점단점
CommandUndo/Redo 지원, 요청 큐잉, 매크로, SRP 준수클래스 수 증가, 구조 복잡
CoR느슨한 결합, 동적 체인 구성, 유연한 처리처리 보장 없음, 디버깅 어려움

조합 패턴

조합효과사용 예
Command + MementoUndo 상태 저장텍스트 에디터
Command + Composite매크로 명령일괄 작업
CoR + Template Method처리 골격 정의검증 파이프라인
CoR + Strategy핸들러별 전략동적 필터링

적용 체크리스트

Command 체크 항목CoR 체크 항목
요청을 객체로 저장해야 하는가?여러 객체가 처리 기회를 가져야 하는가?
Undo/Redo가 필요한가?처리자를 동적으로 결정해야 하는가?
요청을 큐에 저장/지연 실행?체인 순서가 중요한가?
매크로 기록이 필요한가?요청이 전파되어야 하는가?

결론: 요청 처리 아키텍처의 완성

Command와 Chain of Responsibility 패턴은 **“요청 처리 아키텍처”**의 핵심 구성 요소입니다:

패턴별 핵심 가치:

Command 패턴:

  • 요청의 객체화로 재사용성 확보
  • Undo/Redo 메커니즘 구현
  • 지연 실행큐잉 지원
  • 로깅트랜잭션 관리

Chain of Responsibility 패턴:

  • 처리자의 분리느슨한 결합
  • 동적 체인 구성으로 유연성 확보
  • 단일 책임 원칙 실현
  • 확장성재사용성 향상

현대적 활용:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Command Pattern → Modern Evolution:
- GUI 프레임워크 (Event Handling)
- Event Sourcing Systems
- Message Queue Systems
- Transaction Management

Chain of Responsibility → Modern Evolution:
- Web Middleware (Express.js, Spring)
- Exception Handling Chains
- Validation Pipelines
- Security Filter Chains

실무 가이드라인:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
Command 패턴 적용 시점:
- Undo/Redo 기능이 필요할 때
- 요청을 큐에 저장해야 할 때
- 매크로나 스크립트 기능이 필요할 때
- 트랜잭션 로깅이 필요할 때

Chain of Responsibility 적용 시점:
- 여러 처리자가 요청을 처리할 수 있을 때
- 처리자를 동적으로 구성해야 할 때
- 조건문이 복잡하고 처리 로직이 분산될 때
- 미들웨어나 필터 체인이 필요할 때

주의사항:
- 체인이 너무 길어지면 성능 저하
- 순환 참조 방지
- 메모리 누수 주의 (Command 히스토리)
- 예외 처리 전략 수립

두 패턴 모두 **“관심사의 분리”**를 통해 코드의 유지보수성과 확장성을 크게 향상시킵니다. 현대 소프트웨어의 복잡한 요청 처리 시나리오에서 필수적인 패턴들입니다.

다음 글에서는 Template Method와 Iterator 패턴을 탐구하겠습니다. 알고리즘의 골격 정의와 순차적 접근을 통한 코드 재사용과 캡슐화 방법을 살펴보겠습니다.


핵심 메시지: “Command는 ‘무엇을 할 것인가’를 객체로 캡슐화하고, Chain of Responsibility는 ‘누가 할 것인가’를 유연하게 결정한다. 두 패턴의 조합은 현대 소프트웨어의 복잡한 요청 처리 아키텍처의 핵심이다.”