Table of contents
- 코드가 반복되는 이유
- 패턴은 레시피가 아니다
- GoF 23개 패턴 분류
- 패턴을 읽는 법
- 패턴을 쓸 때
- 패턴을 쓰지 말아야 할 때
- SOLID와의 관계
- 이 시리즈의 구성
- 개발 환경
- 핵심 정리
코드가 반복되는 이유
프로젝트를 몇 개 겪다 보면 이상한 기시감이 찾아온다. “이 구조, 예전에도 만들었는데?” 결제 수단을 분기하는 if-else, 알림을 보내야 하는 이벤트 리스너, 객체 생성을 한 곳에 모아두는 팩토리 클래스. 이름과 도메인은 다르지만 뼈대는 놀라울 정도로 비슷하다.
디자인 패턴은 이런 반복되는 구조에 이름을 붙인 것이다. 1994년, 네 명의 저자가 소프트웨어 설계에서 자주 등장하는 23가지 구조를 정리해 한 권의 책으로 묶었다. GoF(Gang of Four)라 불리는 이 저자들 — Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides — 의 Design Patterns: Elements of Reusable Object-Oriented Software가 그 책이다.
이 시리즈는 GoF 23개 패턴을 실무 관점에서 훑는다. 학술적 분류보다는 이 패턴이 어떤 문제를 풀어주는가에 집중하고, Java와 Kotlin 코드로 직접 구현해본다.
패턴은 레시피가 아니다
패턴을 처음 접하면 이 코드를 그대로 쓰면 되는 거구나라고 생각하기 쉽다. 그런데 GoF 책의 서문부터 저자들은 이렇게 말한다. 패턴은 복사-붙여넣기 할 코드가 아니라, 반복되는 설계 문제에 대한 해법의 핵심 구조를 기술한 것이라고.
비유하자면 요리 레시피보다는 요리 기법에 가깝다. 볶음이라는 기법을 알면 야채볶음도 만들고 새우볶음밥도 만든다. 하지만 레시피처럼 양파 50g, 간장 1큰술을 적어둔 게 아니기 때문에 상황에 맞게 응용해야 한다. 패턴도 마찬가지다. Strategy 패턴의 구조를 이해하면 결제 수단 분기에도, 정렬 알고리즘 교체에도, 할인 정책 선택에도 적용할 수 있다.
이 차이를 이해하지 못하면 두 가지 함정에 빠진다. 하나는 모든 곳에 패턴을 끼워 맞추는 것이고, 다른 하나는 내 문제에 딱 맞는 패턴이 없네라며 포기하는 것이다. 둘 다 패턴을 레시피로 본 결과다.
GoF 23개 패턴 분류
GoF는 패턴을 목적에 따라 세 가지로 나눴다. 생성(Creational), 구조(Structural), 행위(Behavioral)다.
| 분류 | 패턴 | 핵심 질문 |
|---|---|---|
| 생성 (Creational) | Singleton, Factory Method, Abstract Factory, Builder, Prototype | 객체를 어떻게 만들 것인가 |
| 구조 (Structural) | Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy | 객체를 어떻게 조합할 것인가 |
| 행위 (Behavioral) | Chain of Responsibility, Command, Interpreter, Iterator, Mediator, Memento, Observer, State, Strategy, Template Method, Visitor | 객체 간 책임을 어떻게 나눌 것인가 |
각 분류의 핵심 질문은 이렇다.
- 생성 패턴: 객체를 직접
new로 만들면 어떤 문제가 생기는가? 생성 과정을 캡슐화하면 무엇이 좋아지는가? - 구조 패턴: 클래스나 객체를 어떻게 조합하면 더 큰 구조를 유연하게 만들 수 있는가?
- 행위 패턴: 객체들이 서로 어떻게 소통하고, 책임을 어떻게 분배해야 변경에 강한 설계가 되는가?
23개를 전부 외울 필요는 없다. 실무에서 자주 쓰이는 패턴은 절반 정도이고, 나머지는 특수한 상황에서만 등장한다. 중요한 건 분류 체계를 이해하는 것이다. 새로운 설계 문제를 만났을 때 이건 생성 문제인가, 구조 문제인가, 행위 문제인가를 먼저 판단할 수 있으면 어떤 패턴을 살펴봐야 하는지 범위가 좁혀진다.
패턴을 읽는 법
GoF 책에서 각 패턴은 일정한 형식으로 기술된다. 핵심 요소 네 가지를 알아두면 어떤 패턴이든 빠르게 파악할 수 있다.
의도(Intent)
이 패턴이 해결하려는 문제를 한 줄로 요약한 것이다. 예를 들어 Strategy 패턴의 의도는 알고리즘 군을 정의하고, 각각을 캡슐화하며, 교환 가능하게 만든다이다. 의도를 읽으면 이 패턴이 자기 문제에 맞는지 30초 안에 판단할 수 있다.
동기(Motivation)
의도만으로는 감이 안 올 때가 많다. 동기 섹션은 구체적인 시나리오를 들어 이런 상황에서 이런 불편이 생기고, 그래서 이 구조가 필요하다를 설명한다. 패턴의 존재 이유를 납득시키는 부분이다.
구조(Structure)
클래스 다이어그램과 시퀀스 다이어그램으로 패턴의 뼈대를 보여준다. 참여자(Participant)가 누구이고, 서로 어떤 관계인지를 시각화한 것이다. 코드를 짜기 전에 머릿속에 그림을 그리는 데 도움이 된다.
결과(Consequences)
패턴을 적용하면 얻는 것과 잃는 것을 정리한다. 모든 패턴에는 트레이드오프가 있다. Strategy 패턴은 알고리즘 교체가 자유로워지지만, 클라이언트가 각 전략의 차이를 알아야 하고, 객체 수가 늘어난다. 이 섹션을 건너뛰면 패턴을 적용했는데 오히려 복잡해졌다는 상황을 맞닥뜨리게 된다.
실무에서 패턴을 공부할 때 권장하는 순서는 이렇다. 의도 → 동기 → 구조 → 결과 순으로 읽고, 그 다음에 코드를 본다. 코드부터 보면 무엇은 알지만 왜를 놓치기 쉽다.
패턴을 쓸 때
패턴이 빛을 발하는 상황은 명확하다.
변경 가능성이 높은 지점을 격리할 때. 요구사항이 바뀔 가능성이 높은 부분을 인터페이스 뒤로 숨기면, 나중에 구현체만 교체하면 된다. Strategy, Factory Method, Observer 같은 패턴이 이 목적으로 쓰인다.
코드의 의도를 드러낼 때. if (status == PENDING) ... else if (status == APPROVED) ...라고 쓰는 것과 State 패턴을 쓰는 것은 동작은 같을 수 있지만, 후자는 상태에 따라 행동이 달라진다는 설계 의도를 구조로 표현한다. 팀원이 코드를 읽을 때 아, State 패턴이구나라고 인식하면 전체 흐름을 빠르게 파악할 수 있다.
팀의 공통 어휘로 쓸 때. 결제 부분에 Strategy 패턴 적용하자라고 말하면, 패턴을 아는 팀원은 별도 설명 없이 구조를 떠올릴 수 있다. 이게 패턴의 가장 실용적인 가치 중 하나다. 설계 논의의 대역폭을 줄여준다.
패턴을 쓰지 말아야 할 때
패턴의 남용은 패턴을 모르는 것만큼 해롭다. 다음 상황에서는 패턴 적용을 의심해야 한다.
문제가 단순할 때. if-else 두 줄이면 끝나는 분기를 Strategy 패턴으로 풀면 인터페이스, 구현체, 컨텍스트 클래스까지 최소 네 개의 파일이 생긴다. 코드양이 늘어나고, 흐름을 따라가려면 여러 파일을 넘나들어야 한다. 이런 경우 단순한 코드가 더 나은 설계다.
간단한 예로, 할인율이 딱 두 가지뿐인 상황을 보자.
// 이 정도면 if-else가 더 낫다
fun getDiscount(type: String): Double =
if (type == "VIP") 0.2 else 0.1
이걸 Strategy로 풀면 아래처럼 된다.
// 과한 추상화의 예
interface DiscountStrategy {
fun calculate(): Double
}
class VipDiscount : DiscountStrategy {
override fun calculate() = 0.2
}
class NormalDiscount : DiscountStrategy {
override fun calculate() = 0.1
}
class PriceCalculator(private val strategy: DiscountStrategy) {
fun getDiscount() = strategy.calculate()
}
할인 종류가 두 개뿐이라면 이 구조는 과잉이다. 할인 종류가 다섯 개, 열 개로 늘어날 가능성이 있을 때 비로소 패턴이 정당화된다.
패턴 이름을 붙이려고 억지로 구조를 바꿀 때. 이 코드에 뭔가 패턴을 적용해야 할 것 같은데라는 생각이 들면 위험 신호다. 패턴은 문제가 먼저 있고, 그 문제에 맞는 해법으로 등장해야 한다. 반대 순서는 설계를 왜곡한다.
팀이 그 패턴을 모를 때. 혼자만 아는 패턴을 적용하면, 다른 팀원에게는 그냥 복잡한 코드일 뿐이다. 패턴의 커뮤니케이션 효과가 사라지고 학습 비용만 남는다.
실무에서의 판단 기준은 의외로 단순하다. 지금 이 코드가 아프지 않으면 패턴을 적용하지 않는다. 변경이 잦아서 고치기 힘들거나, 조건 분기가 계속 늘어나거나, 같은 구조가 반복되어 복사-붙여넣기를 하고 있을 때 — 그때 패턴을 꺼내면 된다.
SOLID와의 관계
디자인 패턴을 공부하다 보면 SOLID 원칙이 자연스럽게 따라붙는다. SOLID는 Robert C. Martin이 정리한 객체지향 설계의 다섯 가지 원칙이다.
| 원칙 | 이름 | 핵심 |
|---|---|---|
| S | Single Responsibility | 클래스는 변경의 이유가 하나여야 한다 |
| O | Open-Closed | 확장에는 열려 있고, 수정에는 닫혀 있어야 한다 |
| L | Liskov Substitution | 하위 타입은 상위 타입을 대체할 수 있어야 한다 |
| I | Interface Segregation | 클라이언트가 쓰지 않는 메서드에 의존하지 않아야 한다 |
| D | Dependency Inversion | 고수준 모듈이 저수준 모듈에 의존하지 않아야 한다 |
패턴과 SOLID의 관계는 이렇다. SOLID가 좋은 설계란 무엇인가를 말해주는 원칙이라면, 디자인 패턴은 그 원칙을 지키면서 반복되는 문제를 어떻게 풀 것인가에 대한 구체적인 해법이다.
예를 들어 Strategy 패턴은 OCP(Open-Closed Principle)를 실현하는 대표적인 방법이다. 새로운 알고리즘을 추가할 때 기존 코드를 수정하지 않고 새 전략 클래스만 만들면 된다. Observer 패턴은 DIP(Dependency Inversion Principle)를 따른다. Subject가 구체적인 Observer를 직접 알지 않고, 추상 인터페이스에 의존한다.
SOLID를 모른 채 패턴만 외우면 이 패턴이 왜 이렇게 생겼는지 이해하기 어렵다. 반대로 SOLID만 알고 패턴을 모르면 원칙은 이해하지만 실제 코드로 옮기는 데 시간이 걸린다. 둘은 함께 배울 때 가장 효과적이다.
이 시리즈의 구성
이 시리즈는 전체 8편으로, GoF 23개 패턴 중 실무에서 자주 마주치는 패턴을 중심으로 다룬다. 편의 구성은 목적이 아니라 어떤 문제를 푸는가를 기준으로 묶었다.
| 편 | 제목 | 다루는 패턴 |
|---|---|---|
| 1편 | 디자인 패턴 입문 | GoF 분류, 읽는 법, 쓸 때와 안 쓸 때 |
| 2편 | Strategy와 State | 행동을 갈아끼우는 두 가지 방법 |
| 3편 | Observer와 Command | 이벤트를 다루는 패턴들 |
| 4편 | Template Method와 Chain of Responsibility | 흐름을 제어하는 패턴들 |
| 5편 | Factory Method, Abstract Factory, Builder | 객체 생성을 설계하기 |
| 6편 | Decorator와 Proxy | 감싸서 기능을 더하거나 제어하기 |
| 7편 | Adapter, Facade, Composite | 구조를 정리하는 패턴들 |
| 8편 | Singleton, Iterator, Prototype | 자주 쓰지만 오해도 많은 패턴들 |
2편부터는 행위 패턴으로 시작한다. 생성이나 구조보다 행위 패턴을 먼저 다루는 이유가 있다. 실무에서 가장 먼저 부딪히는 문제가 이 로직을 어디에 둘 것인가, 조건 분기를 어떻게 정리할 것인가이기 때문이다. 객체를 어떻게 만들지(생성)나 어떻게 조합할지(구조)는 그 다음 문제다.
각 편은 패턴의 의도와 동기부터 시작해서, 클래스 다이어그램과 코드 예시로 구조를 보여주고, 마지막으로 트레이드오프와 실무 팁을 정리하는 흐름을 따른다.
개발 환경
이 시리즈의 코드 예시는 Kotlin을 주 언어로 사용하되, 패턴의 구조를 명확히 보여주기 위해 Java 코드를 병행한다. Kotlin을 쓰는 이유는 간결한 문법 덕분에 패턴의 본질에 집중하기 좋기 때문이다.
아래는 시리즈 전반에 걸쳐 사용할 기본 구조다. 인터페이스를 정의하고 구현체를 끼워 넣는 구조가 대부분의 패턴에서 반복된다.
// 패턴의 핵심: 인터페이스로 추상화하고, 구현체를 교체 가능하게 만든다
interface PaymentMethod {
fun pay(amount: Int)
}
class CreditCard : PaymentMethod {
override fun pay(amount: Int) {
println("신용카드로 ${amount}원 결제")
}
}
class KakaoPay : PaymentMethod {
override fun pay(amount: Int) {
println("카카오페이로 ${amount}원 결제")
}
}
이 정도의 인터페이스-구현체 관계가 편안하게 느껴진다면 시리즈를 따라가는 데 무리가 없을 것이다. 만약 interface나 override 같은 키워드가 낯설다면, 객체지향 프로그래밍의 기초를 먼저 정리하고 오는 쪽을 권한다.
핵심 정리
이 편에서 다룬 내용을 정리하면 이렇다.
- 디자인 패턴은 반복되는 설계 문제에 이름을 붙인 해법의 카탈로그다. 레시피가 아니라 기법이다
- GoF 23개 패턴은 생성(5개), 구조(7개), 행위(11개)로 분류된다
- 패턴을 읽을 때는 의도 → 동기 → 구조 → 결과 순서로 접근한다
- 패턴은 변경 가능성이 높은 지점을 격리하고, 설계 의도를 드러내고, 팀의 공통 어휘가 될 때 가치가 있다
- 문제가 단순하거나, 억지로 끼워 맞추거나, 팀이 모르는 패턴이면 쓰지 않는 게 낫다
- SOLID 원칙은 패턴의 이론적 토대이고, 둘을 함께 배우면 효과적이다
다음 편에서는 행위 패턴의 첫 번째 주자로 Strategy와 State를 다룬다. if-else 분기가 점점 늘어나는 상황에서 이 두 패턴이 어떻게 코드를 정리해주는지 살펴보자.




Loading comments...