GoF 디자인 패턴 정리

GoF의 23가지 디자인 패턴을 생성, 구조, 행위 패턴으로 분류해 정리합니다.

디자인 패턴이란?

디자인 패턴(Design Pattern)은 소프트웨어 설계에서 자주 발생하는 문제들에 대한 재사용 가능한 해결책입니다. GoF(Gang of Four)가 1994년 저서에서 23가지 패턴을 정리했으며, 크게 세 가지로 분류됩니다.

분류 목적
생성(Creational) 객체 생성 방식을 다룸
구조(Structural) 클래스/객체 합성 방식을 다룸
행위(Behavioral) 객체 간 책임 분배와 협력을 다룸

생성 패턴 (Creational Patterns)

객체를 어떻게 생성하고 어떤 방식으로 조합할지에 관한 패턴입니다.

Singleton (싱글톤)

인스턴스를 하나만 생성해 전역 접근점을 제공합니다.

class Database {
  private static instance: Database;

  private constructor() {}

  static getInstance(): Database {
    if (!Database.instance) {
      Database.instance = new Database();
    }
    return Database.instance;
  }

  query(sql: string) {
    console.log(`Query: ${sql}`);
  }
}

const db1 = Database.getInstance();
const db2 = Database.getInstance();
console.log(db1 === db2); // true

사용 예시: DB 커넥션, 로그 관리, 설정 객체


Factory Method (팩토리 메서드)

객체 생성을 서브클래스에 위임해 어떤 클래스의 인스턴스를 생성할지 서브클래스가 결정합니다.

abstract class Notifier {
  abstract createMessage(): Message;

  send(text: string) {
    const msg = this.createMessage();
    msg.deliver(text);
  }
}

class EmailNotifier extends Notifier {
  createMessage(): Message {
    return new EmailMessage();
  }
}

class SMSNotifier extends Notifier {
  createMessage(): Message {
    return new SMSMessage();
  }
}

사용 예시: 다양한 타입의 객체를 생성해야 할 때, 생성 로직을 분리하고 싶을 때


Abstract Factory (추상 팩토리)

관련된 객체군을 일관되게 생성하는 인터페이스를 제공합니다.

interface UIFactory {
  createButton(): Button;
  createCheckbox(): Checkbox;
}

class WindowsFactory implements UIFactory {
  createButton() { return new WindowsButton(); }
  createCheckbox() { return new WindowsCheckbox(); }
}

class MacFactory implements UIFactory {
  createButton() { return new MacButton(); }
  createCheckbox() { return new MacCheckbox(); }
}

사용 예시: 크로스 플랫폼 UI, 테마 변경, DB 드라이버 교체


Builder (빌더)

복잡한 객체를 단계적으로 조립할 수 있게 합니다.

class QueryBuilder {
  private table = '';
  private conditions: string[] = [];
  private limit?: number;

  from(table: string) {
    this.table = table;
    return this;
  }

  where(condition: string) {
    this.conditions.push(condition);
    return this;
  }

  take(n: number) {
    this.limit = n;
    return this;
  }

  build(): string {
    let query = `SELECT * FROM ${this.table}`;
    if (this.conditions.length) query += ` WHERE ${this.conditions.join(' AND ')}`;
    if (this.limit) query += ` LIMIT ${this.limit}`;
    return query;
  }
}

const query = new QueryBuilder()
  .from('users')
  .where('age > 18')
  .take(10)
  .build();

사용 예시: SQL 빌더, HTTP 요청 객체, 복잡한 설정 객체 생성


Prototype (프로토타입)

기존 객체를 복제해 새 객체를 만듭니다.

interface Cloneable {
  clone(): this;
}

class Config implements Cloneable {
  constructor(public host: string, public port: number) {}

  clone(): this {
    return Object.assign(Object.create(Object.getPrototypeOf(this)), this);
  }
}

const base = new Config('localhost', 3000);
const dev = base.clone();
dev.port = 8080;

사용 예시: 객체 생성 비용이 클 때, 원본을 유지하며 변형이 필요할 때


구조 패턴 (Structural Patterns)

클래스나 객체를 조합해 더 큰 구조를 만드는 패턴입니다.

Adapter (어댑터)

호환되지 않는 인터페이스를 연결해주는 변환기입니다.

// 기존 인터페이스
class OldLogger {
  writeLog(msg: string) { console.log(`[OLD] ${msg}`); }
}

// 새 인터페이스
interface Logger {
  log(msg: string): void;
}

// 어댑터
class LoggerAdapter implements Logger {
  constructor(private old: OldLogger) {}

  log(msg: string) {
    this.old.writeLog(msg);
  }
}

사용 예시: 레거시 코드 통합, 외부 라이브러리 래핑


Decorator (데코레이터)

동적으로 기능을 추가합니다. 상속 대신 합성을 사용합니다.

interface TextProcessor {
  process(text: string): string;
}

class PlainText implements TextProcessor {
  process(text: string) { return text; }
}

class BoldDecorator implements TextProcessor {
  constructor(private wrapped: TextProcessor) {}
  process(text: string) { return `<b>${this.wrapped.process(text)}</b>`; }
}

class ItalicDecorator implements TextProcessor {
  constructor(private wrapped: TextProcessor) {}
  process(text: string) { return `<i>${this.wrapped.process(text)}</i>`; }
}

const text = new ItalicDecorator(new BoldDecorator(new PlainText()));
console.log(text.process('Hello')); // <i><b>Hello</b></i>

사용 예시: 미들웨어, 스트림 처리, UI 컴포넌트 확장


Facade (퍼사드)

복잡한 서브시스템을 단순한 인터페이스로 감쌉니다.

class HomeTheater {
  private tv = new TV();
  private sound = new SoundSystem();
  private lights = new Lights();

  watchMovie() {
    this.lights.dim(30);
    this.tv.on();
    this.sound.setVolume(50);
  }

  endMovie() {
    this.tv.off();
    this.sound.off();
    this.lights.bright();
  }
}

사용 예시: SDK/라이브러리 설계, 복잡한 API 단순화


Composite (컴포지트)

개별 객체와 복합 객체를 동일하게 다룹니다. 트리 구조 표현에 적합합니다.

interface FileSystemItem {
  getName(): string;
  getSize(): number;
}

class File implements FileSystemItem {
  constructor(private name: string, private size: number) {}
  getName() { return this.name; }
  getSize() { return this.size; }
}

class Folder implements FileSystemItem {
  private items: FileSystemItem[] = [];
  constructor(private name: string) {}
  add(item: FileSystemItem) { this.items.push(item); }
  getName() { return this.name; }
  getSize() { return this.items.reduce((sum, i) => sum + i.getSize(), 0); }
}

사용 예시: 파일 시스템, DOM 트리, 메뉴 구조


Proxy (프록시)

객체에 대한 접근을 제어하는 대리인입니다.

interface Image {
  display(): void;
}

class RealImage implements Image {
  constructor(private filename: string) {
    console.log(`Loading ${filename}...`);
  }
  display() { console.log(`Displaying ${this.filename}`); }
}

class ProxyImage implements Image {
  private real?: RealImage;
  constructor(private filename: string) {}

  display() {
    if (!this.real) {
      this.real = new RealImage(this.filename); // 지연 로딩
    }
    this.real.display();
  }
}

사용 예시: 지연 로딩, 캐싱, 접근 권한 제어


Bridge (브리지)

구현부를 추상부로부터 분리해 각각 독립적으로 확장할 수 있게 합니다.

interface Renderer {
  render(shape: string): string;
}

class SVGRenderer implements Renderer {
  render(shape: string) { return `<svg>${shape}</svg>`; }
}

class CanvasRenderer implements Renderer {
  render(shape: string) { return `canvas.draw(${shape})`; }
}

abstract class Shape {
  constructor(protected renderer: Renderer) {}
  abstract draw(): string;
}

class Circle extends Shape {
  draw() { return this.renderer.render('circle'); }
}

사용 예시: 플랫폼 독립적 렌더링, 드라이버 분리


Flyweight (플라이웨이트)

공유를 통해 메모리 사용량을 줄입니다. 수많은 유사 객체를 다룰 때 유용합니다.

사용 예시: 게임의 총알/파티클, 문서의 글자 객체


행위 패턴 (Behavioral Patterns)

객체 간의 알고리즘과 책임 분배에 관한 패턴입니다.

Observer (옵저버)

객체의 상태 변화를 여러 구독자에게 자동 통보합니다.

interface Observer {
  update(data: unknown): void;
}

class EventEmitter {
  private listeners: Map<string, Observer[]> = new Map();

  subscribe(event: string, observer: Observer) {
    if (!this.listeners.has(event)) this.listeners.set(event, []);
    this.listeners.get(event)!.push(observer);
  }

  emit(event: string, data: unknown) {
    this.listeners.get(event)?.forEach(o => o.update(data));
  }
}

사용 예시: 이벤트 시스템, MVC의 View 갱신, 실시간 알림


Strategy (전략)

알고리즘을 캡슐화하고 런타임에 교체할 수 있게 합니다.

interface SortStrategy {
  sort(data: number[]): number[];
}

class BubbleSort implements SortStrategy {
  sort(data: number[]) { /* ... */ return data; }
}

class QuickSort implements SortStrategy {
  sort(data: number[]) { /* ... */ return data; }
}

class Sorter {
  constructor(private strategy: SortStrategy) {}

  setStrategy(strategy: SortStrategy) { this.strategy = strategy; }

  execute(data: number[]) { return this.strategy.sort(data); }
}

사용 예시: 정렬 알고리즘, 결제 수단, 압축 방식 변경


Command (커맨드)

요청을 객체로 캡슐화해 큐잉, 로깅, 취소(undo)가 가능하게 합니다.

interface Command {
  execute(): void;
  undo(): void;
}

class TextEditor {
  private history: Command[] = [];

  run(cmd: Command) {
    cmd.execute();
    this.history.push(cmd);
  }

  undoLast() {
    this.history.pop()?.undo();
  }
}

사용 예시: Ctrl+Z 실행 취소, 매크로 기록, 작업 큐


Template Method (템플릿 메서드)

알고리즘의 골격을 정의하고, 세부 단계는 서브클래스가 구현합니다.

abstract class DataParser {
  // 템플릿 메서드
  parse(raw: string) {
    const data = this.readData(raw);
    const processed = this.processData(data);
    return this.formatOutput(processed);
  }

  abstract readData(raw: string): unknown;
  abstract processData(data: unknown): unknown;

  formatOutput(data: unknown): string {
    return JSON.stringify(data);
  }
}

사용 예시: 데이터 파이프라인, 테스트 프레임워크, 게임 루프


State (상태)

객체의 내부 상태에 따라 행동이 달라지도록 합니다. 조건문 대신 상태 객체를 사용합니다.

interface TrafficState {
  handle(light: TrafficLight): void;
}

class RedState implements TrafficState {
  handle(light: TrafficLight) {
    console.log('Stop!');
    light.setState(new GreenState());
  }
}

class GreenState implements TrafficState {
  handle(light: TrafficLight) {
    console.log('Go!');
    light.setState(new RedState());
  }
}

사용 예시: 주문 상태 관리, 게임 캐릭터 AI, UI 컴포넌트 상태


Iterator (이터레이터)

컬렉션의 내부 구조를 드러내지 않고 순차적으로 접근하게 합니다.

사용 예시: JavaScript의 for...of, 커서 기반 DB 결과 순회


Chain of Responsibility (책임 연쇄)

요청을 처리할 수 있는 객체를 체인으로 연결해 순서대로 처리를 시도합니다.

abstract class Handler {
  private next?: Handler;

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

  handle(request: number): string {
    if (this.next) return this.next.handle(request);
    return 'Unhandled';
  }
}

class LowHandler extends Handler {
  handle(request: number) {
    if (request < 10) return `Low handled: ${request}`;
    return super.handle(request);
  }
}

사용 예시: HTTP 미들웨어, 이벤트 버블링, 예외 처리 체인


Mediator (중재자)

객체 간 직접 통신을 줄이고 중재자를 통해 소통하게 합니다.

사용 예시: 채팅방, MVC의 Controller, 항공 관제 시스템


Memento (메멘토)

객체의 상태를 저장하고 복원합니다. 내부 캡슐화를 깨지 않습니다.

사용 예시: 스냅샷, 게임 세이브, 실행 취소 히스토리


Visitor (방문자)

데이터 구조와 연산을 분리합니다. 구조는 그대로 두고 새 연산을 추가할 수 있습니다.

사용 예시: AST 탐색, 문서 내보내기(HTML/PDF/XML), 컴파일러


Interpreter (인터프리터)

언어 문법을 클래스로 표현하고 해석기를 구현합니다.

사용 예시: 정규식, SQL 파서, 수식 계산기


정리

분류 패턴
생성 Singleton, Factory Method, Abstract Factory, Builder, Prototype
구조 Adapter, Decorator, Facade, Composite, Proxy, Bridge, Flyweight
행위 Observer, Strategy, Command, Template Method, State, Iterator, Chain of Responsibility, Mediator, Memento, Visitor, Interpreter

디자인 패턴은 외우는 것보다 "언제 쓰는가"를 이해하는 것이 중요합니다. 실제 코드를 작성하면서 문제 상황과 패턴을 연결짓는 연습을 반복하면 자연스럽게 익힐 수 있습니다.