Featured image of post [Design Patterns] 02. 패턴 분석의 프레임워크

[Design Patterns] 02. 패턴 분석의 프레임워크

GoF 패턴을 체계적으로 분석하고 평가하는 과학적 방법론을 제시합니다. Intent 분석부터 Trade-off 평가까지, 패턴의 본질을 꿰뚫어보는 전문가적 사고 과정을 학습하고, 상황에 맞는 최적의 패턴을 선택할 수 있는 분석 능력을 기릅니다. 인지과학적 관점에서 패턴 인식과 스키마 이론을 탐구합니다.

GoF 패턴을 체계적으로 분석하고 평가하는 과학적 방법론을 제시합니다. Intent 분석부터 Trade-off 평가까지, 패턴의 본질을 꿰뚫어보는 전문가적 사고 과정을 학습합니다.

서론: 패턴을 보는 눈

“패턴을 안다는 것과 패턴을 이해한다는 것은 전혀 다른 차원의 문제다.”

많은 개발자들이 GoF의 23개 패턴을 외우고 있습니다. Observer는 일대다 관계, Strategy는 알고리즘 교체… 하지만 정작 실무에서 “이 상황에서 어떤 패턴을 써야 할까?” 혹은 **“이 패턴이 정말 최선의 선택일까?”**라는 질문 앞에서는 막막해집니다.

패턴을 단순히 암기하는 것과 패턴의 본질을 꿰뚫어보는 것 사이에는 거대한 간극이 있습니다. 진정한 설계 전문가는 패턴을 분석하고, 평가하고, 상황에 맞게 선택할 수 있는 능력을 갖춘 사람입니다.

이번 글에서는 패턴을 체계적으로 분석하고 평가하는 과학적 방법론을 제시합니다. 이는 단순한 기법이 아니라, 사고의 프레임워크입니다.

GoF 패턴 분석 템플릿의 심층 해부

GoF의 『Design Patterns』(1994)는 23개 패턴 각각을 Intent, Also Known As, Motivation, Applicability, Structure, Participants, Collaborations, Consequences, Implementation, Sample Code, Known Uses, Related Patterns라는 고정된 12개 항목의 템플릿으로 기술한다. 이 템플릿 자체가 저자들의 핵심 발명 중 하나로, 패턴을 “이야기"가 아니라 “재현 가능하게 분석·비교할 수 있는 명세"로 바꾸는 장치다.

이 템플릿의 뿌리는 건축가 Christopher Alexander가 『A Pattern Language』(1977)에서 제시한 패턴 기술 방식에 있다. Alexander는 각 패턴을 맥락(Context)-문제(Problem)-해결책(Solution)의 3단 구조로 서술했는데, GoF는 이를 소프트웨어 설계에 맞게 확장하면서 Intent를 문제 정의로, Structure·Participants·Collaborations를 해결책의 해부학으로, Consequences를 결과(Trade-off)로 대응시켰다. 이 글에서 사용하는 4단계 분석(Intent-Structure-Participants-Collaborations)은 GoF 템플릿의 핵심 축만 추려 실무 분석에 적용하기 쉽게 재구성한 것이다.

Intent (의도) - 패턴의 영혼

GoF 책에서 가장 중요한 섹션은 바로 **“Intent”**입니다. 여기에 패턴의 핵심 가치가 압축되어 있습니다.

Observer 패턴의 Intent 분석:

“Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.” — Gamma, Helm, Johnson, Vlissides, 『Design Patterns』(1994)

이 한 문장을 해부해보면:

  • 핵심 문제: “one-to-many dependency”
  • 트리거 조건: “when one object changes state”
  • 해결책: “all its dependents are notified and updated automatically”
  • 목표: 자동화된 상태 동기화

Intent 분석 체크리스트:

1
2
3
4
□ 해결하려는 핵심 문제가 명확한가?
□ 문제의 범위가 적절히 정의되었는가?
□ 해결책의 본질이 간결하게 표현되었는가?
□ 다른 패턴과 구분되는 고유성이 있는가?

Intent 분석이 중요한 이유는 소프트웨어 설계가 본질적으로 문제 공간(problem space)과 해결 공간(solution space)을 오가는 작업이기 때문이다. Intent 문장은 문제 공간의 언어로 쓰여 있어서, 이를 정확히 해부하지 않으면 해결 공간(Structure 이하)의 세부사항에 매몰되어 “왜 이 구조를 선택했는가"를 잃어버리기 쉽다. 다만 Intent만으로는 구현이 결정되지 않는다는 한계도 있다 — 같은 Intent 문장이 Push 모델과 Pull 모델처럼 서로 다른 Structure로 실현될 수 있으므로, Intent는 분석의 출발점이지 종착점이 아니다.

Structure (구조) - 패턴의 해부학

구조 다이어그램은 패턴의 **“해부학”**입니다. 단순히 클래스 관계를 보여주는 것이 아니라, 역할 분담의 철학을 담고 있습니다.

Strategy 패턴 구조 분석:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Context: 전략을 사용하는 주체
public class SortContext {
    private SortStrategy strategy;  // 의존성 주입 지점
    
    public void setStrategy(SortStrategy strategy) {
        this.strategy = strategy;   // 런타임 교체 가능
    }
    
    public void executeSort(int[] data) {
        strategy.sort(data);        // 위임(delegation)
    }
}

// Strategy: 알고리즘의 공통 인터페이스
public interface SortStrategy {
    void sort(int[] data);          // 템플릿 메서드
}

// ConcreteStrategy: 구체적 구현
public class QuickSortStrategy implements SortStrategy {
    public void sort(int[] data) {
        Arrays.sort(data);  // java.util.Arrays의 이중 피벗 퀵정렬에 위임 (표준 라이브러리 우선)
    }
}

구조 분석의 핵심 포인트:

  1. 역할 분리: Context는 “언제”, Strategy는 “어떻게”
  2. 의존성 방향: Context → Strategy (역방향 불가)
  3. 교체 메커니즘: setStrategy() 통한 런타임 변경
  4. 위임 패턴: Context가 실제 작업을 Strategy에 위임

이 같은 역할 분리는 SOLID 원칙 중 의존성 역전 원칙(Dependency Inversion Principle)이 요구하는 방향과 일치한다 — 고수준 모듈(Context)이 저수준 모듈(ConcreteStrategy)에 직접 의존하지 않고, 둘 다 추상화(SortStrategy 인터페이스)에 의존하도록 만드는 것이 Structure 분석의 실질적 목표다. Structure 다이어그램을 그릴 때 화살표의 방향을 먼저 확인해야 하는 이유가 여기에 있다 — 화살표가 거꾸로 향하면 그 패턴은 의도한 유연성을 제공하지 못한다.

Participants (참여자) - 역할과 책임

각 참여자는 단일 책임 원칙을 따라 명확한 역할을 가집니다.

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
// Client: 명령을 조립하는 역할
public class MacroRecorder {
    public void createMacro() {
        Command[] commands = {
            new CopyCommand(editor),
            new PasteCommand(editor),
            new SaveCommand(editor)
        };
        MacroCommand macro = new MacroCommand(commands);
        invoker.setCommand(macro);
    }
}

// Invoker: 명령을 실행하는 역할
public class MenuButton {
    private Command command;
    
    public void click() {
        command.execute();  // 구체적 명령을 몰라도 실행 가능
    }
}

// Command: 명령의 추상화
public interface Command {
    void execute();
    void undo();           // 실행 취소 지원
}

// ConcreteCommand: 구체적 명령 구현
public class CopyCommand implements Command {
    private TextEditor receiver;
    private String backup;
    
    public void execute() {
        backup = receiver.getSelection();
        receiver.copy();
    }
    
    public void undo() {
        receiver.setSelection(backup);
    }
}

// Receiver: 실제 작업을 수행하는 객체
public class TextEditor {
    public void copy() { /* 실제 복사 로직 */ }
    public void paste() { /* 실제 붙여넣기 로직 */ }
}

이 매트릭스의 “알아야 할 것 / 몰라도 되는 것” 구분은 Parnas가 「On the Criteria to Be Used in Decomposing Systems into Modules」(Communications of the ACM, 1972)에서 제시한 정보 은닉(information hiding) 원칙을 참여자별로 명시한 것이다. Parnas는 모듈 경계를 처리 순서가 아니라 “변경되기 쉬운 설계 결정을 숨기는 경계"로 그어야 한다고 주장했는데, Command 패턴에서 Invoker가 ConcreteCommand의 존재조차 몰라도 되는 구조가 이 원칙의 실제 사례다. 참여자 분석이 부실하면 이런 은닉 경계가 무너져, 한 클래스의 변경이 의도치 않게 다른 클래스로 전파되는 결합이 생긴다.

참여자 분석 매트릭스:

참여자주요 책임알아야 할 것몰라도 되는 것
Client명령 조립Command 인터페이스구체적 실행 방법
Invoker명령 실행 트리거Command 인터페이스구체적 명령 내용
Command인터페이스 정의Receiver 인터페이스구체적 구현 방법
ConcreteCmd구체적 명령 구현Receiver의 메서드다른 Command들
Receiver실제 작업 수행자신의 도메인 로직Command 존재 여부

Collaborations (협력) - 상호작용의 예술

협력 패턴은 시나리오별 상호작용을 보여줍니다. 이는 패턴의 동적 측면입니다.

Observer 패턴 협력 시퀀스:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Subject.notifyObservers() 호출 시:

1. Subject → Observer1: update()
2. Subject → Observer2: update()  
3. Subject → Observer3: update()

Observer.update() 내부에서:

4. Observer1 → Subject: getState()
5. Observer1: updateInternalState()
6. Observer2 → Subject: getState()
7. Observer2: updateInternalState()

협력 분석의 핵심 질문:

  • 누가 협력을 시작하는가? (Subject)
  • 협력의 순서가 중요한가? (Observer들의 순서는 보통 중요하지 않음)
  • 실패 시 어떻게 처리하는가? (일부 Observer 실패 시 다른 Observer들은?)
  • 순환 참조 위험이 있는가? (Observer가 Subject 상태를 변경하면?)

협력 분석이 구조 분석과 구별되는 지점은 시간 축이다. Structure가 “어떤 관계가 존재하는가"라는 정적 스냅샷이라면, Collaborations는 UML 시퀀스 다이어그램이 표현하는 것과 같은 동적 순서·타이밍을 다룬다. Observer 패턴에서 특히 주의할 점은, 통지 순서가 코드상 리스트 순회 순서에 불과할 뿐 명세로 보장된 것이 아니라는 사실이다 — 그런데도 개발자들은 종종 “먼저 등록한 Observer가 먼저 통지받는다"를 암묵적으로 전제하고 코드를 작성해, 내부 컬렉션 구현이 바뀌는 순간(예: HashSet으로 교체) 재현하기 어려운 버그가 발생한다.

패턴 적용 조건 분석 기법

문제 영역 식별 매트릭스

패턴 적용을 위해서는 먼저 문제의 본질을 정확히 파악해야 합니다.

문제 유형별 패턴 매핑:

문제 유형1차 후보 패턴2차 후보 패턴
객체 생성이 복잡함Factory MethodAbstract Factory, Builder
객체 생성 비용이 높음SingletonFlyweight, Object Pool
런타임에 행동을 변경해야 함StrategyState, Command
복잡한 객체 구조를 단순화FacadeAdapter, Proxy
객체 간 일대다 의존성ObserverMediator, Event Bus
알고리즘을 캡슐화해야 함Template MethodStrategy, Command

이런 매핑 표는 어디까지나 첫 필터링을 위한 휴리스틱이지, 결정 규칙이 아니다. 실제로는 하나의 문제가 여러 행에 동시에 걸치는 경우가 흔하다 — 로깅 시스템은 “런타임 행동 변경"과 “복잡한 구조 단순화"에 동시에 해당할 수 있고, 이때 표는 후보 패턴 목록을 좁혀줄 뿐 최종 선택은 이어지는 적용 가능성 체크리스트와 Trade-off 분석을 거쳐야 한다.

적용 가능성 평가 체크리스트

Context 분석:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 예시: 로깅 시스템에서 Strategy 패턴 적용 검토

// 현재 상황
public class Logger {
    public void log(String message, LogLevel level) {
        if (level == LogLevel.DEBUG) {
            System.out.println("[DEBUG] " + message);
        } else if (level == LogLevel.INFO) {
            writeToFile("[INFO] " + message);
        } else if (level == LogLevel.ERROR) {
            sendToSentry("[ERROR] " + message);
        }
    }
}

// Strategy 패턴 적용 가능성 평가

평가 기준:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
- 알고리즘이 여러 개인가? 
   → YES: DEBUG/INFO/ERROR 각각 다른 출력 방식

- 런타임에 알고리즘을 변경해야 하는가?
   → YES: 환경(개발/운영)에 따라 로깅 방식 변경

- 새로운 알고리즘이 추가될 가능성이 있는가?
   → YES: WARN 레벨, 외부 모니터링 시스템 연동 등

- 알고리즘들이 공통 인터페이스를 가질 수 있는가?
   → YES: log(String message) 인터페이스로 통일 가능

- 알고리즘들 간에 상태 공유가 필요한가?
   → NO: 각 로그 전략은 독립적

결론: Strategy 패턴 적용 적합

이 체크리스트가 유효한 이유는 각 질문이 GoF Structure 정의의 전제 조건을 거꾸로 검증하기 때문이다. “알고리즘 간 상태 공유가 필요한가?“라는 질문에 YES라고 답한다면, Strategy가 아니라 State 패턴을 검토해야 한다 — State는 알고리즘(상태)들이 서로의 존재를 알고 전이할 수 있다는 점에서 Strategy와 구조가 다르다. 즉 체크리스트의 각 항목은 임의의 경험칙이 아니라, 후보 패턴과 대안 패턴의 Structure 차이에서 직접 도출된 것이다.

대안 패턴 비교 분석

같은 문제를 해결하는 여러 패턴이 있을 때의 선택 기준:

캐싱 구현 시 패턴 선택:

 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
// 옵션 1: Proxy 패턴
public class CacheProxy implements DataService {
    private DataService realService;
    private Map<String, Object> cache = new HashMap<>();
    
    public Object getData(String key) {
        if (cache.containsKey(key)) {
            return cache.get(key);
        }
        Object data = realService.getData(key);
        cache.put(key, data);
        return data;
    }
}

// 옵션 2: Decorator 패턴 (캐싱 로직은 Proxy와 동일하므로 computeIfAbsent로 축약)
public class CacheDecorator implements DataService {
    private final DataService wrappedService;
    private final Map<String, Object> cache = new HashMap<>();

    public Object getData(String key) {
        return cache.computeIfAbsent(key, wrappedService::getData);
    }
}

// 옵션 3: Strategy 패턴
public class DataServiceContext {
    private CacheStrategy cacheStrategy;
    private DataService dataService;
    
    public Object getData(String key) {
        return cacheStrategy.getData(key, dataService);
    }
}

비교 분석:

기준ProxyDecoratorStrategy
투명성높음중간낮음
런타임 교체어려움어려움쉬움
다중 기능 조합어려움쉬움중간
성능 오버헤드낮음중간높음
구현 복잡도낮음중간높음

세 옵션의 차이는 결국 “언제 캐싱 여부가 결정되는가"의 문제로 요약된다. Proxy는 원본 서비스 접근을 아예 대체하므로 클라이언트가 캐싱의 존재를 몰라도 되고, Decorator는 여러 부가 기능(캐싱, 로깅, 압축)을 조합 가능한 계층으로 쌓을 수 있으며, Strategy는 캐싱 정책 자체를 런타임에 바꿀 수 있다. 세 패턴 모두 같은 인터페이스(DataService)를 구현하지만, 그 인터페이스 뒤에서 “무엇이 고정되고 무엇이 변하는가"에 대한 답이 다르다는 점이 패턴을 구분하는 진짜 기준이다.

1
2
3
4
선택 기준:
- 단순 캐싱만 필요 → Proxy
- 캐싱 + 로깅 + 압축 등 다중 기능 → Decorator  
- 캐싱 전략을 런타임에 변경 → Strategy

Trade-off 분석 프레임워크

Trade-off 분석의 이론적 뿌리는 컴퓨터과학의 오래된 원칙인 공간-시간 트레이드오프(space-time tradeoff)에 있다 — 같은 문제를 메모리를 더 써서 빠르게 풀거나, 시간을 더 써서 메모리를 아낄 수 있다는 것이다. 디자인 패턴 선택에서도 이 원칙은 그대로 적용된다. Flyweight는 메모리를 아끼는 대신 매번 HashMap 조회라는 시간 비용을 지불하고, Abstract Factory는 실행 시간 비용 없이 유연성을 얻는 대신 클래스 수라는 정적 복잡도 비용을 지불한다. 따라서 Trade-off 분석의 핵심 질문은 “무엇을 아끼고 무엇을 지불할 것인가"이지, “어느 쪽이 항상 더 낫다"가 아니다.

성능 vs 유연성 분석

Flyweight vs 일반 객체:

 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
// 일반 객체 방식: 성능 우수, 유연성 낮음
public class Character {
    private char character;
    private Font font;
    private Color color;
    private int x, y;
    
    // 10,000개 문자 = 10,000개 Font, Color 객체
}

// Flyweight 방식: 메모리 효율적, 복잡성 증가
public class CharacterFlyweight {
    private char character;
    private Font font;      // intrinsic state (공유)
    private Color color;    // intrinsic state (공유)
    
    public void render(int x, int y, Graphics g) {
        // extrinsic state는 파라미터로 전달
    }
}

public class CharacterFactory {
    private Map<String, CharacterFlyweight> flyweights = new HashMap<>();
    
    public CharacterFlyweight getFlyweight(char c, Font f, Color col) {
        String key = c + f.toString() + col.toString();
        return flyweights.computeIfAbsent(key, 
            k -> new CharacterFlyweight(c, f, col));
    }
}

성능 측정 데이터:

예시 수치이며 실제 측정치가 아닙니다. 플랫폼·JVM 버전에 따라 실제 값은 달라질 수 있습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
10,000개 문자 객체 생성 시:

일반 방식:
- 메모리 사용량: ~40MB (4KB × 10,000)
- 생성 시간: ~50ms
- 접근 시간: ~1ns (직접 접근)

Flyweight 방식:
- 메모리 사용량: ~1MB (공유 객체 + 팩토리)
- 생성 시간: ~20ms (중복 제거)
- 접근 시간: ~100ns (HashMap 조회)

결론: 메모리가 중요하면 Flyweight, 속도가 중요하면 일반 방식

이 수치가 예시임을 감안하더라도, Flyweight의 손익분기점은 일반적으로 “공유 가능한 상태(intrinsic state)의 비율"에 달려 있다. 문자 10,000개 중 실제 서로 다른 폰트·색상 조합이 20종류뿐이라면 Flyweight는 명확히 유리하지만, 10,000개가 전부 서로 다른 조합이라면 공유할 것이 없어 HashMap 조회 비용만 추가된다. 따라서 Flyweight 적용 여부를 결정하기 전에 실제 데이터의 중복률을 먼저 측정해야 한다.

복잡성 vs 재사용성 분석

Abstract Factory의 복잡성 증가:

 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
// 단순한 팩토리: 복잡성 낮음, 재사용성 낮음
public class ButtonFactory {
    public Button createButton(String os) {
        if ("Windows".equals(os)) {
            return new WindowsButton();
        } else if ("Mac".equals(os)) {
            return new MacButton();
        }
        throw new IllegalArgumentException("Unsupported OS");
    }
}

// Abstract Factory: 복잡성 높음, 재사용성 높음
public abstract class GUIFactory {
    public abstract Button createButton();
    public abstract Checkbox createCheckbox();
    public abstract Menu createMenu();
}

public class WindowsFactory extends GUIFactory {
    public Button createButton() { return new WindowsButton(); }
    public Checkbox createCheckbox() { return new WindowsCheckbox(); }
    public Menu createMenu() { return new WindowsMenu(); }
}

public class MacFactory extends GUIFactory {
    public Button createButton() { return new MacButton(); }
    public Checkbox createCheckbox() { return new MacCheckbox(); }
    public Menu createMenu() { return new MacMenu(); }
}

복잡성 매트릭스:

“이해 시간"은 실측 데이터가 아니라 상대적 난이도를 직관적으로 보여주기 위한 예시 추정치입니다. 실제 소요 시간은 개발자의 경험과 코드베이스 맥락에 따라 크게 달라집니다.

1
2
3
4
5
6
7
구현 방식          | 클래스 수 | Cyclomatic | 이해 시간(예시) | 확장 비용
Simple Factory    |    3     |     3      |     5분        |   높음
Abstract Factory  |    12    |     8      |     30분       |   낮음

비교 기준:
- 제품군이 2개 이하 → Simple Factory
- 제품군이 3개 이상이고 자주 확장 → Abstract Factory

메모리 vs 속도 Trade-off

Singleton vs Factory의 메모리 사용:

 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
// Singleton: 메모리 효율적, 전역 상태 위험
public class DatabaseConnection {
    private static DatabaseConnection instance;
    private Connection connection;
    
    public static DatabaseConnection getInstance() {
        if (instance == null) {
            synchronized (DatabaseConnection.class) {
                if (instance == null) {
                    instance = new DatabaseConnection();
                }
            }
        }
        return instance;
    }
    
    // 메모리: 1개 인스턴스만 유지
    // 동시성: synchronized로 인한 성능 저하
}

// Factory: 유연성 높음, 메모리 사용량 증가
public class ConnectionFactory {
    private Pool<Connection> connectionPool;
    
    public Connection getConnection() {
        return connectionPool.borrowObject();
    }
    
    // 메모리: N개 Connection 객체 유지
    // 동시성: Thread-safe pool 구현
}

성능 벤치마크:

예시 수치이며 실제 측정치가 아닙니다. 플랫폼·JVM 버전에 따라 실제 값은 달라질 수 있습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
동시 접속 1000명 시:

Singleton 방식:
- 메모리: 50MB (1개 Connection + 대기 큐)
- 평균 응답시간: 500ms (순차 처리)
- TPS: 2000

Connection Pool 방식:  
- 메모리: 200MB (20개 Connection pool)
- 평균 응답시간: 50ms (병렬 처리)  
- TPS: 20000

Trade-off: 메모리 4배 증가로 성능 10배 향상

이 비교가 시사하는 것은 Singleton의 문제가 패턴 자체가 아니라 “동시성 병목을 단일 자원으로 처리한다"는 설계 결정에 있다는 점이다. Connection Pool은 여러 Connection을 동시에 허용해 병목을 완화하지만, 그 대가로 각 Connection의 생명주기·반납 실패를 관리하는 복잡도를 추가로 떠안는다. 즉 Trade-off 분석의 결론은 언제나 “무엇을 제거했는가"가 아니라 “그 문제를 어디로 옮겼는가"를 확인하는 데 있다.

패턴 평가 매트릭스

패턴 평가 매트릭스는 다속성 의사결정(Multi-Criteria Decision Analysis, MCDA) 이론에서 가장 널리 쓰이는 가중합(weighted sum) 방식을 패턴 선택에 적용한 것이다. Hwang과 Yoon은 『Multiple Attribute Decision Making: Methods and Applications』(1981)에서 여러 평가 기준을 하나의 점수로 환산하는 다양한 기법을 체계화했는데, 가중합 방식은 그중 계산이 가장 단순하면서도 기준 간 상충(trade-off)을 명시적으로 드러낸다는 장점이 있다. 다만 이 방식은 기준들이 서로 독립적이라고 가정한다는 한계가 있다 — 실제로는 “코드 복잡도"와 “팀 숙련도 요구"처럼 기준끼리 강하게 상관된 경우가 많아, 가중치를 정할 때 이중 계산(double counting)을 피하도록 주의해야 한다.

정량적 평가 기준

패턴 평가 매트릭스 템플릿:

평가 기준가중치ObserverStrategyCommand
코드 복잡도 (낮을수록 좋음)25%684
성능 오버헤드 (낮을수록 좋음)20%597
확장 용이성 (높을수록 좋음)20%998
팀 숙련도 요구 (낮을수록 좋음)15%785
메모리 사용량 (낮을수록 좋음)10%697
테스트 용이성 (높을수록 좋음)10%899
1
2
3
4
가중 평균 계산:
Observer = (6×0.25 + 5×0.20 + 9×0.20 + 7×0.15 + 6×0.10 + 8×0.10) = 6.75
Strategy = (8×0.25 + 9×0.20 + 9×0.20 + 8×0.15 + 9×0.10 + 9×0.10) = 8.60
Command = (4×0.25 + 7×0.20 + 8×0.20 + 5×0.15 + 7×0.10 + 9×0.10) = 6.35

위 계산은 Strategy(8.60)가 Observer(6.75)와 Command(6.35)보다 높은 총점을 받았음을 보여주지만, 이 결과는 표에 설정된 가중치(코드 복잡도 25%, 성능 오버헤드 20% 등)를 전제로 한다. 만약 팀이 성능을 코드 복잡도보다 중요하게 여긴다면 가중치를 재조정해야 하고, 그 순간 순위가 뒤바뀔 수도 있다. 따라서 이 점수 자체보다 “어떤 가중치를 왜 선택했는가"를 팀이 합의하는 과정이 매트릭스의 진짜 가치다.

상황별 가중치 적용

프로젝트 특성에 따른 가중치 조정:

프로젝트 유형개발 속도코드 복잡도확장성유지보수성성능메모리 효율성
스타트업 초기 프로젝트40%30%20%-10%-
대규모 엔터프라이즈15%-35%25%25%-
실시간 시스템-10%15%-50%25%

가중치를 프로젝트 유형에 따라 다르게 설정한다는 것은 평가 기준 자체가 절대적이지 않다는 것을 인정하는 것이다. 스타트업 초기 프로젝트에서 개발 속도에 40%를 배정하는 이유는 성능이 덜 중요해서가 아니라, 시장 검증 이전에 아키텍처를 과도하게 다듬는 것이 더 큰 리스크이기 때문이다. 반대로 실시간 시스템에서 성능에 50%를 배정하는 것은 지연 시간 요구사항이 설계 실패의 기준(SLA 위반)과 직결되기 때문이다 — 가중치는 임의의 숫자가 아니라 “무엇이 실패했을 때 가장 큰 비용을 치르는가"에 대한 판단을 수치화한 것이다.

팀 특성 고려사항

팀 숙련도를 평가 기준에 포함하는 것은 패턴 자체의 우열이 아니라 팀과 패턴 사이의 적합도(fit)를 측정하기 위해서다. 같은 Abstract Factory라도 제네릭과 함수형 인터페이스에 익숙한 팀에게는 낮은 진입 장벽이지만, 객체지향 기초를 막 익힌 팀에게는 오히려 유지보수 리스크가 된다. 따라서 패턴 평가 매트릭스의 “팀 숙련도 요구” 항목은 패턴의 객관적 복잡도가 아니라, 그 패턴을 도입할 조직의 현재 역량과의 거리(gap)를 측정해야 한다.

팀 숙련도별 패턴 선택 가이드:

 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
// 초급 팀: 단순하고 직관적인 패턴 선호
public class SimpleFactory {
    public static Logger createLogger(String type) {
        switch (type) {
            case "file": return new FileLogger();
            case "console": return new ConsoleLogger();
            default: throw new IllegalArgumentException();
        }
    }
}

// 중급 팀: 적당한 복잡도의 패턴 활용 가능
public class LoggerBuilder {
    private String output;
    private String format;
    private LogLevel level;
    
    public LoggerBuilder output(String output) {
        this.output = output;
        return this;
    }
    
    public Logger build() {
        return new Logger(output, format, level);
    }
}

// 고급 팀: 복잡한 패턴도 효과적으로 활용
public class LoggerFactory {
    private Map<String, Supplier<Logger>> loggerSuppliers;
    
    public <T extends Logger> void registerLogger(
        String name, 
        Class<T> loggerClass,
        Function<Configuration, T> factory) {
        // 제네릭과 함수형 인터페이스를 활용한 고급 팩토리
    }
}

인지과학적 패턴 분석

청킹(Chunking)과 패턴 인식

청킹(chunking) 개념은 인지심리학자 George Miller가 「The Magical Number Seven, Plus or Minus Two」(1956)에서 처음 제시했고, 이후 de Groot(1946/1965)와 Chase & Simon의 「Perception in Chess」(Cognitive Psychology, 1973)에서 체스 마스터를 대상으로 실증됐다. 이들의 연구에 따르면 체스 마스터는 초보자보다 기억력이 뛰어난 것이 아니라, 개별 말의 위치가 아니라 “룩 앤 폰 엔드게임” 같은 의미 있는 패턴 단위로 보드를 인식한다 — 실제로 말이 무작위로 배치된 보드에서는 마스터와 초보자의 기억력 차이가 크게 줄어든다는 것이 Chase & Simon의 핵심 발견이었다. 숙련된 개발자가 20줄의 코드를 보자마자 “Observer 패턴"이라고 인식하는 것은 정확히 같은 메커니즘이다.

전문가의 패턴 인식 과정:

 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
// 초보자가 보는 것: 20줄의 개별 코드
public class WeatherStation {
    private List<Display> displays = new ArrayList<>();
    
    public void addDisplay(Display display) {
        displays.add(display);
    }
    
    public void removeDisplay(Display display) {
        displays.remove(display);
    }
    
    public void notifyDisplays() {
        for (Display display : displays) {
            display.update(temperature, humidity, pressure);
        }
    }
    
    public void measurementsChanged() {
        notifyDisplays();
    }
}

// 전문가가 보는 것: "Observer 패턴"
// → 즉시 다음 사항들을 추론:
//   - Subject-Observer 관계
//   - Push vs Pull 모델 (Push 사용)
//   - 느슨한 결합
//   - 확장 가능한 구조

패턴 인식 훈련법:

  1. 패턴 시그니처 학습: List<Observer> + notify() = Observer 패턴
  2. 의도 기반 분류: “일대다 의존성” → Observer
  3. 구조적 특징 암기: Subject, Observer, ConcreteSubject, ConcreteObserver
  4. 변형 패턴 인식: EventBus, Reactive Streams도 Observer의 변형

청킹된 단위가 안정적으로 재사용되려면 그 단위를 조직하는 구조, 즉 스키마(schema)가 필요하다. 스키마 개념은 영국 심리학자 Frederic Bartlett가 『Remembering: A Study in Experimental and Social Psychology』(1932)에서 처음 제안했고, 이후 Rumelhart가 「Schemata: The Building Blocks of Cognition」(1980)에서 지식 표상 이론으로 정교화했다. 아래 표는 Observer 패턴의 스키마를 구조적·행동적·적용적 세 층으로 나눠 보여준다 — 전문가가 패턴을 “인식"한 순간 활성화되는 것이 바로 이 세 층의 지식 묶음이다.

스키마 이론과 패턴 적용

패턴 스키마의 구성 요소:

1
2
3
4
5
6
7
Observer 패턴 스키마:
┌─ 구조적 스키마 ─┐    ┌─ 행동적 스키마 ─┐    ┌─ 적용 스키마 ─┐
│ Subject         │    │ attach()       │    │ 상태 변경     │
│ Observer        │    │ detach()       │    │ 통지 필요     │
│ ConcreteSubject │    │ notify()       │    │ 일대다 관계   │
│ ConcreteObserver│    │ update()       │    │ 느슨한 결합   │
└─────────────────┘    └─────────────────┘    └─────────────────┘

스키마 활성화 트리거:

  • “변경 사항을 여러 곳에 알려야 한다” → Observer 스키마 활성화
  • “알고리즘을 바꿔가며 사용해야 한다” → Strategy 스키마 활성화
  • “복잡한 객체를 단계별로 만들어야 한다” → Builder 스키마 활성화

패턴의 진화적 관점

패턴이 언어와 시대에 따라 변형되는 현상은 우연이 아니라 소프트웨어 진화의 일반 법칙을 따른다. Meir Lehman은 「Programs, Life Cycles, and Laws of Software Evolution」(Proceedings of the IEEE, 1980)에서 사용되는 소프트웨어는 계속 변경되어야 한다는 “지속적 변경의 법칙"과, 그 변경 과정에서 복잡도가 계속 증가한다는 “복잡도 증가의 법칙"을 제시했다. Singleton이 Service Locator를 거쳐 Dependency Injection으로 진화한 과정은 후자의 법칙이 설계 패턴 수준에서 나타난 사례로 볼 수 있다 — 전역 상태의 문제를 해결하려는 시도 자체가 새로운 복잡도(컨테이너, 설정 파일)를 낳았다.

언어별 패턴 적응

JavaScript에서의 Observer 패턴 진화:

 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
// 전통적 Observer (Java 스타일)
class Subject {
    constructor() {
        this.observers = [];
    }
    
    attach(observer) {
        this.observers.push(observer);
    }
    
    notify(data) {
        this.observers.forEach(observer => observer.update(data));
    }
}

// JavaScript 관용적 Observer (EventEmitter)
const EventEmitter = require('events');

class WeatherStation extends EventEmitter {
    updateWeather(data) {
        this.emit('weatherChanged', data);
    }
}

const station = new WeatherStation();
station.on('weatherChanged', data => console.log(data));

// 현대적 Reactive Observer (RxJS)
import { Subject } from 'rxjs';

const weatherSubject = new Subject();
const subscription = weatherSubject.subscribe({
    next: data => console.log(data),
    error: err => console.error(err),
    complete: () => console.log('Complete')
});

JavaScript의 세 가지 구현이 보여주는 것은 Observer 패턴의 Intent(일대다 의존성 통지)는 동일하게 유지되면서, Structure만 언어의 관용구에 맞게 바뀐다는 사실이다. EventEmitter는 Node.js 런타임이 이미 제공하는 표준 라이브러리이므로 Subject 클래스를 직접 구현할 필요가 없고, RxJS의 Subject는 여기에 연산자 체이닝(operator chaining)이라는 함수형 개념을 더해 통지 스트림 자체를 가공할 수 있게 확장한 것이다. 패턴을 “번역"할 때는 언어 고유의 표준 라이브러리를 먼저 찾아보는 것이 새로 구현하는 것보다 우선되어야 한다.

언어별 패턴 적응 원칙:

  • C++: RAII와 결합된 패턴 (스마트 포인터 활용)
  • Python: Duck Typing 활용한 간소화된 패턴
  • Rust: 소유권 시스템과 조화되는 패턴 변형
  • Go: 인터페이스 기반 간소화된 패턴

언어별 적응 원칙을 관통하는 공통 기준은 “그 언어가 이미 언어 차원에서 해결해 준 문제를 패턴으로 다시 구현하지 않는다"는 것이다. Python의 Duck Typing은 Strategy가 요구하는 공통 인터페이스를 명시적 선언 없이도 충족시키고, Go의 인터페이스는 묵시적 구현(implicit implementation)을 지원해 Adapter 패턴의 상당 부분을 문법 차원에서 흡수한다. 패턴을 언어에 이식할 때 원본 구조를 그대로 옮기려는 시도가 오히려 그 언어의 관용구에 반하는 과잉 설계가 되는 경우가 많다.

패턴의 자연적 진화

Singleton → Service Locator → Dependency Injection 진화:

 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
// 1세대: Singleton (1990년대)
public class DatabaseConnection {
    private static DatabaseConnection instance;
    // 전역 상태, 테스트 어려움
}

// 2세대: Service Locator (2000년대)  
public class ServiceLocator {
    private static Map<Class<?>, Object> services = new HashMap<>();
    
    public static <T> T getService(Class<T> serviceClass) {
        return (T) services.get(serviceClass);
    }
    // 중앙집중식 관리, 여전히 전역 상태
}

// 3세대: Dependency Injection (2010년대~)
@Component
public class OrderService {
    private final PaymentService paymentService;
    
    @Autowired
    public OrderService(PaymentService paymentService) {
        this.paymentService = paymentService;
    }
    // 명시적 의존성, 테스트 용이
}

세 세대 모두 “필요한 객체를 어떻게 구할 것인가"라는 같은 Intent를 공유하지만, Structure가 세대를 거치며 정반대 방향으로 바뀌었다는 점이 흥미롭다 — Singleton과 Service Locator는 객체가 “자신을 찾아오라"고 요구하는 pull 방식인 반면, Dependency Injection은 컨테이너가 “필요한 객체를 미리 넣어준다"는 push 방식이다. 이 전환은 앞서 다룬 Trade-off 분석의 실제 사례이기도 하다 — 테스트 용이성을 얻는 대신 컨테이너 설정이라는 새로운 학습 비용을 지불한 것이다.

실전 패턴 분석 예제

케이스 스터디: 로깅 시스템 설계

요구사항:

  • 다양한 출력 대상 (콘솔, 파일, 네트워크)
  • 로그 레벨별 필터링
  • 포맷 커스터마이징
  • 성능 최적화 (비동기 처리)

이 요구사항은 하나의 패턴으로 전부 해결되지 않는다는 점이 실무 설계의 전형적인 어려움이다. 각 요구사항을 개별 패턴에 매핑하기 전에, 어떤 요구사항이 “구조적 결합"의 문제이고 어떤 것이 “행동의 변화"의 문제인지 구분하는 것이 먼저다 — 출력 대상 다양화는 Strategy가 다루는 행동의 변화이고, 필터 조합은 Chain of Responsibility가 다루는 구조적 흐름 제어이며, 설정 옵션이 많은 것은 Builder가 다루는 생성의 복잡도다. 이렇게 요구사항을 문제 유형별로 먼저 분해하는 것이 이 글 전체에서 다룬 “문제 영역 식별” 절차의 실전 적용이다.

패턴 적용 분석:

 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
// Strategy 패턴: 출력 전략
public interface LogAppender {
    void append(LogEvent event);
}

public class ConsoleAppender implements LogAppender {
    public void append(LogEvent event) {
        System.out.println(event.getMessage());
    }
}

public class AsyncAppender implements LogAppender {
    private final LogAppender delegate;
    private final BlockingQueue<LogEvent> queue = new LinkedBlockingQueue<>();
    
    public AsyncAppender(LogAppender delegate) {
        this.delegate = delegate;
        startBackgroundThread();
    }
    
    public void append(LogEvent event) {
        queue.offer(event);  // 비동기 처리
    }
}

// Chain of Responsibility: 필터 체인
public abstract class LogFilter {
    protected LogFilter next;
    
    public void setNext(LogFilter next) {
        this.next = next;
    }
    
    public final void filter(LogEvent event) {
        if (shouldProcess(event)) {
            process(event);
            if (next != null) {
                next.filter(event);
            }
        }
    }
    
    protected abstract boolean shouldProcess(LogEvent event);
    protected abstract void process(LogEvent event);
}

// Builder 패턴: 로거 구성
public class LoggerBuilder {
    private List<LogAppender> appenders = new ArrayList<>();
    private LogLevel level = LogLevel.INFO;
    private LogFormatter formatter = new SimpleFormatter();
    
    public LoggerBuilder addAppender(LogAppender appender) {
        this.appenders.add(appender);
        return this;
    }
    
    public LoggerBuilder level(LogLevel level) {
        this.level = level;
        return this;
    }
    
    public Logger build() {
        return new Logger(appenders, level, formatter);
    }
}

패턴 선택 근거:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Strategy (LogAppender):
- 출력 방식이 다양함
- 런타임 교체 필요 없음 (설정 시점에 결정)
- 각 전략이 독립적

Chain of Responsibility (LogFilter):  
- 여러 필터를 조합해야 함
- 필터 순서가 중요함
- 동적으로 필터 체인 구성 가능

Builder (LoggerBuilder):
- 설정 옵션이 많음
- 선택적 매개변수 지원
- 불변 객체 생성

이 로깅 시스템 사례가 보여주듯, 실무에서는 하나의 요구사항 뭉치 안에 여러 패턴이 공존한다. 세 패턴을 조합할 때 주의할 점은 각 패턴의 경계를 명확히 유지하는 것이다 — LogAppender가 필터링 로직까지 떠맡거나 LoggerBuilder가 실제 로그 출력을 직접 수행하기 시작하면, 패턴 간 책임이 뒤섞이면서 이 글 전체에서 강조한 “참여자별 단일 책임"이 무너진다.

한눈에 보는 패턴 분석 프레임워크

패턴 분석 체크리스트

분석 단계핵심 질문확인 항목
Intent 분석이 패턴이 해결하는 핵심 문제는?문제 정의 명확성, 범위 적절성, 해법 간결성
Structure 분석역할 분담은 적절한가?의존성 방향, 교체 메커니즘, 위임 패턴
Participants 분석각 참여자의 책임은 명확한가?단일 책임, 알아야 할 것/몰라도 될 것
Collaborations 분석상호작용은 적절한가?협력 시작점, 순서 중요성, 실패 처리

패턴 적용 결정 매트릭스

문제 유형1차 후보2차 후보선택 기준
객체 생성이 복잡함Factory MethodAbstract Factory, Builder제품군 수, 생성 단계 복잡도
객체 생성 비용이 높음SingletonFlyweight, Object Pool인스턴스 공유 가능 여부
런타임 행동 변경StrategyState, Command상태 의존성, 실행 취소 필요
복잡한 구조 단순화FacadeAdapter, Proxy서브시스템 수, 호환성 문제
일대다 의존성ObserverMediator, Event Bus통신 방향, 결합도 요구사항
알고리즘 캡슐화Template MethodStrategy, Command알고리즘 골격 고정 여부

Trade-off 분석 기준

기준설명고려 사항
성능 vs 유연성런타임 오버헤드와 확장성의 균형메모리 사용량, 접근 시간, HashMap 조회 비용
복잡성 vs 재사용성클래스 수 증가와 확장 비용의 균형Cyclomatic 복잡도, 이해 시간, 확장 비용
투명성 vs 교체 용이성기존 코드 영향과 런타임 변경의 균형컴파일타임 의존성, 인터페이스 변경

패턴 평가 매트릭스 템플릿

평가 기준가중치 예시측정 방법
코드 복잡도25%Cyclomatic Complexity, 클래스 수
성능 오버헤드20%벤치마크, 메모리 프로파일링
확장 용이성20%새 기능 추가 시 변경 파일 수
팀 숙련도 요구15%이해 시간, 러닝 커브
메모리 사용량10%힙 메모리 분석
테스트 용이성10%Mock 필요 수, 테스트 코드 라인

프로젝트 유형별 가중치 조정

프로젝트 유형개발 속도확장성성능유지보수성
스타트업 초기40%20%10%30%
대규모 엔터프라이즈15%35%25%25%
실시간 시스템10%15%50%25%
레거시 유지보수20%15%15%50%

흔한 오개념

패턴 분석 프레임워크를 처음 접할 때 빠지기 쉬운 오해들입니다.

  • “Intent만 알면 충분하다”: Intent는 패턴의 출발점일 뿐입니다. 같은 Intent(“일대다 의존성 통지”)를 가진 상황도 Push/Pull 모델, 실패 처리 방식, 순환 참조 위험에 따라 실제 구현이 크게 달라집니다. Structure·Participants·Collaborations까지 함께 분석해야 실무에 적용할 수 있습니다.
  • “평가 매트릭스 점수가 절대 기준이다”: 앞서 본 가중 평균 계산은 특정 프로젝트를 가정한 예시 산출값입니다. 가중치 자체가 팀·프로젝트 유형에 따라 달라지므로, 매트릭스는 “무엇을 근거로 비교했는지"를 드러내는 도구이지 정답을 알려주는 공식이 아닙니다.
  • “복잡한 패턴일수록 더 좋은 설계다”: Abstract Factory가 Simple Factory보다 항상 우월한 것은 아닙니다. 제품군이 2개 이하인 상황에서 Abstract Factory를 적용하면 불필요한 클래스만 늘어납니다. 패턴의 가치는 복잡도가 아니라 문제와의 적합성에서 나옵니다.
  • “Observer들은 등록된 순서대로 항상 통지받는다”: 대부분의 구현에서 List 기반 순회는 등록 순서를 우연히 재현할 뿐, GoF 명세 어디에도 순서 보장은 없습니다. 내부 컬렉션을 HashSet으로 바꾸거나 병렬 스트림으로 순회하면 순서가 깨지는데, 이런 재현하기 어려운 버그는 Collaborations 분석에서 “순서가 중요한가?“를 먼저 확인하지 않았을 때 발생합니다.

평가 기준

이 글을 읽고 나면 다음 항목들을 스스로 점검해볼 수 있습니다.

  • GoF 패턴 템플릿의 Intent, Structure, Participants, Collaborations 4가지 분석 축을 각각 설명할 수 있다.
  • 동일한 문제(예: 캐싱)를 여러 패턴(Proxy, Decorator, Strategy)으로 구현했을 때의 Trade-off를 비교할 수 있다.
  • 패턴 평가 매트릭스에서 가중 평균을 계산하는 방법을 이해하고 프로젝트 유형별로 가중치를 조정할 수 있다.
  • 청킹(Chunking)과 스키마(Schema) 이론이 패턴 인식과 어떻게 연결되는지 설명할 수 있다.
  • 성능·벤치마크 수치가 예시 자료인지 실측 데이터인지 구분하고, 실제 적용 전 자체 측정이 필요한 이유를 설명할 수 있다.

결론: 패턴 분석을 마스터하기

패턴 분석 능력은 하루아침에 기를 수 있는 것이 아닙니다. 하지만 체계적인 프레임워크를 따라 꾸준히 연습하면 다음과 같은 실천 단계를 통해 전문가적 사고력을 개발할 수 있습니다.

  1. 문제 본질 파악 + 분석 일지 작성: “정말 해결해야 할 핵심 문제는 무엇인가?“를 매일 마주친 패턴과 함께 기록한다.
  2. 패턴 후보 선별 + 코드 리뷰 적용: “이 문제를 해결할 수 있는 패턴들은?“을 코드 리뷰에서 “이 코드에 숨어있는 패턴은?“이라는 질문으로 검증한다.
  3. Trade-off 분석 + 적용 후기 작성: “각 패턴의 장단점과 적용 비용은?“을 선택 후 실제로 얼마나 효과적이었는지 회고로 남긴다.
  4. 상황적 적합성 판단: “우리 팀과 프로젝트에 가장 적합한 것은?“을 팀 숙련도와 프로젝트 특성에 맞춰 결정한다.
  5. 진화 가능성 검토 + 다양한 구현 실험: “미래 요구사항 변화에 어떻게 대응할 것인가?“를 같은 패턴을 다른 언어로 구현해보며 확인한다.

패턴을 올바르게 분석하고 평가하는 능력은 패턴을 단순히 아는 것보다 훨씬 중요하며, 이것이 진정한 설계 전문가와 코드 작성자를 구분하는 핵심 역량입니다. 이는 설계 사고력의 핵심이며, 진정한 소프트웨어 아키텍트로 성장하는 발판이 됩니다.

다음 글에서는 이런 분석 능력을 바탕으로 객체지향 설계의 본질을 더 깊이 탐구해보겠습니다. 패턴은 결국 좋은 객체지향 설계 원칙들의 구체적 표현이기 때문입니다.