디자인 패턴이란 무엇인가: GoF 23패턴과 분류 체계

GoF가 정리한 23개 디자인 패턴의 역사와 생성·구조·행위 3계열 분류, SOLID와의 관계, 적용 시점 판단 기준을 실무 관점에서 정리한다.

2026-08-13 · 최초 발행 2025-10-14

같은 문제를 팀마다 다른 방식으로 풀고 있다면, 코드를 고치기 전에 먼저 언어부터 맞출 필요가 있다. 디자인 패턴은 반복해서 등장하는 설계 문제에 대해 이미 검증된 해법을 카탈로그 형태로 모아둔 것이다. 특정 언어나 프레임워크에 매이지 않고, 역할·책임·협력이라는 관점에서 설계를 이야기할 수 있게 해준다.

GoF와 패턴의 계보

패턴 하나는 문제(Context/Forces), 해법(Structure/Participants/Collaboration), 결과(Consequences/Trade-offs), 그리고 구현 시 고려사항과 예시로 구성된다. 이 형식을 업계 표준으로 굳힌 것이 GoF(Gang of Four, Gamma·Helm·Johnson·Vlissides)다. 이들은 1994년 저서 Design Patterns: Elements of Reusable Object-Oriented Software에서 23개 패턴을 정리했고, 이후 객체지향 설계 품질을 논할 때 쓰는 공통 어휘와 카탈로그를 업계에 제공했다.

분류 체계: 생성·구조·행위

GoF 패턴은 세 계열로 나뉜다.

  • 생성(Creational): 객체 생성 과정의 캡슐화와 생명주기 관리에 초점을 둔다.
  • 구조(Structural): 클래스와 객체를 조합해 구조를 형성하고 관계를 단순화한다.
  • 행위(Behavioral): 객체 간 상호작용, 책임 분배, 알고리즘의 변동성 관리를 다룬다.

이 분류는 SOLID·DRY·KISS 같은 설계 원칙과도 맞물린다. SOLID 관점에서 보면 패턴은 SRP·OCP·DIP 같은 원칙을 구체적인 코드 구조로 실현하는 메커니즘이다. 예를 들어 Strategy는 OCP를 실천하는 수단이고, Factory Method는 DIP를 촉진한다. DRY 관점에서는 중복 로직을 공통 추상으로 끌어올려 제어하는데, Facade와 Template Method가 대표적이다. 다만 KISS 원칙이 경고하듯 불필요한 복잡도는 억제해야 하며, 패턴 적용은 문제의 복잡도 대비 순이익이 명확할 때만 선택해야 한다.

언제 도입을 고려하는가

패턴이 실제로 유리하게 작동하는 상황은 구체적이다. 전략 교체형 로직에서는 Strategy를 적용해 if-else 분기를 축소하고, 신규 정책을 추가할 때 기존 코드 수정을 최소화(OCP)할 수 있다. 여러 서브시스템을 통합해야 할 때는 Facade로 복잡한 호출 순서를 은닉해 클라이언트 코드를 단순화하고 회귀 결함을 줄일 수 있다. 이벤트 중심 UI나 도메인에서는 Observer로 느슨한 결합을 구현해, 구독자를 추가하거나 해지할 때 영향 범위를 최소화할 수 있다.

적용 시점은 다음 신호로 판단한다.

  • 코드 스멜: 거대한 조건문, 중복된 생성 로직, 과도한 서브클래싱, 침투적 의존성이 관측될 때
  • 변경 요구의 빈도와 불확실성: 알고리즘이나 정책의 변동성이 높을수록 Strategy·State·Template Method가 유리하다
  • 경계 관리 필요: 외부 시스템이나 레거시와 연동할 때는 Adapter·Facade·Proxy를 고려한다

남용과 안티패턴

패턴을 도입한다고 항상 이득인 것은 아니다. 과도한 추상화는 오버엔지니어링으로 이어져 학습 비용과 디버깅 난이도를 높이고 성능을 저하시킬 수 있다. 모든 문제에 특정 패턴을 무비판적으로 적용하는 Golden Hammer도 위험하다. 패턴은 본래 책임 분리를 강화하는 도구이지만, 잘못 적용하면 오히려 God Object처럼 책임이 한 곳에 몰리거나 Anemic Domain Model처럼 도메인 모델이 빈약해지는 안티패턴으로 퇴행할 수 있다.

적용 절차

패턴 도입은 요구 정의부터 회귀 검증까지 하나의 절차로 다룰 수 있다.

1~2개순이익 충분불충분요구/문제 정의코드 스멜/변동성 분석적합 패턴 후보 선정스파이크 구현/POC성능/복잡도/테스트성 평가리팩터링 적용현상 유지/대안 모색문서화/코딩 규약 반영회귀 테스트/운영 검증

GoF 패턴 한눈에 보기

아래 표는 언어에 무관하게 23개 패턴의 분류와 역할을 정리한 것이다. 예시 코드는 Java 또는 의사코드 기반의 단문 호출 예다.

패턴명 분류 주요 역할 예시 코드
Singleton(싱글톤) 생성 인스턴스 단일화와 전역 접근 제어 Singleton.getInstance()
Factory Method(팩토리 메서드) 생성 생성 로직 위임을 통한 서브타입 결정 creator.createProduct()
Abstract Factory(추상 팩토리) 생성 관련 객체군의 일관된 생성 factory.createButton()
Builder(빌더) 생성 단계적 조립으로 복잡 객체 생성 new Builder().setX(1).build()
Prototype(프로토타입) 생성 복제 기반 객체 생성과 비용 절감 proto.clone()
Adapter(어댑터) 구조 인터페이스 호환성 변환 new Adapter(adaptee).request()
Bridge(브리지) 구조 추상화와 구현 분리, 독립 확장 new RefinedAbstraction(impl).op()
Composite(컴포지트) 구조 부분-전체 계층 표현과 일관된 연산 root.add(child); root.operation()
Decorator(데코레이터) 구조 기능의 동적 부가, 조합적 확장 new LoggingDecorator(comp).op()
Facade(퍼사드) 구조 서브시스템 복잡성 은닉, 단순 인터페이스 new Facade().doWork()
Flyweight(플라이웨이트) 구조 공유를 통한 메모리 절감 pool.get(key).render(ctx)
Proxy(프록시) 구조 접근 제어/지연 로딩/원격 대리 new Proxy(real).request()
Chain of Responsibility(책임 연쇄) 행위 요청을 연쇄 처리, 핸들러 분리 h1.setNext(h2).handle(req)
Command(커맨드) 행위 요청을 객체화, 큐/로그/되돌리기 invoker.execute(new Save(rcv))
Interpreter(인터프리터) 행위 DSL 문법 해석 expr.interpret(ctx)
Iterator(이터레이터) 행위 집합 순회 캡슐화 it = agg.iterator(); it.next()
Mediator(미디에이터) 행위 객체 상호작용 중앙 조정 mediator.notify(sender, e)
Memento(메멘토) 행위 상태 스냅샷과 복원 caretaker.push(origin.save())
Observer(옵서버) 행위 발행-구독 기반 통지 sub.attach(obs); sub.notify()
State(상태) 행위 상태 객체에 행위 위임 ctx.setState(new S1()); ctx.handle()
Strategy(전략) 행위 알고리즘 캡슐화와 교체 ctx.setStrategy(new S1()); ctx.do()
Template Method(템플릿 메서드) 행위 공통 알고리즘 골격 재사용 new Sub().templateMethod()
Visitor(방문자) 행위 구조는 고정, 연산은 확장 node.accept(visitor)

Strategy로 보는 최소 구현

패턴을 표로만 보면 감이 잘 안 온다. Strategy 하나를 Python으로 최소 구현하면 이렇다. 환경은 Python 3.10 이상, 외부 라이브러리는 필요 없다.

from abc import ABC, abstractmethod

class Strategy(ABC):
    @abstractmethod
    def calc(self, a: int, b: int) -> int: ...

class Add(Strategy):
    def calc(self, a, b): return a + b

class Mul(Strategy):
    def calc(self, a, b): return a * b

class Context:
    def __init__(self, strategy: Strategy): self.strategy = strategy
    def set_strategy(self, strategy: Strategy): self.strategy = strategy
    def execute(self, a, b): return self.strategy.calc(a, b)

if __name__ == "__main__":
    ctx = Context(Add())
    print(ctx.execute(2, 3))  # 5
    ctx.set_strategy(Mul())
    print(ctx.execute(2, 3))  # 6

전략 인터페이스를 정의하고, 전략 구현체를 분리하고, 컨텍스트에 주입한 뒤 런타임에 교체한다. 이 네 단계가 Strategy가 테스트 독립성을 확보하는 방식이다.

실무에서 마주치는 자리

  • 마이크로서비스 경계에서는 Facade와 Adapter로 BFF와 레거시 API 사이의 호환 계층을 구성한다. 통신 프로토콜이 바뀌어도 클라이언트 영향을 최소화할 수 있다.
  • 도메인 규칙이 자주 바뀌는 곳에서는 Strategy와 State로 요금·프로모션 정책을 캡슐화한다. 신규 정책 롤아웃과 A/B 테스트를 지원하기 쉬워진다.
  • UI 컴포넌트나 플러그인 아키텍처에서는 Decorator와 Composite로 위젯 조합과 기능 부가를 구현한다. 테마·스킨 교체를 OCP 방식으로 처리할 수 있다.
  • 데이터 접근·캐싱 계층에서는 Proxy로 캐시·권한·지연 로딩을 제어한다. Repository와 결합하면 테스트 더블 주입도 쉬워진다.
  • 워크플로나 명령 처리에서는 Command와 Chain of Responsibility로 작업 큐, 재시도, 롤백 시맨틱을 구현한다. 감사 로그와 재처리가 단순해진다.

도입 효과와 트레이드오프

경험값 기준으로 보면 패턴 도입은 변경에 따른 코드 수정 범위를 3060% 감소시키고, 회귀 결함률을 2035% 감소시키는 것으로 추정된다. 공통 구조를 재사용하면서 신규 기능 리드타임도 15~25% 단축된다. 정성적으로는 팀 간 공통 어휘가 정립되면서 설계 의사소통 효율이 오르고, 테스트 용이성과 대체 가능성이 개선되어 리스크 관리가 강화된다.

다만 대가도 있다. 초기 학습 비용과 추상화 비용이 발생하고, 단순한 문제에 적용하면 과설계 위험이 생긴다. 코드 스멜과 변동성이 실제로 관측될 때만 적용을 검토하는 편이 안전하다.

디자인 패턴GoFSOLID소프트웨어 설계객체지향