Featured image of post [Design Patterns] 15. 인터프리터와 미디에이터: 파싱과 조정의 패턴

[Design Patterns] 15. 인터프리터와 미디에이터: 파싱과 조정의 패턴

언어를 객체로 구현하는 Interpreter 패턴과 복잡한 상호작용을 중재하는 Mediator 패턴을 심도 있게 분석합니다. 불리언 규칙 엔진, 수식 계산기, 다중 사용자 채팅 시스템의 메시지 중재 등 복잡한 로직과 관계를 체계적으로 관리하는 전문가 기법을 학습합니다.

Interpreter와 Mediator 패턴을 통해 언어 처리와 객체 관계 관리를 탐구합니다. DSL 구현과 복잡한 상호작용의 중앙 집중화 방법을 학습합니다.

서론: 언어의 구현과 관계의 중재

“좋은 소프트웨어는 복잡한 것을 단순하게 표현한다. Interpreter는 언어를 통해, Mediator는 중재를 통해 이를 실현한다.”

현대 소프트웨어 개발에서 우리는 두 가지 근본적인 도전에 직면합니다:

  1. 복잡한 규칙과 논리를 어떻게 표현하고 실행할 것인가? (언어 처리)
  2. 수많은 객체들의 복잡한 상호작용을 어떻게 관리할 것인가? (관계 관리)

Interpreter 패턴“언어를 객체로 만들어 실행 가능하게” 합니다. 문법 규칙을 클래스로 표현하고, 문장을 객체 트리로 변환하여 해석하고 실행합니다.

Mediator 패턴“복잡한 관계를 중재자로 단순화” 합니다. 여러 객체 간의 상호작용을 중앙의 중재자가 관리하여 결합도를 낮춥니다.

GoF는 두 패턴을 각각 다음과 같이 정의합니다.

“Given a language, define a representation for its grammar along with an interpreter that uses the representation to interpret sentences in the language.” — Gamma et al., 1994

“Define an object that encapsulates how a set of objects interact. Mediator promotes loose coupling by keeping objects from referring to each other explicitly, and it lets you vary their interaction independently.” — Gamma et al., 1994

이 두 패턴은 **“복잡성의 구조화”**라는 공통 목표를 가집니다:

  • Interpreter: 문법적 복잡성의 구조화
  • Mediator: 관계적 복잡성의 구조화

Interpreter 패턴 - 언어의 객체화

Interpreter 패턴의 핵심 철학

Interpreter 패턴은 “문법을 클래스로, 문장을 객체 트리로” 변환하는 것입니다. 이를 통해 언어의 실행 엔진을 객체지향적으로 구현할 수 있습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Interpreter 패턴 없이 구현한다면?
import java.util.Map;

class BadRuleEngine {
    public boolean evaluate(String expression, Map<String, Boolean> variables) {
        // 😱 복잡한 파싱과 평가 로직이 한 곳에 집중
        if (expression.contains("AND")) {
            String[] parts = expression.split("AND");
            return evaluate(parts[0].trim(), variables) && 
                   evaluate(parts[1].trim(), variables);
        } else if (expression.contains("OR")) {
            String[] parts = expression.split("OR");
            return evaluate(parts[0].trim(), variables) || 
                   evaluate(parts[1].trim(), variables);
        } else {
            return variables.getOrDefault(expression.trim(), false);
        }
        // 😱 새로운 연산자 추가 시 전체 메서드 수정
        // 😱 우선순위, 괄호 처리 등이 매우 복잡
        // 😱 에러 처리와 디버깅이 어려움
    }
}

Interpreter 패턴으로 우아하게 해결

  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
// Interpreter 패턴의 우아함
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;

// 1. 추상 표현식 인터페이스
interface Expression {
    boolean interpret(Context context);
    String toString(); // 디버깅을 위한 문자열 표현
}

// 2. 컨텍스트 - 해석에 필요한 정보 저장
class Context {
    private final Map<String, Boolean> variables;
    private final Map<String, Expression> functions;
    private final Stack<String> callStack; // 디버깅용
    
    public Context() {
        this.variables = new HashMap<>();
        this.functions = new HashMap<>();
        this.callStack = new Stack<>();
    }
    
    public boolean getVariable(String name) {
        if (!variables.containsKey(name)) {
            throw new RuntimeException("Undefined variable: " + name);
        }
        return variables.get(name);
    }
    
    public void setVariable(String name, boolean value) {
        variables.put(name, value);
    }
    
    public void defineFunction(String name, Expression expression) {
        functions.put(name, expression);
    }
    
    public Expression getFunction(String name) {
        return functions.get(name);
    }
    
    public void pushCall(String name) {
        callStack.push(name);
    }
    
    public void popCall() {
        if (!callStack.isEmpty()) {
            callStack.pop();
        }
    }
    
    public List<String> getCallStack() {
        return new ArrayList<>(callStack);
    }
}

// 3. 단말 표현식 (Terminal Expressions)
class VariableExpression implements Expression {
    private final String variableName;
    
    public VariableExpression(String variableName) {
        this.variableName = variableName;
    }
    
    @Override
    public boolean interpret(Context context) {
        return context.getVariable(variableName);
    }
    
    @Override
    public String toString() {
        return variableName;
    }
}

class ConstantExpression implements Expression {
    private final boolean value;
    
    public ConstantExpression(boolean value) {
        this.value = value;
    }
    
    @Override
    public boolean interpret(Context context) {
        return value;
    }
    
    @Override
    public String toString() {
        return Boolean.toString(value);
    }
}

// 4. 비단말 표현식 (Non-Terminal Expressions)
class AndExpression implements Expression {
    private final Expression left;
    private final Expression right;
    
    public AndExpression(Expression left, Expression right) {
        this.left = left;
        this.right = right;
    }
    
    @Override
    public boolean interpret(Context context) {
        // 단락 평가 (Short-circuit evaluation)
        boolean leftResult = left.interpret(context);
        if (!leftResult) {
            return false; // 왼쪽이 false면 오른쪽은 평가하지 않음
        }
        return right.interpret(context);
    }
    
    @Override
    public String toString() {
        return "(" + left.toString() + " AND " + right.toString() + ")";
    }
}

class OrExpression implements Expression {
    private final Expression left;
    private final Expression right;
    
    public OrExpression(Expression left, Expression right) {
        this.left = left;
        this.right = right;
    }
    
    @Override
    public boolean interpret(Context context) {
        // 단락 평가
        boolean leftResult = left.interpret(context);
        if (leftResult) {
            return true; // 왼쪽이 true면 오른쪽은 평가하지 않음
        }
        return right.interpret(context);
    }
    
    @Override
    public String toString() {
        return "(" + left.toString() + " OR " + right.toString() + ")";
    }
}

class NotExpression implements Expression {
    private final Expression expression;
    
    public NotExpression(Expression expression) {
        this.expression = expression;
    }
    
    @Override
    public boolean interpret(Context context) {
        return !expression.interpret(context);
    }
    
    @Override
    public String toString() {
        return "NOT(" + expression.toString() + ")";
    }
}

// 5. 함수 호출 표현식
class FunctionExpression implements Expression {
    private final String functionName;
    
    public FunctionExpression(String functionName) {
        this.functionName = functionName;
    }
    
    @Override
    public boolean interpret(Context context) {
        Expression function = context.getFunction(functionName);
        if (function == null) {
            throw new RuntimeException("Undefined function: " + functionName);
        }
        
        context.pushCall(functionName);
        try {
            return function.interpret(context);
        } finally {
            context.popCall();
        }
    }
    
    @Override
    public String toString() {
        return functionName + "()";
    }
}

BooleanExpressionParser는 재귀 하강 파서(recursive descent parser)로 동작한다. 재귀 하강 파서는 문법의 각 규칙을 그대로 하나의 메서드로 옮기고, 낮은 우선순위 규칙의 메서드가 높은 우선순위 규칙의 메서드를 호출하는 방식으로 우선순위를 코드 구조 자체에 반영한다. 이 파서는 parseOrparseAndparseNotparseAtom 순서로 메서드를 계층화하는데, 호출 순서가 곧 연산자 우선순위다: OR가 가장 낮고 AND가 그다음이며 NOT이 가장 높다. parseOr는 먼저 parseAnd를 호출해 왼쪽 피연산자를 얻은 뒤 OR 토큰이 남아 있는 동안 반복해서 오른쪽을 파싱하므로 A OR B OR C는 왼쪽 결합(left-associative)으로 (A OR B) OR C 트리가 된다. 이 계층을 BNF 문법으로 옮기면 다음과 같다.

1
2
3
4
5
expr   ::= or
or     ::= and ("OR" and)*
and    ::= not ("AND" not)*
not    ::= "NOT" atom | atom
atom   ::= "(" or ")" | "true" | "false" | identifier ["(" ")"]

괄호는 parseAtom(를 만나면 최상위 규칙인 parseOr를 재귀적으로 다시 호출해 처리하는데, 이 재귀 호출이 바로 괄호 안 표현식이 우선순위와 무관하게 하나의 단위로 묶이는 이유다.

  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
// 6. 파서 - 문자열을 Expression 트리로 변환
class BooleanExpressionParser {
    private final String input;
    private int position;
    
    public BooleanExpressionParser(String input) {
        this.input = input.replaceAll("\\s+", ""); // 공백 제거
        this.position = 0;
    }
    
    public Expression parse() {
        Expression result = parseOr();
        if (position < input.length()) {
            throw new RuntimeException("Unexpected character at position " + position);
        }
        return result;
    }
    
    private Expression parseOr() {
        Expression left = parseAnd();
        
        while (position < input.length() && peek("OR")) {
            consume("OR");
            Expression right = parseAnd();
            left = new OrExpression(left, right);
        }
        
        return left;
    }
    
    private Expression parseAnd() {
        Expression left = parseNot();
        
        while (position < input.length() && peek("AND")) {
            consume("AND");
            Expression right = parseNot();
            left = new AndExpression(left, right);
        }
        
        return left;
    }
    
    private Expression parseNot() {
        if (peek("NOT")) {
            consume("NOT");
            return new NotExpression(parseAtom());
        }
        return parseAtom();
    }
    
    private Expression parseAtom() {
        if (peek("(")) {
            consume("(");
            Expression expr = parseOr();
            consume(")");
            return expr;
        }
        
        if (peek("true")) {
            consume("true");
            return new ConstantExpression(true);
        }
        
        if (peek("false")) {
            consume("false");
            return new ConstantExpression(false);
        }
        
        // 변수나 함수 파싱
        String identifier = parseIdentifier();
        if (peek("(")) {
            consume("(");
            consume(")");
            return new FunctionExpression(identifier);
        }
        
        return new VariableExpression(identifier);
    }
    
    private String parseIdentifier() {
        StringBuilder sb = new StringBuilder();
        while (position < input.length() && Character.isLetterOrDigit(input.charAt(position))) {
            sb.append(input.charAt(position++));
        }
        
        if (sb.length() == 0) {
            throw new RuntimeException("Expected identifier at position " + position);
        }
        
        return sb.toString();
    }
    
    private boolean peek(String token) {
        return input.substring(position).startsWith(token);
    }
    
    private void consume(String token) {
        if (!peek(token)) {
            throw new RuntimeException("Expected '" + token + "' at position " + position);
        }
        position += token.length();
    }
}

// 7. 비즈니스 규칙 엔진
class BusinessRuleEngine {
    private final Map<String, Expression> rules;
    private final Context context;
    
    public BusinessRuleEngine() {
        this.rules = new HashMap<>();
        this.context = new Context();
    }
    
    public void addRule(String name, String ruleExpression) {
        BooleanExpressionParser parser = new BooleanExpressionParser(ruleExpression);
        Expression expression = parser.parse();
        rules.put(name, expression);
        System.out.println("Added rule '" + name + "': " + expression);
    }
    
    public void setVariable(String name, boolean value) {
        context.setVariable(name, value);
    }
    
    public boolean evaluateRule(String ruleName) {
        Expression rule = rules.get(ruleName);
        if (rule == null) {
            throw new RuntimeException("Rule not found: " + ruleName);
        }
        
        try {
            boolean result = rule.interpret(context);
            System.out.println("Rule '" + ruleName + "' evaluated to: " + result);
            return result;
        } catch (Exception e) {
            System.err.println("Error evaluating rule '" + ruleName + "': " + e.getMessage());
            System.err.println("Call stack: " + context.getCallStack());
            throw e;
        }
    }
    
    public Map<String, Boolean> evaluateAllRules() {
        Map<String, Boolean> results = new HashMap<>();
        for (String ruleName : rules.keySet()) {
            try {
                results.put(ruleName, evaluateRule(ruleName));
            } catch (Exception e) {
                results.put(ruleName, false); // 에러 시 false로 처리
            }
        }
        return results;
    }
    
    public void printRules() {
        System.out.println("=== Business Rules ===");
        for (Map.Entry<String, Expression> entry : rules.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
    }
}

// 사용 예시
class InterpreterPatternDemo {
    public static void main(String[] args) {
        BusinessRuleEngine engine = new BusinessRuleEngine();
        
        // 비즈니스 규칙 정의
        engine.addRule("basic_discount", "isVip OR (age >= 65 AND isLoyalCustomer)");
        engine.addRule("free_shipping", "orderValue >= 50 OR isPremiumMember");
        engine.addRule("special_offer", "isNewCustomer AND NOT hasUsedPromo");
        
        // 고객 정보 설정
        engine.setVariable("isVip", false);
        engine.setVariable("age", 70);
        engine.setVariable("isLoyalCustomer", true);
        engine.setVariable("orderValue", 30);
        engine.setVariable("isPremiumMember", false);
        engine.setVariable("isNewCustomer", true);
        engine.setVariable("hasUsedPromo", false);
        
        // 규칙 평가
        System.out.println("\n=== Rule Evaluation ===");
        Map<String, Boolean> results = engine.evaluateAllRules();
        
        // 결과 출력
        System.out.println("\n=== Results ===");
        results.forEach((rule, result) -> 
            System.out.printf("%s: %s\n", rule, result ? "[APPLY]" : "[NO]")
        );
    }
}

표현식이 객체 트리로 변환되는 과정

BooleanExpressionParser"isVip OR (isSenior AND isLoyalCustomer)"를 파싱하면, 문자열이 아래와 같은 Expression 객체 트리(AST)로 변환됩니다. interpret(Context)는 이 트리를 루트에서부터 재귀적으로 내려가며 평가하는데, AndExpression은 왼쪽이 false면 오른쪽을 평가하지 않는 단락 평가(short-circuit)를 수행합니다.

flowchart TD
    Or["OrExpression"]
    Var1["VariableExpression
(isVip)"] And["AndExpression"] Var2["VariableExpression
(isSenior)"] Var3["VariableExpression
(isLoyalCustomer)"] Or --> Var1 Or --> And And --> Var2 And --> Var3

고급 Interpreter - 수식 계산기

 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
// 수식 계산을 위한 Interpreter 구현
interface MathExpression {
    double evaluate();
    String toString();
}

class NumberExpression implements MathExpression {
    private final double value;
    
    public NumberExpression(double value) {
        this.value = value;
    }
    
    @Override
    public double evaluate() {
        return value;
    }
    
    @Override
    public String toString() {
        return String.valueOf(value);
    }
}

class AddExpression implements MathExpression {
    private final MathExpression left, right;
    
    public AddExpression(MathExpression left, MathExpression right) {
        this.left = left;
        this.right = right;
    }
    
    @Override
    public double evaluate() {
        return left.evaluate() + right.evaluate();
    }
    
    @Override
    public String toString() {
        return "(" + left + " + " + right + ")";
    }
}

class MultiplyExpression implements MathExpression {
    private final MathExpression left, right;
    
    public MultiplyExpression(MathExpression left, MathExpression right) {
        this.left = left;
        this.right = right;
    }
    
    @Override
    public double evaluate() {
        return left.evaluate() * right.evaluate();
    }
    
    @Override
    public String toString() {
        return "(" + left + " * " + right + ")";
    }
}

// 사용 예시: (5 + 3) * 2
class MathDemo {
    public static void main(String[] args) {
        MathExpression expr = new MultiplyExpression(
            new AddExpression(
                new NumberExpression(5),
                new NumberExpression(3)
            ),
            new NumberExpression(2)
        );
        
        System.out.println("Expression: " + expr);
        System.out.println("Result: " + expr.evaluate()); // 16.0
    }
}

Mediator 패턴 - 관계의 중재

Mediator 패턴의 핵심 철학

Mediator 패턴은 **“많은 객체 간의 복잡한 상호작용을 중재자가 관리”**하여 객체들이 서로를 직접 참조하지 않도록 합니다.

 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
// Mediator 패턴 없이 구현한다면?
import java.util.ArrayList;
import java.util.List;

class BadChatSystem {
    class BadUser {
        private String name;
        private List<BadUser> contacts = new ArrayList<>();
        
        public void addContact(BadUser user) {
            contacts.add(user);
            user.contacts.add(this); // 😱 양방향 결합
        }
        
        public void sendMessage(String message, BadUser recipient) {
            // 😱 사용자가 직접 다른 사용자에게 메시지 전송
            recipient.receiveMessage(message, this);
        }
        
        public void broadcastMessage(String message) {
            // 😱 모든 연락처를 직접 관리
            for (BadUser contact : contacts) {
                contact.receiveMessage(message, this);
            }
        }
        
        // 😱 새로운 기능 추가 시 모든 User 클래스 수정 필요
        // 😱 그룹 채팅, 메시지 필터링 등을 추가하기 어려움
    }
}

Mediator 패턴으로 우아하게 해결

  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
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
// Mediator 패턴의 우아함
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;

// 1. Mediator 인터페이스
interface ChatMediator {
    void sendMessage(Message message, User sender);
    void addUser(User user);
    void removeUser(User user);
    void createGroup(String groupName, List<User> members);
    void sendGroupMessage(String groupName, Message message, User sender);
}

// 2. Message 클래스
class Message {
    private final String content;
    private final MessageType type;
    private final LocalDateTime timestamp;
    private final String messageId;
    
    public Message(String content, MessageType type) {
        this.content = content;
        this.type = type;
        this.timestamp = LocalDateTime.now();
        this.messageId = UUID.randomUUID().toString();
    }
    
    // getters
    public String getContent() { return content; }
    public MessageType getType() { return type; }
    public LocalDateTime getTimestamp() { return timestamp; }
    public String getMessageId() { return messageId; }
}

enum MessageType {
    TEXT, IMAGE, FILE, SYSTEM_NOTIFICATION
}

// 3. 구체적인 Mediator 구현
class AdvancedChatRoom implements ChatMediator {
    private final List<User> users;
    private final Map<String, List<User>> groups;
    private final List<MessageFilter> filters;
    private final MessageLogger logger;
    
    public AdvancedChatRoom() {
        this.users = new ArrayList<>();
        this.groups = new HashMap<>();
        this.filters = new ArrayList<>();
        this.logger = new MessageLogger();
    }
    
    @Override
    public void addUser(User user) {
        users.add(user);
        user.setMediator(this);
        
        // 시스템 알림
        Message welcomeMessage = new Message(
            user.getName() + " joined the chat", 
            MessageType.SYSTEM_NOTIFICATION
        );
        broadcastSystemMessage(welcomeMessage, user);
        
        System.out.println("👋 " + user.getName() + " joined the chat room");
    }
    
    @Override
    public void removeUser(User user) {
        users.remove(user);
        
        // 모든 그룹에서 제거
        groups.values().forEach(group -> group.remove(user));
        
        Message leaveMessage = new Message(
            user.getName() + " left the chat", 
            MessageType.SYSTEM_NOTIFICATION
        );
        broadcastSystemMessage(leaveMessage, user);
        
        System.out.println("👋 " + user.getName() + " left the chat room");
    }
    
    @Override
    public void sendMessage(Message message, User sender) {
        // 메시지 필터링
        if (!applyFilters(message, sender)) {
            System.out.println("🚫 Message blocked by filter");
            return;
        }
        
        // 로깅
        logger.logMessage(message, sender, users);
        
        // 모든 사용자에게 전송 (발신자 제외)
        for (User user : users) {
            if (user != sender) {
                user.receive(message, sender.getName());
            }
        }
    }
    
    @Override
    public void createGroup(String groupName, List<User> members) {
        // 유효한 사용자들만 필터링
        List<User> validMembers = members.stream()
            .filter(users::contains)
            .collect(Collectors.toList());
        
        groups.put(groupName, new ArrayList<>(validMembers));
        
        Message groupCreatedMessage = new Message(
            "Group '" + groupName + "' created with " + validMembers.size() + " members",
            MessageType.SYSTEM_NOTIFICATION
        );
        
        // 그룹 멤버들에게만 알림
        for (User member : validMembers) {
            member.receive(groupCreatedMessage, "System");
        }
        
        System.out.println("👥 Group '" + groupName + "' created");
    }
    
    @Override
    public void sendGroupMessage(String groupName, Message message, User sender) {
        List<User> groupMembers = groups.get(groupName);
        if (groupMembers == null) {
            sender.receive(
                new Message("Group '" + groupName + "' not found", MessageType.SYSTEM_NOTIFICATION), 
                "System"
            );
            return;
        }
        
        if (!groupMembers.contains(sender)) {
            sender.receive(
                new Message("You are not a member of group '" + groupName + "'", MessageType.SYSTEM_NOTIFICATION), 
                "System"
            );
            return;
        }
        
        // 메시지 필터링
        if (!applyFilters(message, sender)) {
            return;
        }
        
        // 로깅
        logger.logGroupMessage(message, sender, groupName, groupMembers);
        
        // 그룹 멤버들에게 전송 (발신자 제외)
        for (User member : groupMembers) {
            if (member != sender) {
                member.receiveGroupMessage(message, sender.getName(), groupName);
            }
        }
    }
    
    public void addMessageFilter(MessageFilter filter) {
        filters.add(filter);
    }
    
    private boolean applyFilters(Message message, User sender) {
        for (MessageFilter filter : filters) {
            if (!filter.isAllowed(message, sender)) {
                return false;
            }
        }
        return true;
    }
    
    private void broadcastSystemMessage(Message message, User excludeUser) {
        for (User user : users) {
            if (user != excludeUser) {
                user.receive(message, "System");
            }
        }
    }
    
    public void printStatistics() {
        System.out.println("=== Chat Room Statistics ===");
        System.out.println("Total users: " + users.size());
        System.out.println("Total groups: " + groups.size());
        System.out.println("Total messages: " + logger.getTotalMessages());
    }
}

// 4. User 추상 클래스
abstract class User {
    protected ChatMediator mediator;
    protected final String name;
    protected final UserType userType;
    
    public User(String name, UserType userType) {
        this.name = name;
        this.userType = userType;
    }
    
    public void setMediator(ChatMediator mediator) {
        this.mediator = mediator;
    }
    
    public abstract void send(String messageContent);
    public abstract void receive(Message message, String senderName);
    public abstract void receiveGroupMessage(Message message, String senderName, String groupName);
    
    public String getName() { return name; }
    public UserType getUserType() { return userType; }
}

enum UserType {
    REGULAR, MODERATOR, ADMIN
}

// 5. 구체적인 User 구현
class RegularUser extends User {
    public RegularUser(String name) {
        super(name, UserType.REGULAR);
    }
    
    @Override
    public void send(String messageContent) {
        if (mediator != null) {
            Message message = new Message(messageContent, MessageType.TEXT);
            mediator.sendMessage(message, this);
        }
    }
    
    public void sendToGroup(String groupName, String messageContent) {
        if (mediator != null) {
            Message message = new Message(messageContent, MessageType.TEXT);
            mediator.sendGroupMessage(groupName, message, this);
        }
    }
    
    @Override
    public void receive(Message message, String senderName) {
        System.out.printf("📱 [%s] %s: %s\n", 
                         name, senderName, message.getContent());
    }
    
    @Override
    public void receiveGroupMessage(Message message, String senderName, String groupName) {
        System.out.printf("👥 [%s] %s@%s: %s\n", 
                         name, senderName, groupName, message.getContent());
    }
}

class ModeratorUser extends RegularUser {
    public ModeratorUser(String name) {
        super(name);
    }
    
    public void kickUser(User user) {
        if (mediator != null) {
            mediator.removeUser(user);
            System.out.println("🔨 " + name + " kicked " + user.getName());
        }
    }
    
    @Override
    public void receive(Message message, String senderName) {
        System.out.printf("🛡️ [%s] %s: %s\n", 
                         name, senderName, message.getContent());
    }
}

// 6. 메시지 필터
interface MessageFilter {
    boolean isAllowed(Message message, User sender);
}

class ProfanityFilter implements MessageFilter {
    private final Set<String> bannedWords = Set.of("spam", "bad", "inappropriate");
    
    @Override
    public boolean isAllowed(Message message, User sender) {
        String content = message.getContent().toLowerCase();
        for (String bannedWord : bannedWords) {
            if (content.contains(bannedWord)) {
                System.out.println("🚫 Message from " + sender.getName() + " blocked: contains '" + bannedWord + "'");
                return false;
            }
        }
        return true;
    }
}

class RateLimitFilter implements MessageFilter {
    private final Map<User, List<LocalDateTime>> userMessageTimes = new HashMap<>();
    private final int maxMessagesPerMinute = 10;
    
    @Override
    public boolean isAllowed(Message message, User sender) {
        LocalDateTime now = LocalDateTime.now();
        List<LocalDateTime> messageTimes = userMessageTimes.computeIfAbsent(sender, k -> new ArrayList<>());
        
        // 1분 이전 메시지들 제거
        messageTimes.removeIf(time -> time.isBefore(now.minusMinutes(1)));
        
        if (messageTimes.size() >= maxMessagesPerMinute) {
            System.out.println("🚫 Rate limit exceeded for " + sender.getName());
            return false;
        }
        
        messageTimes.add(now);
        return true;
    }
}

// 7. 메시지 로거
class MessageLogger {
    private int totalMessages = 0;
    private final Map<String, Integer> userMessageCounts = new HashMap<>();
    
    public void logMessage(Message message, User sender, List<User> recipients) {
        totalMessages++;
        userMessageCounts.merge(sender.getName(), 1, Integer::sum);
        
        System.out.printf("📝 LOG: %s sent message to %d users at %s\n",
                         sender.getName(), recipients.size() - 1, message.getTimestamp());
    }
    
    public void logGroupMessage(Message message, User sender, String groupName, List<User> members) {
        totalMessages++;
        userMessageCounts.merge(sender.getName(), 1, Integer::sum);
        
        System.out.printf("📝 LOG: %s sent group message to %s (%d members) at %s\n",
                         sender.getName(), groupName, members.size() - 1, message.getTimestamp());
    }
    
    public int getTotalMessages() {
        return totalMessages;
    }
    
    public Map<String, Integer> getUserMessageCounts() {
        return new HashMap<>(userMessageCounts);
    }
}

// 사용 예시
class MediatorPatternDemo {
    public static void main(String[] args) {
        // 채팅방 생성
        AdvancedChatRoom chatRoom = new AdvancedChatRoom();
        
        // 필터 추가
        chatRoom.addMessageFilter(new ProfanityFilter());
        chatRoom.addMessageFilter(new RateLimitFilter());
        
        // 사용자 생성 및 추가
        User alice = new RegularUser("Alice");
        User bob = new RegularUser("Bob");
        User charlie = new RegularUser("Charlie");
        User moderator = new ModeratorUser("ModeratorDave");
        
        chatRoom.addUser(alice);
        chatRoom.addUser(bob);
        chatRoom.addUser(charlie);
        chatRoom.addUser(moderator);
        
        System.out.println("\n=== General Chat ===");
        
        // 일반 채팅
        alice.send("Hello everyone!");
        bob.send("Hi Alice!");
        charlie.send("Good morning!");
        
        System.out.println("\n=== Group Creation ===");
        
        // 그룹 생성
        chatRoom.createGroup("developers", Arrays.asList(alice, bob, charlie));
        
        System.out.println("\n=== Group Chat ===");
        
        // 그룹 채팅
        ((RegularUser) alice).sendToGroup("developers", "Let's discuss the new project");
        ((RegularUser) bob).sendToGroup("developers", "Great idea!");
        
        System.out.println("\n=== Filter Testing ===");
        
        // 필터 테스트
        alice.send("This message contains spam keyword");
        
        System.out.println("\n=== Statistics ===");
        
        // 통계 출력
        chatRoom.printStatistics();
    }
}

Interpreter와 Mediator의 현대적 활용

Spring Framework에서의 활용

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Spring에서 Mediator 패턴 활용
@Component
public class ApplicationEventPublisher {
    // Spring의 ApplicationContext가 Mediator 역할
    
    @EventListener
    public void handleOrderCreated(OrderCreatedEvent event) {
        // 주문 생성 이벤트 처리
    }
    
    @EventListener  
    public void handleUserRegistered(UserRegisteredEvent event) {
        // 사용자 등록 이벤트 처리
    }
}

// Spring Expression Language (SpEL) - Interpreter 패턴
@Value("#{systemProperties['user.name']}")
private String userName;

@PreAuthorize("hasRole('ADMIN') and #order.amount > 1000")
public void processOrder(Order order) {
    // SpEL이 Interpreter 패턴으로 표현식 해석
}

현대적 DSL 설계

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Fluent Interface를 활용한 내부 DSL
class QueryBuilder {
    public static Query select(String... columns) {
        return new Query().select(columns);
    }
}

// 사용 예시 - SQL과 유사한 DSL
Query query = select("name", "email")
    .from("users")
    .where("age").greaterThan(18)
    .and("status").equals("active")
    .orderBy("name")
    .limit(10);

한눈에 보는 Interpreter & Mediator 패턴

Interpreter vs Mediator 핵심 비교

비교 항목Interpreter 패턴Mediator 패턴
핵심 목적언어/문법 해석객체 간 상호작용 조정
해결 문제문법 파싱, DSL 처리복잡한 객체 간 의존성
구조문법 규칙별 클래스 계층중재자 + Colleague 구조
확장 방식새 표현식 클래스 추가중재자 로직 수정
복잡도 위치분산 (각 표현식 클래스)집중 (Mediator)
사용 빈도드묾 (특수 목적)중간 (GUI, 시스템 통합)

Interpreter 패턴 구성 요소

구성 요소역할예시
AbstractExpression해석 인터페이스 정의interpret(Context)
TerminalExpression종단 기호 해석숫자, 변수, 리터럴
NonterminalExpression비종단 기호 해석연산자, 조합 규칙
Context전역 정보 저장변수 값, 해석 상태
Client구문 트리 구성파서 역할

Mediator 패턴 통신 비교

비교 항목직접 통신Mediator 통신
결합도O(n²) 연결O(n) 연결
의존성Colleague 서로 의존Mediator만 의존
확장성새 Colleague 추가 어려움Mediator만 수정
복잡성분산 (각 객체에)집중 (Mediator에)

적용 시나리오 비교

시나리오InterpreterMediator
수식 계산기OX
SQL 파서OX
정규표현식 엔진OX
채팅방XO
GUI 폼 컴포넌트 연동XO
항공 관제 시스템XO
DSL 구현OX

현대적 대안 비교

패턴전통적 구현현대적 대안
Interpreter직접 구현ANTLR, Parser Combinator, 정규표현식
Mediator직접 구현Event Bus, Message Queue, Redux

장단점 비교

패턴장점단점
Interpreter문법 변경 용이, 새 표현식 추가 쉬움복잡한 문법에 비효율, 성능 이슈
Mediator결합도 감소, 상호작용 집중 관리God Object 위험, 단일 실패점

Mediator vs Observer 비교

비교 항목MediatorObserver
통신 방향양방향단방향
중재자 역할능동적 조정없음 (Subject만)
결합도Mediator에 집중Subject-Observer
사용 목적복잡한 상호작용 조정상태 변경 통지

흔한 오개념: Mediator와 Observer 혼동

“여러 객체에 한 곳에서 메시지를 전달한다"는 설명만 보면 Mediator와 Observer가 같은 패턴처럼 느껴지기 쉽습니다. 하지만 위 표의 “중재자 역할” 행이 가리키듯 둘의 차이는 통신 방향이 아니라 결정 로직이 어디에 있는가입니다. Observer의 Subject는 상태가 바뀌었다는 사실만 통지할 뿐, 누가 어떻게 반응할지는 전혀 알지 못하고 관여하지도 않습니다. 반면 AdvancedChatRoom은 단순히 메시지를 뿌리는 것이 아니라, 어떤 사용자가 어느 그룹에 속해 있는지 확인하고(sendGroupMessage), 필터를 통과했는지 판단하고(applyFilters), 누구를 수신자 목록에서 제외할지 스스로 결정합니다 — 이런 능동적 판단이 바로 Mediator를 Mediator이게 하는 부분입니다. 실무에서 흔한 실수는 “여러 Colleague가 서로 조율해야 하는” 문제(예: 그룹 멤버십 검사, 메시지 필터링 순서 결정)를 각 Colleague가 독립적으로 반응하는 Observer 여러 개로 흉내 내려는 것입니다. 이 경우 조율 로직이 사라진 채 여러 Observer에 흩어지면서, 원래 Mediator 하나로 집중되어야 할 결정이 다시 여러 객체에 분산되는 결과를 낳습니다.

적용 체크리스트

Interpreter 체크 항목Mediator 체크 항목
단순한 문법인가? (복잡하면 파서 도구 사용)객체 간 복잡한 의존성이 있는가?
문법 변경이 빈번한가?N:N 통신을 N:1로 줄이고 싶은가?
성능이 크리티컬하지 않은가?객체들의 상호작용을 한 곳에서 관리?
DSL이 비즈니스 가치를 제공하는가?새 Colleague 추가 시 기존 코드 수정 최소화?

결론: 복잡성의 구조화

Interpreter와 Mediator 패턴은 서로 다른 종류의 복잡성을 해결합니다:

패턴별 핵심 가치:

Interpreter 패턴:

  • 문법적 복잡성의 구조화
  • 도메인 특화 언어 구현
  • 규칙 엔진표현식 평가
  • 확장 가능한 문법 정의

Mediator 패턴:

  • 관계적 복잡성의 단순화
  • 느슨한 결합 실현
  • 중앙집중식 제어
  • 재사용 가능한 상호작용

두 패턴 모두 **“복잡성을 구조화”**하여 이해하기 쉽고 확장 가능한 시스템을 만드는 핵심 도구입니다. 현대 소프트웨어 아키텍처에서 필수불가결한 패턴들입니다.

다음 글에서는 Memento와 Visitor 패턴을 탐구하겠습니다. 상태 보존과 연산 분리를 통한 유연한 객체 조작 방법을 살펴보겠습니다.


핵심 메시지: “Interpreter는 복잡한 규칙을 객체로 만들어 실행 가능하게 하고, Mediator는 복잡한 관계를 중재자로 단순화한다. 두 패턴 모두 복잡성을 구조화하여 시스템의 이해도와 확장성을 높이는 핵심 메커니즘이다.”

평가 기준

독자가 이 글을 읽은 후 달성해야 할 목표:

  • Interpreter 패턴을 사용한 간단한 DSL을 구현할 수 있다
  • Mediator 패턴을 활용한 컴포넌트 간 통신을 설계할 수 있다
  • 파싱과 해석의 기본 원리를 이해할 수 있다
  • 복잡한 객체 관계를 중재자로 단순화할 수 있다
  • 각 패턴의 성능 특성과 적용 한계를 파악할 수 있다