코딩 · 디자인 패턴
디자인 패턴 정리: GoF 23가지 패턴
핵심 답변
디자인 패턴은 소프트웨어 설계에서 반복적으로 나타나는 문제에 대해 이름이 붙은 재사용 가능한 해법이며, 구체적인 코드보다 한 단계 높은 수준에서 설명됩니다. 대표적인 목록은 Gamma, Helm, Johnson, Vlissides가 1994년에 펴낸 『Design Patterns: Elements of Reusable Object-Oriented Software』("Gang of Four", GoF)이며, 생성 패턴 5개, 구조 패턴 7개, 행위 패턴 11개로 총 23가지 패턴을 세 그룹으로 나눕니다. 패턴은 팀에 공통 어휘를 제공하지만, 특정 문제를 풀기 위한 도구일 뿐 그 자체가 목표는 아닙니다.
디자인 패턴이란
디자인 패턴은 라이브러리도, 복사해서 쓰는 코드 조각도 아닙니다. 반복되는 문제, 해법의 핵심, 그리고 그 해법을 쓸 때의 트레이드오프를 설명한 것입니다. 코드는 사용하는 언어와 프로그램에 맞게 직접 작성해야 합니다.
GoF 책은 모든 패턴을 네 가지 핵심 요소로 설명합니다.
- 이름: "Decorator로 감싸면 됩니다"처럼 개발자들이 설계를 두세 단어로 이야기할 수 있게 해 주는 말입니다.
- 문제: 패턴을 언제 적용하는지, 그리고 패턴이 필요해지는 상황입니다.
- 해법: 관련된 클래스나 객체, 각자의 책임, 그리고 서로 협력하는 방식입니다.
- 결과: 비용과 이점입니다. 예를 들어 클래스가 늘어나는 대신 유연성이 높아집니다.
이 아이디어는 건축에서 왔습니다. Christopher Alexander의 책 『A Pattern Language』(1977)는 건물과 도시에서 반복되는 해법을 설명했고, GoF 저자들은 같은 발상을 객체 지향 소프트웨어에 적용했습니다.
Gang of Four 책
『Design Patterns: Elements of Reusable Object-Oriented Software』는 1994년 Addison-Wesley에서 출간되었습니다. 예제는 C++와 Smalltalk로 되어 있지만, 패턴 자체는 Java, C#, TypeScript, Python, Kotlin을 포함한 모든 객체 지향 언어에 적용할 수 있습니다.
이 책은 패턴 목록 외에도 대부분의 패턴을 관통하는 두 가지 일반 원칙을 제시합니다.
- 구현이 아니라 인터페이스에 맞춰 프로그래밍하라. 코드는 구체적인 클래스가 아니라 객체가 무엇을 할 수 있는지에 의존해야 합니다.
- 클래스 상속보다 객체 합성을 선호하라. 객체를 조합해 동작을 만드는 방식이 깊은 클래스 계층을 만드는 것보다 대체로 더 유연합니다.
또한 이 책은 범위에 따라 패턴을 분류합니다. 클래스 패턴은 상속을 사용하며 컴파일 시점에 고정됩니다(Factory Method, 클래스 Adapter, Interpreter, Template Method). 객체 패턴은 합성을 사용하며 실행 중에 바꿀 수 있습니다(나머지 전부).
분류별 GoF 23가지 패턴
생성 패턴은 객체를 어떻게 만드는지를, 구조 패턴은 클래스와 객체를 어떻게 조합해 더 큰 구조를 만드는지를, 행위 패턴은 객체들이 어떻게 소통하고 책임을 나누는지를 다룹니다.
| 패턴 | 분류 | 의도 한 줄 요약 |
|---|---|---|
| Abstract Factory | 생성 | 구체적인 클래스를 지정하지 않고 서로 관련된 객체들의 군(family)을 만듭니다. |
| Builder | 생성 | 복잡한 객체를 단계별로 만들며, 생성 과정과 표현을 분리합니다. |
| Factory Method | 생성 | 어떤 클래스를 인스턴스화할지 서브클래스가 결정하게 합니다. |
| Prototype | 생성 | 기존 인스턴스를 복사해서 새 객체를 만듭니다. |
| Singleton | 생성 | 클래스의 인스턴스가 하나뿐임을 보장하고 그 인스턴스에 대한 전역 접근을 제공합니다. |
| Adapter | 구조 | 한 인터페이스를 클라이언트가 기대하는 다른 인터페이스로 변환합니다. |
| Bridge | 구조 | 추상화와 구현을 분리해 둘이 독립적으로 변할 수 있게 합니다. |
| Composite | 구조 | 개별 객체와 객체의 트리를 동일하게 다룹니다. |
| Decorator | 구조 | 객체를 감싸서 동적으로 책임을 추가합니다. |
| Facade | 구조 | 복잡한 서브시스템에 하나의 단순한 인터페이스를 제공합니다. |
| Flyweight | 구조 | 공통 상태를 공유해 작은 객체를 대량으로 효율적으로 지원합니다. |
| Proxy | 구조 | 다른 객체에 대한 접근을 제어하는 대리 객체를 제공합니다. |
| Chain of Responsibility | 행위 | 요청을 처리할 때까지 핸들러 체인을 따라 전달합니다. |
| Command | 행위 | 요청을 객체로 만들어 큐에 넣거나 기록하거나 취소할 수 있게 합니다. |
| Interpreter | 행위 | 간단한 언어의 문법을 클래스로 표현하고 그 언어의 문장을 평가합니다. |
| Iterator | 행위 | 내부 구조를 드러내지 않고 컬렉션의 요소를 순서대로 접근합니다. |
| Mediator | 행위 | 객체 간 통신을 한곳에 모아 서로를 직접 참조하지 않게 합니다. |
| Memento | 행위 | 캡슐화를 깨지 않고 객체의 상태를 저장하고 복원합니다. |
| Observer | 행위 | 객체가 바뀌면 의존하는 객체들에 자동으로 알립니다. |
| State | 행위 | 내부 상태가 바뀌면 객체의 동작도 바뀌게 합니다. |
| Strategy | 행위 | 알고리즘 군을 하나의 인터페이스 뒤에서 서로 교체할 수 있게 합니다. |
| Template Method | 행위 | 알고리즘의 뼈대를 정의하고 일부 단계는 서브클래스가 채우게 합니다. |
| Visitor | 행위 | 클래스 계층을 바꾸지 않고 새로운 연산을 추가합니다. |
패턴 실전: TypeScript의 Strategy
패턴이 실제로 쓰이는 작은 예제입니다. 결제 화면에서 배송비를 여러 방식으로 계산해야 합니다. 점점 길어지는 if/else 대신, 각 규칙을 하나의 인터페이스 뒤에 있는 교체 가능한 객체로 만듭니다.
interface ShippingStrategy {
cost(weightKg: number): number;
}
class FlatRate implements ShippingStrategy {
cost(): number { return 5; }
}
class PerKilogram implements ShippingStrategy {
cost(weightKg: number): number { return 2 * weightKg; }
}
class Checkout {
constructor(private shipping: ShippingStrategy) {}
setShipping(s: ShippingStrategy): void { this.shipping = s; }
total(itemsPrice: number, weightKg: number): number {
return itemsPrice + this.shipping.cost(weightKg);
}
}
const checkout = new Checkout(new FlatRate());
console.log(checkout.total(40, 3)); // 45
checkout.setShipping(new PerKilogram());
console.log(checkout.total(40, 3)); // 46TypeScript에서는 함수가 값이므로 같은 설계를 더 적은 코드로 쓸 수 있습니다. 이는 흔한 주제입니다. 일급 함수를 지원하는 언어에서는 많은 패턴이 더 단순해지거나 사라집니다.
type Shipping = (weightKg: number) => number;
const flatRate: Shipping = () => 5;
const perKilogram: Shipping = (w) => 2 * w;
function total(itemsPrice: number, weightKg: number, shipping: Shipping): number {
return itemsPrice + shipping(weightKg);
}
console.log(total(40, 3, flatRate)); // 45
console.log(total(40, 3, perKilogram)); // 46패턴을 선택하는 방법
패턴이 아니라 문제에서 출발하세요. 패턴은 코드의 실제 고통을 없애 줄 때만 추가 클래스를 들일 가치가 있습니다. 다음 질문이 보통의 후보를 알려 줍니다.
| 문제가 이렇다면... | 고려할 패턴 |
|---|---|
| 코드가 어떤 구체 클래스를 생성하는지에 의존하면 안 된다 | Factory Method, Abstract Factory |
| 생성자에 선택적 매개변수가 많다 | Builder |
| 기존 클래스를 다른 인터페이스에 맞춰야 한다 | Adapter |
| 복잡한 라이브러리에 단순한 진입점이 필요하다 | Facade |
| 서브클래스가 폭발적으로 늘지 않게 객체에 기능을 추가하고 싶다 | Decorator |
| 메뉴, 파일, UI 위젯 같은 트리 구조를 다룬다 | Composite, Visitor, Iterator |
| 한 객체가 바뀔 때 여러 부분이 반응해야 한다 | Observer |
| 모드나 상태에 따라 동작이 달라지고 분기가 많다 | State |
| 교체 가능한 알고리즘이나 규칙이 필요하다 | Strategy |
| 동작을 취소하거나 큐에 넣거나 기록해야 한다 | Command, Memento |
- 먼저 동작하는 가장 단순한 코드를 작성합니다.
- 같은 종류의 변경이 계속 괴롭힌다면(예: 새 결제 수단을 추가할 때마다 파일 다섯 개를 고쳐야 한다면) 그 문제에 이름을 붙입니다.
- 그 문제를 다루는 패턴을 찾아보고 각 패턴의 결과(단점 포함)를 읽어 봅니다.
- 고통을 없애 주는 가장 작은 형태의 패턴만 적용하고 거기서 멈춥니다.
비판과 한계
디자인 패턴은 유용하지만 자주 잘못 쓰입니다. 흔히 나오는 비판을 알아 두면 좋습니다.
- 남용: "혹시 몰라서" 팩토리, 인터페이스, 매니저를 추가하면 단순한 코드도 읽기 어려워집니다. 이를 패턴 열병이라고 부르기도 합니다.
- 언어 기능의 부재: 1996년 Peter Norvig은 GoF 23가지 패턴 중 16개가 Lisp, Dylan 같은 동적 언어에서는 보이지 않거나 더 단순하다는 것을 보였습니다. 일급 함수가 Strategy와 Command를 대신하고, 내장 이터레이터가 Iterator를 대신합니다.
- 특히 Singleton은 전역 상태로 쓰일 때 의존성을 숨기고 테스트를 어렵게 하므로 안티패턴으로 여기는 의견이 많습니다.
- 이 목록은 1994년의 것입니다. 이후 의존성 주입, 리포지토리, Model-View-Controller 변형처럼 책에 없는 유명한 패턴들이 더해졌습니다.
이 책의 오래가는 가치는 공통 어휘와 트레이드오프를 따져 보는 습관입니다. "이건 Adapter입니다"라는 한마디가 다른 개발자에게 많은 것을 알려 줍니다.
관련 원칙
패턴은 객체 지향 설계의 SOLID 원칙과 함께 설명되는 경우가 많습니다. 예를 들어 Strategy와 Decorator는 개방-폐쇄 원칙을 따르는 직접적인 방법이고, Abstract Factory는 의존성 역전 원칙을 적용하는 흔한 방법입니다. 원칙을 익히면 패턴이 왜 그런 모양인지 이해하는 데 도움이 됩니다.
자주 묻는 질문
디자인 패턴은 모두 몇 가지인가요?
Gang of Four 책에는 생성 패턴 5개, 구조 패턴 7개, 행위 패턴 11개로 총 23가지 패턴이 실려 있습니다. 이후 동시성, 엔터프라이즈 애플리케이션, 사용자 인터페이스 등을 위한 다른 많은 패턴도 소개되었습니다.
어떤 디자인 패턴부터 배워야 하나요?
실제 코드에서 가장 자주 만나는 Strategy, Observer, Factory Method, Decorator, Adapter, Facade부터 시작하세요. 그다음 Builder, Composite, Command를 배우면 됩니다.
디자인 패턴은 지금도 유효한가요?
네. 공통 어휘로서, 그리고 반복되는 문제의 해법으로서 여전히 유효합니다. 일부 패턴은 이제 언어와 프레임워크에 내장되어 있어 전체 클래스 구조를 직접 작성하지 않고도 쓸 수 있습니다.
디자인 패턴은 객체 지향 언어에만 적용되나요?
GoF 패턴은 객체 지향 언어를 위해 쓰였지만, 그 바탕이 되는 아이디어는 어디에나 나타납니다. 함수형 언어에서는 많은 패턴이 간단한 고차 함수가 됩니다.
디자인 패턴과 알고리즘은 무엇이 다른가요?
알고리즘은 정렬처럼 계산 문제를 푸는 정확한 단계의 순서입니다. 디자인 패턴은 코드와 책임을 어떻게 조직할지를 설명하며, 단계별로 따르는 것이 아니라 프로그램마다 맞춰 적용합니다.