교육 자료 · Object-Oriented Design

디자인 패턴에
뛰어들기

이미 검증된 22가지 설계 해법으로
바꾸기 쉬운 코드를 만드는 방법

5파트
22디자인 패턴
8설계 원칙

원저 · Alexander Shvets 『디자인 패턴에 뛰어들기』(Refactoring.Guru)의 목차 구성을 바탕으로 재작성한 학습용 슬라이드입니다.

생성5구조7행동1022 PATTERNS

학습 지도

이 자료는 이렇게 나아갑니다

패턴을 외우기 전에, 패턴이 필요한지부터 짚습니다. 원리 → 원칙 → 패턴 순서입니다.

PART 1

객체지향의 토대

클래스·객체·4대 기둥·객체 간 관계

6장
PART 2

디자인 패턴이란

패턴의 정의, 역사, 분류 체계

4장
PART 3

설계 원칙

좋은 설계의 조건 · 3대 원칙 · SOLID

11장
PART 4

생성 · 구조 패턴

객체를 만들고 조립하는 12가지 해법

14장
PART 5

행동 패턴과 마무리

객체가 협업하는 10가지 해법 + 적용 가이드

14장

PART 1

객체지향의
토대

패턴은 객체지향 위에 세워집니다. 토대가 흔들리면 패턴도 무너집니다.

  • 01객체와 클래스
  • 02클래스 계층구조
  • 03OOP의 네 기둥
  • 04추상화와 캡슐화
  • 05상속과 다형성
  • 06객체 간의 여섯 가지 관계

1-1 · OOP의 기초

클래스는 청사진, 객체는 실물

클래스는 무엇을 가지고 무엇을 할 수 있는지를 적어 둔 설계도입니다. 객체는 그 설계도로 찍어 낸 실제 인스턴스입니다.

CoffeeMachine
- brand : String- waterLevel : int- beanType : BeanType
+ brew(size) : Coffee+ refill(ml) : void+ clean() : void

클래스 — 필드(상태)와 메서드(행동)의 묶음

사무실 3층 머신waterLevel = 1200
라운지 머신waterLevel = 400
연구동 머신waterLevel = 0

객체 — 같은 청사진, 서로 다른 상태

필드에 담긴 값이 곧 객체의 상태이고, 메서드가 곧 객체의 행동입니다.

1-2 · OOP의 기초

계층구조는 공통점을 위로 올린 결과입니다

자식은 부모의 필드와 메서드를 물려받습니다. 공통된 것을 위로 올릴수록 아래는 자기 차이만 갖게 됩니다.

확장부모의 것을 그대로 쓰면서 새 기능만 더합니다
재정의물려받은 메서드의 내용을 자기 방식으로 바꿉니다
대체 가능자식은 언제나 부모 자리에 들어갈 수 있어야 합니다

1-3 · OOP의 기둥들

객체지향을 떠받치는 네 개의 기둥

01

추상화 Abstraction

지금 맥락에 필요한 속성과 행동만 남기고 나머지는 지웁니다. 비행 시뮬레이터의 비행기와 좌석 예약 시스템의 비행기는 같은 대상이지만 전혀 다른 모델입니다.

02

캡슐화 Encapsulation

내부는 감추고 정해진 인터페이스만 노출합니다. 자동차를 몰기 위해 점화 플러그를 알 필요는 없습니다.

03

상속 Inheritance

이미 있는 클래스 위에 새 클래스를 세워 코드 중복을 없앱니다. 단, 부모의 인터페이스 전체를 함께 물려받습니다.

04

다형성 Polymorphism

같은 메시지를 보내도 객체마다 다르게 반응합니다. 호출하는 쪽은 상대가 누구인지 몰라도 됩니다.

패턴 22가지는 결국 이 네 가지를 어떤 비율로 조합할 것인가에 대한 답들입니다.

1-4 · 추상화와 캡슐화

무엇을 감추고, 무엇을 보여줄 것인가

캡슐화는 단순한 접근 제어자 문제가 아닙니다. 바깥이 의존해도 되는 최소한의 표면을 정하는 설계 행위입니다.

감추지 않으면
  • 내부 필드를 여기저기서 직접 건드립니다
  • 구현을 바꾸는 순간 호출부 전체가 깨집니다
  • 어디까지가 공개 약속인지 아무도 모릅니다
감추면
  • 공개 인터페이스만 지키면 내부는 언제든 바꿉니다
  • 변경의 파급 범위가 클래스 안으로 갇힙니다
  • 인터페이스가 곧 팀 간의 계약이 됩니다

추상화 수준의 기준은 맥락입니다. 같은 대상이라도 시스템의 목적이 다르면 남길 속성이 달라집니다.

1-5 · 상속과 다형성

상속은 편리하지만, 값이 붙어 있습니다

상속은 코드 중복을 줄이는 가장 빠른 길이지만, 동시에 가장 강한 결합을 만듭니다.

주의 1

인터페이스를 통째로 물려받습니다

필요 없는 메서드까지 함께 따라옵니다. 그것을 비워 두는 순간 리스코프 치환 원칙이 깨집니다.

주의 2

부모를 고치면 자식이 깨집니다

겉으로는 무해해 보이는 부모의 수정이 모든 자식에게 예고 없이 번집니다.

주의 3

단일 상속의 벽

대부분의 언어는 부모를 하나만 허용합니다. 두 축으로 확장해야 하는 순간 막힙니다.

주의 4

계층이 깊어질수록 읽기 어렵습니다

실제 동작을 이해하려면 계층을 위로 계속 거슬러 올라가야 합니다.

그래서 뒤에서 다룰 원칙이 나옵니다 — 상속보다 합성을 사용하세요.

1-6 · 객체 간의 관계

여섯 가지 관계, 결합의 강도 순서

UML 화살표는 장식이 아닙니다. 얼마나 강하게 묶여 있는가를 나타내는 척도입니다.

의존 (Dependency)
15메서드 인자로만 잠깐 스침
연관 (Association)
35필드로 상시 참조
집합 (Aggregation)
50담고 있지만 수명은 별개
합성 (Composition)
70담고 있고 수명까지 책임짐
구현 (Implementation)
85인터페이스 계약 전체를 이행
상속 (Inheritance)
100구현과 인터페이스를 모두 물려받음

가로축은 결합도의 상대적 강도를 학습용으로 도식화한 값입니다. 위로 갈수록 느슨하고, 아래로 갈수록 바꾸기 어렵습니다.

PART 2

디자인 패턴이란
무엇인가

패턴은 복사해 붙여 넣는 코드가 아니라, 문제를 푸는 방식의 설명서입니다.

  • 01패턴의 정의
  • 02패턴 설명의 구성
  • 03역사와 GoF
  • 04왜 배워야 하는가

2-1 · 디자인 패턴이란?

알고리즘이 레시피라면, 패턴은 청사진입니다

알고리즘

레시피

  • 목표에 이르는 정확한 단계를 지정합니다
  • 순서를 바꾸면 결과가 달라집니다
  • 그대로 따라 하면 됩니다
디자인 패턴

청사진

  • 해결책의 구조와 특징만 설명합니다
  • 구현 순서와 세부는 상황에 맡깁니다
  • 두 사람이 같은 패턴으로 다른 코드를 씁니다

그래서 패턴은 라이브러리처럼 가져다 쓰는 것이 아니라, 내 문제에 맞춰 매번 다시 그리는 것입니다.

2-2 · 패턴 설명의 구성

패턴 하나를 제대로 읽는 여섯 칸

이 자료의 패턴 슬라이드도 같은 순서를 따릅니다.

01

의도

패턴이 어떤 문제를 푸는지 한 문장으로 요약합니다. 이 한 줄로 후보를 좁힐 수 있어야 합니다.

02

문제

패턴을 쓰지 않았을 때 코드가 겪는 구체적인 고통입니다. 내 코드에 같은 증상이 있는지 대조합니다.

03

해결책

구조를 어떻게 바꾸어 그 고통을 없애는지 설명합니다. 클래스가 아니라 관계가 바뀝니다.

04

구조

참여하는 클래스와 그들이 주고받는 관계입니다. 이름은 바꿔도 관계는 유지해야 합니다.

05

적용 시점

언제 써야 하는지만큼 언제 쓰면 안 되는지가 중요합니다. 둘 다 확인하세요.

06

장단점

패턴은 언제나 무언가를 내주고 무언가를 얻습니다. 그 교환이 지금 남는 장사인지 판단합니다.

2-3 · 역사와 분류

1994년, 네 사람이 23가지를 정리했습니다

패턴 개념 자체는 건축가 크리스토퍼 알렉산더에게서 왔습니다. 이를 소프트웨어로 옮긴 것이 'Gang of Four'의 카탈로그입니다.

22이 자료가 다루는 패턴
  • 생성 패턴 · Creational523%
  • 구조 패턴 · Structural732%
  • 행동 패턴 · Behavioral1045%
  • 생성 패턴객체를 어떻게 만들 것인가. 생성 과정의 유연성과 재사용을 높입니다.
  • 구조 패턴객체를 어떻게 조립할 것인가. 구조를 유연하고 효율적으로 유지합니다.
  • 행동 패턴객체가 어떻게 협업할 것인가. 책임 분배와 소통 방식을 다룹니다.

GoF 원서는 23개를 다루지만, 인터프리터 패턴은 오늘날 활용 빈도가 낮아 이 자료에서는 22개를 다룹니다.

2-4 · 왜 배워야 할까요?

패턴을 몰라도 코딩은 됩니다. 다만 두 가지를 잃습니다.

01

검증된 해결책 도구 상자

매번 처음부터 고민하는 대신, 수십 년간 다듬어진 접근법을 꺼내 씁니다. 함정도 이미 문서화되어 있습니다.

02

팀의 공통 언어

"프록시로 감싸자"는 한마디가 다이어그램 열 장을 대신합니다. 설계 논의의 대역폭이 완전히 달라집니다.

함께 기억할 것

패턴을 배우면 모든 문제가 패턴으로 보입니다. 단순한 문제는 단순하게 푸는 것이 여전히 최선입니다. 패턴은 문제가 실제로 나타난 뒤에 꺼내는 도구이지, 미리 깔아 두는 바닥이 아닙니다.

PART 3

소프트웨어
설계 원칙

22개 패턴은 모두 이 원칙들을 서로 다른 방식으로 지켜 낸 결과물입니다.

  • 01좋은 설계의 조건
  • 02변하는 것을 캡슐화하라
  • 03인터페이스에 프로그래밍하라
  • 04상속보다 합성
  • 05SOLID 다섯 원칙

3-1 · 좋은 디자인의 특징

재사용은 공짜가 아닙니다

코드를 다시 쓸 수 있게 만들수록 설계는 복잡해집니다. 재사용성과 단순함은 서로 당깁니다.

복사해서 붙여 넣기
20가장 싸고 가장 빨리 썩습니다
클래스 단위 재사용
55라이브러리로 묶어 쓰는 수준
프레임워크 수준 재사용
90확장 지점을 미리 설계해 둔 수준
확장성

요구사항은 반드시 바뀝니다. 좋은 설계는 변화를 막는 설계가 아니라, 변화가 닿는 면적을 줄이는 설계입니다.

균형

재사용을 높일수록 추상 계층이 늘고 읽기는 어려워집니다. 어느 지점에서 멈출지 결정하는 것이 설계자의 일입니다.

3-2 · 디자인 원칙

원칙 하나 · 변화하는 내용을 캡슐화하세요

바뀔 부분을 미리 찾아 바뀌지 않는 부분과 분리해 두면, 수정의 충격이 그 안에서 멈춥니다.

분리 전
method getOrderTotal(order)
  total = 0
  for each item in order.lineItems
    total += item.price * item.quantity

  if order.country == "KR"
    total += total * 0.10
  else if order.country == "US"
    total += total * order.state.tax
  ...
  return total

세금 규칙이 하나 추가될 때마다 이 메서드를 엽니다.

분리 후
method getOrderTotal(order)
  total = 0
  for each item in order.lineItems
    total += item.price * item.quantity

  total += total * getTaxRate(order)
  return total

method getTaxRate(order)
  // 세금 규칙만 이 안에서 자랍니다

메서드로 뺀 다음 단계는 클래스로 빼는 것입니다. 그것이 전략 패턴입니다.

3-3 · 디자인 원칙

원칙 둘 · 구현이 아닌 인터페이스에 프로그래밍하세요

무엇을 할 수 있는지에 의존하고, 그것을 어떻게 하는지에는 의존하지 마세요.

1단계한 객체가 다른 객체에서 실제로 필요로 하는 것을 추립니다
2단계그 메서드들을 인터페이스로 선언합니다
3단계의존하는 쪽이 인터페이스만 바라보게 바꿉니다

3-4 · 디자인 원칙

원칙 셋 · 상속보다 합성을 사용하세요

축이 둘 이상이 되는 순간, 상속은 곱셈이 되고 합성은 덧셈이 됩니다.

축이 늘어날 때 필요한 클래스 수

1개 축2개 축3개 축4개 축5개 축상속 (곱셈)합성 (덧셈)
  • 상속의 문제필요 없는 메서드까지 물려받고, 부모 변경이 모든 자식으로 번지며, 부모는 하나만 고를 수 있습니다.
  • 합성의 답"~이다(is-a)" 대신 "~을 가진다(has-a)"로 바꿉니다. 필요한 능력만 골라 조립하고, 실행 중에 교체합니다.

브리지 · 전략 · 데코레이터 · 상태는 모두 이 원칙의 구체적인 적용 사례입니다.

3-5 · SOLID

다섯 글자로 압축된 객체지향 설계의 기준

각 원칙은 변경이 닥쳤을 때 무엇이 흔들리는가를 다른 각도에서 관리합니다.

S

단일 책임 원칙

클래스가 바뀌어야 할 이유는 하나뿐이어야 합니다

O

개방/폐쇄 원칙

확장에는 열려 있고 변경에는 닫혀 있어야 합니다

L

리스코프 치환 원칙

자식은 부모 자리에 그대로 들어갈 수 있어야 합니다

I

인터페이스 분리 원칙

쓰지 않는 메서드에 의존하도록 강요하지 마세요

D

의존관계 역전 원칙

상위 모듈이 하위 구현이 아닌 추상에 의존해야 합니다

원칙은 비용을 수반합니다. 모두를 항상 지키면 유연하지만 거대한 코드가 됩니다. 실제로 바뀔 만한 곳에만 적용하세요.

3-S · SOLID

S 단일 책임 원칙 Single Responsibility

클래스가 바뀌어야 할 이유는 오직 하나여야 합니다.

한 클래스가 여러 이유로 수정된다면, 서로 다른 팀이 같은 파일을 동시에 열게 됩니다. 책임을 쪼개면 변경의 이유마다 서로 다른 파일이 열립니다.

위반
class Employee
  getName()
  printTimeSheetReport()

// 보고서 형식이 바뀌어도
// Employee가 열립니다
준수
class Employee
  getName()

class TimeSheetReport
  print(employee)

// 각자 자기 이유로만
// 바뀝니다

3-O · SOLID

O 개방/폐쇄 원칙 Open / Closed

확장에는 열려 있고, 변경에는 닫혀 있어야 합니다.

새 기능을 넣기 위해 이미 동작하는 코드를 열지 않아도 되는 구조를 만듭니다. 이미 배포되어 다른 곳이 의존하는 코드일수록 값어치가 큽니다.

위반
class Order
  getShippingCost()
    if type == "ground" ...
    if type == "air" ...
    // 배송사 추가 = 이 파일 수정
준수
interface Shipping
  getCost(order)

class Ground implements Shipping
class Air    implements Shipping
// 배송사 추가 = 새 파일 하나

3-L · SOLID

L 리스코프 치환 원칙 Liskov Substitution

자식 객체는 부모 객체를 대체할 수 있어야 합니다. 클라이언트 코드를 고치지 않고서 말입니다.

다섯 원칙 중 가장 형식적인 규칙을 가진 원칙입니다. 공개 라이브러리처럼 남이 내 클래스를 확장하는 상황에서 특히 중요합니다.

01

매개변수는 넓히거나 그대로

자식 메서드가 부모보다 더 좁은 타입을 요구하면 안 됩니다

02

반환값은 좁히거나 그대로

자식이 부모보다 더 넓은 타입을 반환하면 클라이언트가 감당하지 못합니다

03

예외를 새로 던지지 않기

부모가 던지지 않던 예외를 자식이 던지면 호출부가 무너집니다

04

사전 조건을 강화하지 않기

부모가 받던 입력을 자식이 거부하면 대체가 아닙니다

05

사후 조건을 약화하지 않기

부모가 보장하던 결과를 자식이 지키지 않으면 안 됩니다

06

불변식을 유지하기

부모가 지키던 상태 규칙을 자식이 깨뜨리면 안 됩니다

템플릿 메서드 패턴에서 자식이 골격의 약속을 어길 때, 가장 자주 이 원칙이 깨집니다.

3-I · SOLID

I 인터페이스 분리 원칙 Interface Segregation

클라이언트가 쓰지 않는 메서드에 의존하도록 강요하지 마세요.

인터페이스가 넓어질수록 구현체는 쓰지도 않을 메서드를 비워 두게 됩니다. 비워 둔 메서드는 언젠가 예외로 터집니다.

위반
interface CloudProvider
  storeFile()
  getFile()
  createServer()
  listServers()
  getCDNAddress()

// 스토리지만 하는 업체도
// 전부 구현해야 합니다
준수
interface CloudStorage
  storeFile() / getFile()

interface Compute
  createServer() / listServers()

interface CDN
  getCDNAddress()

// 필요한 것만 골라 구현합니다

3-D · SOLID

D 의존관계 역전 원칙 Dependency Inversion

상위 모듈이 하위 모듈에 의존해서는 안 됩니다. 둘 다 추상에 의존해야 합니다.

보통 설계는 위에서 아래로 흐릅니다. 이 원칙은 그 화살표를 뒤집어, 비즈니스 로직이 데이터베이스를 부리도록 만듭니다.

위반
BudgetReport ──▶ MySQLDatabase

// 비즈니스 로직이
// DB 구현에 못 박혀 있습니다
// DB 교체 = 로직 수정
준수
BudgetReport ──▶ interface DatabaseMySQLDatabase / MongoDB

// 인터페이스를 상위 계층이 소유합니다
// 화살표가 뒤집혔습니다

PART 4 · CREATIONAL · 5

생성 패턴

객체를 어떻게 만들 것인가. 생성 코드를 사용 코드에서 떼어 내는 다섯 가지 방법입니다.

  • 01팩토리 메서드
  • 02추상 팩토리
  • 03빌더
  • 04프로토타입
  • 05싱글턴
01 생성 패턴

팩토리 메서드 Factory Method

객체를 만드는 책임을 자식 클래스에 넘깁니다

문제

코드 곳곳에 new 구상클래스()가 박혀 있으면, 새로운 타입 하나를 추가할 때마다 호출 지점을 전부 찾아 고쳐야 합니다.

해결책

생성자 직접 호출을 팩토리 메서드로 감싸고, 어떤 객체를 반환할지는 자식 클래스가 결정하게 합니다.

이럴 때 씁니다
  • 필요한 객체의 타입을 미리 알 수 없을 때
  • 프레임워크 사용자에게 컴포넌트 확장 지점을 열어 줄 때
  • 무거운 객체를 재사용해 자원을 아끼고 싶을 때
얻는 것
  • 생성 코드와 사용 코드의 결합이 끊어집니다
  • 새 제품을 추가해도 기존 코드는 그대로입니다
내주는 것
  • 제품마다 자식 클래스가 필요해 클래스 수가 늘어납니다
현업 체감 사용 빈도
80
구현 복잡도
30
02 생성 패턴

추상 팩토리 Abstract Factory

서로 어울리는 객체들을 한 벌씩 통째로 만듭니다

문제

의자·소파·테이블처럼 스타일이 맞아야 하는 객체들을 따로 만들면, 어느 순간 모던 소파에 빅토리안 의자가 섞입니다.

해결책

제품군마다 팩토리를 하나씩 두고, 클라이언트는 추상 팩토리 인터페이스만 바라보게 합니다.

이럴 때 씁니다
  • 여러 제품군을 다루고 조합의 일관성이 중요할 때
  • 제품군 전체를 한 번에 교체해야 할 때
얻는 것
  • 같은 팩토리에서 나온 제품끼리는 호환이 보장됩니다
  • 제품군 교체가 팩토리 한 줄 교체로 끝납니다
내주는 것
  • 인터페이스와 클래스가 큰 폭으로 늘어납니다
현업 체감 사용 빈도
70
구현 복잡도
50
03 생성 패턴

빌더 Builder

복잡한 객체를 단계별로 조립합니다

문제

선택 항목이 많은 객체를 만들다 보면 생성자 매개변수가 끝없이 길어지고, 대부분의 호출에서 절반은 null이 됩니다.

해결책

조립 단계를 빌더 객체로 옮기고, 필요한 단계만 골라 호출한 뒤 결과를 받아 옵니다.

이럴 때 씁니다
  • 매개변수가 비대해진 생성자를 없애고 싶을 때
  • 같은 조립 절차로 서로 다른 표현을 만들어야 할 때
얻는 것
  • 단계별로 조립하고, 미루고, 재사용할 수 있습니다
  • 조립 절차와 최종 표현이 분리됩니다
내주는 것
  • 빌더 클래스가 추가되어 전체 구조가 복잡해집니다
현업 체감 사용 빈도
75
구현 복잡도
45
04 생성 패턴

프로토타입 Prototype

새로 만들지 않고 기존 객체를 복제합니다

문제

객체를 복사하려면 모든 필드를 알아야 하는데, 비공개 필드는 바깥에서 보이지 않습니다. 게다가 구상 클래스에 묶이게 됩니다.

해결책

복제 책임을 객체 자신에게 맡기고, 공통 clone() 인터페이스로 통일합니다.

이럴 때 씁니다
  • 구상 클래스에 의존하지 않고 객체를 복사해야 할 때
  • 초기화 비용이 큰 객체를 반복해서 만들어야 할 때
얻는 것
  • 클래스에 묶이지 않고 복사할 수 있습니다
  • 무거운 초기화 과정을 통째로 건너뜁니다
내주는 것
  • 순환 참조가 얽힌 객체의 복제는 까다롭습니다
현업 체감 사용 빈도
40
구현 복잡도
25
05 생성 패턴

싱글턴 Singleton

인스턴스는 하나, 접근점도 하나로 고정합니다

문제

설정 객체나 연결 풀처럼 공유해야 할 자원을 여러 번 만들면, 같은 것을 가리켜야 할 상태가 조용히 갈라집니다.

해결책

생성자를 잠그고, 정적 메서드가 언제나 같은 인스턴스를 돌려주게 합니다.

이럴 때 씁니다
  • 프로그램 전체가 단 하나의 인스턴스를 공유해야 할 때
얻는 것
  • 유일성이 보장되고 어디에서나 접근할 수 있습니다
내주는 것
  • 전역 상태를 만들어 단위 테스트를 어렵게 합니다
  • 다중 스레드 환경에서 별도 처리가 필요합니다

가장 자주 남용되는 패턴입니다. 단일 책임 원칙을 함께 위반하기 쉬워 안티패턴으로 불리기도 합니다.

현업 체감 사용 빈도
55
구현 복잡도
15

PART 4 · STRUCTURAL · 7

구조 패턴

객체를 어떻게 조립할 것인가. 구조를 유연하게 유지하는 일곱 가지 방법입니다.

  • 01어댑터
  • 02브리지
  • 03복합체
  • 04데코레이터
  • 05퍼사드
  • 06플라이웨이트
  • 07프록시
06 구조 패턴

어댑터 Adapter

맞지 않는 인터페이스 사이에 통역사를 세웁니다

문제

우리 코드는 JSON을 다루는데, 꼭 써야 하는 분석 라이브러리는 XML만 받습니다. 라이브러리는 고칠 수 없습니다.

해결책

한쪽 인터페이스를 받아 다른 쪽 형식으로 변환해 전달하는 중간 객체를 둡니다.

이럴 때 씁니다
  • 기존 클래스를 고칠 수 없는데 새 인터페이스에 끼워 넣어야 할 때
  • 여러 자식 클래스가 공통 기능을 나눠 갖기 어려울 때
얻는 것
  • 변환 로직이 한곳에 모입니다
  • 기존 코드를 건드리지 않고 새 어댑터를 추가합니다
내주는 것
  • 클래스와 인터페이스가 늘어납니다
현업 체감 사용 빈도
80
구현 복잡도
25
07 구조 패턴

브리지 Bridge

추상과 구현을 갈라 각각 따로 키웁니다

문제

도형 × 색상처럼 두 축이 곱해지는 순간 클래스 수가 폭발합니다. 색을 하나 더하면 도형 수만큼 클래스가 늘어납니다.

해결책

한 축을 별도 계층으로 빼내고 상속 대신 합성으로 연결합니다. 곱셈이 덧셈으로 바뀝니다.

이럴 때 씁니다
  • 여러 축으로 동시에 확장해야 하는 거대한 클래스가 있을 때
  • 런타임에 구현을 교체해야 할 때
얻는 것
  • 플랫폼 독립적인 코드를 만들 수 있습니다
  • 두 축을 서로 간섭 없이 확장합니다
내주는 것
  • 응집도가 높은 클래스에 적용하면 오히려 복잡해집니다
현업 체감 사용 빈도
35
구현 복잡도
55
08 구조 패턴

복합체 Composite

부분과 전체를 똑같은 방식으로 다룹니다

문제

상자 안에 상자가 든 주문의 총액을 구하려면, 어떤 것이 상자이고 어떤 것이 제품인지 매번 확인하며 재귀를 헤집어야 합니다.

해결책

잎과 복합 노드가 같은 인터페이스를 구현하고, 복합 노드는 받은 작업을 자식들에게 그대로 위임합니다.

이럴 때 씁니다
  • 데이터가 트리 구조를 이룰 때
  • 개별 객체와 그룹을 구분 없이 처리하고 싶을 때
얻는 것
  • 복잡한 트리를 재귀적으로 단순하게 다룹니다
  • 새 노드 타입을 기존 코드 수정 없이 추가합니다
내주는 것
  • 기능이 크게 다른 클래스들에 공통 인터페이스를 강요하기 어렵습니다
현업 체감 사용 빈도
45
구현 복잡도
45
09 구조 패턴

데코레이터 Decorator

상속 대신 감싸서 기능을 더합니다

문제

알림에 SMS·슬랙·메일을 조합하려고 상속을 쓰면, 가능한 조합의 수만큼 클래스를 만들어야 합니다.

해결책

같은 인터페이스를 가진 래퍼로 객체를 감싸고, 필요한 만큼 래퍼를 겹겹이 중첩합니다.

이럴 때 씁니다
  • 런타임에 기능을 붙였다 뗐다 해야 할 때
  • final 클래스처럼 상속으로 확장할 수 없을 때
얻는 것
  • 새 자식 클래스 없이 행동을 확장합니다
  • 여러 행동을 자유롭게 조합합니다
내주는 것
  • 래퍼 스택 중간의 하나만 걷어내기 어렵고, 감싼 순서가 결과를 바꿉니다
현업 체감 사용 빈도
60
구현 복잡도
45
10 구조 패턴

퍼사드 Facade

복잡한 서브시스템 앞에 문 하나를 냅니다

문제

라이브러리 수십 개 클래스를 정해진 순서로 초기화하고 호출해야 하는데, 그 절차가 비즈니스 코드 전체에 번져 있습니다.

해결책

자주 쓰는 시나리오만 노출하는 단순한 인터페이스를 하나 만들어 그 뒤로 복잡도를 감춥니다.

이럴 때 씁니다
  • 복잡한 시스템에 제한적이지만 직관적인 진입점이 필요할 때
  • 서브시스템을 계층으로 나누고 싶을 때
얻는 것
  • 서브시스템의 복잡도로부터 코드를 격리합니다
내주는 것
  • 퍼사드가 모든 것에 결합된 신(God) 객체로 자랄 수 있습니다
현업 체감 사용 빈도
75
구현 복잡도
15
11 구조 패턴

플라이웨이트 Flyweight

공통 상태는 나누고, 고유 상태만 들고 다닙니다

문제

총알 수만 개가 각자 텍스처와 색상 데이터를 복사해서 들고 있으면, 같은 내용이 수만 번 중복되어 메모리가 바닥납니다.

해결책

변하지 않는 내적 상태를 공유 객체로 빼내고, 변하는 외적 상태는 호출할 때 인수로 넘깁니다.

이럴 때 씁니다
  • 비슷한 객체를 대량으로 만들어야 하고 메모리가 부족할 때
얻는 것
  • 메모리 사용량을 극적으로 줄입니다
내주는 것
  • CPU 시간과 코드 복잡도를 메모리와 맞바꿉니다
  • 실제로 메모리가 문제가 아니라면 적용할 이유가 없습니다
현업 체감 사용 빈도
25
구현 복잡도
65
12 구조 패턴

프록시 Proxy

진짜 객체 앞에 대리인을 세웁니다

문제

시작할 때마다 무거운 객체를 준비해 두지만, 실제로 쓰이는 것은 그중 일부뿐입니다.

해결책

같은 인터페이스를 구현한 대리 객체가 요청을 가로채, 필요한 순간에만 실제 객체를 만들고 전달합니다.

이럴 때 씁니다
  • 지연 초기화, 접근 제어, 원격 호출, 캐싱, 로깅이 필요할 때
얻는 것
  • 클라이언트가 모르게 부가 동작을 끼워 넣습니다
  • 실제 객체의 수명을 대신 관리합니다
내주는 것
  • 호출 경로가 길어져 응답이 늦어질 수 있습니다
현업 체감 사용 빈도
45
구현 복잡도
40

PART 5 · BEHAVIORAL · 10

행동 패턴

객체가 어떻게 협업할 것인가. 책임을 나누고 소통을 설계하는 열 가지 방법입니다.

  • 01책임 연쇄
  • 02커맨드
  • 03반복자
  • 04중재자
  • 05메멘토
  • 06옵서버
  • 07상태
  • 08전략
  • 09템플릿 메서드
  • 10비지터
13 행동 패턴

책임 연쇄 Chain of Responsibility

처리기를 줄 세우고 요청을 흘려보냅니다

문제

인증·권한·속도 제한·캐시 검사가 한 함수 안에 겹겹이 쌓이면서, 순서를 바꾸는 것조차 위험해졌습니다.

해결책

각 검사를 독립된 핸들러로 만들어 사슬로 잇고, 처리하거나 다음 핸들러로 넘기게 합니다.

이럴 때 씁니다
  • 처리기의 종류와 순서를 런타임에 바꿔야 할 때
  • 요청을 여러 객체가 순서대로 검토해야 할 때
얻는 것
  • 처리 순서를 자유롭게 조립합니다
  • 각 핸들러가 한 가지만 책임집니다
내주는 것
  • 요청이 아무에게도 처리되지 않은 채 사슬 끝에 도달할 수 있습니다
현업 체감 사용 빈도
45
구현 복잡도
40
14 행동 패턴

커맨드 Command

요청을 객체로 만들어 보관합니다

문제

버튼·메뉴·단축키가 결국 같은 일을 하는데, UI 클래스마다 같은 로직이 복사되어 있습니다.

해결책

작업을 execute()를 가진 커맨드 객체로 감싸고, 호출자는 그 인터페이스만 알게 합니다.

이럴 때 씁니다
  • 작업을 큐에 넣거나 예약해야 할 때
  • 실행 취소와 재실행을 지원해야 할 때
얻는 것
  • 작업을 시키는 쪽과 수행하는 쪽을 분리합니다
  • 실행 취소와 작업 이력이 가능해집니다
내주는 것
  • 호출자와 수신자 사이에 계층이 하나 더 생깁니다
현업 체감 사용 빈도
70
구현 복잡도
35
15 행동 패턴

반복자 Iterator

내부 구조는 감추고 순회 방법만 건넵니다

문제

배열·트리·그래프마다 순회 코드가 다른데, 컬렉션이 저장뿐 아니라 순회 기능까지 떠맡아 비대해집니다.

해결책

순회 상태와 알고리즘을 별도의 반복자 객체로 옮깁니다.

이럴 때 씁니다
  • 컬렉션의 내부 표현을 감추고 싶을 때
  • 같은 컬렉션에 여러 순회 방식을 제공해야 할 때
얻는 것
  • 순회 코드를 재사용하고 병렬 순회가 가능해집니다
  • 순회를 잠시 멈췄다가 이어서 할 수 있습니다
내주는 것
  • 단순한 컬렉션에는 과한 설계입니다
현업 체감 사용 빈도
65
구현 복잡도
25
16 행동 패턴

중재자 Mediator

서로 직접 부르지 말고 관제탑을 거칩니다

문제

폼 위젯들이 서로를 직접 참조하기 시작하면, 위젯 하나를 다른 화면에서 재사용하는 것이 불가능해집니다.

해결책

컴포넌트 간 통신을 중재자에게 몰아주고, 각 컴포넌트는 중재자 하나만 알게 합니다.

이럴 때 씁니다
  • 컴포넌트들이 강하게 얽혀 재사용이 어려울 때
  • 다른 컴포넌트에 묶여 자식 클래스를 만들 수 없을 때
얻는 것
  • 통신 로직이 한곳에 모입니다
  • 컴포넌트를 다른 맥락에서 재사용할 수 있습니다
내주는 것
  • 중재자가 비대한 신(God) 객체로 자랄 수 있습니다
현업 체감 사용 빈도
40
구현 복잡도
45
17 행동 패턴

메멘토 Memento

상태를 봉인해 저장하고 되돌립니다

문제

실행 취소를 만들려면 객체 내부를 들여다봐야 하는데, 그러는 순간 캡슐화가 깨집니다.

해결책

객체가 스스로 스냅샷을 만들고, 관리자는 내용을 열어보지 못한 채 보관만 합니다.

이럴 때 씁니다
  • 실행 취소·이력 복원·트랜잭션 롤백이 필요할 때
얻는 것
  • 캡슐화를 깨지 않고 상태를 복원합니다
  • 상태 보관 책임을 원본 객체에서 덜어 냅니다
내주는 것
  • 스냅샷을 자주 만들면 메모리를 크게 소모합니다
현업 체감 사용 빈도
30
구현 복잡도
55
18 행동 패턴

옵서버 Observer

관심 있는 대상에게만 자동으로 알립니다

문제

신제품 입고를 알기 위해 매일 매장에 전화하거나, 반대로 관심 없는 사람에게까지 메일을 보내게 됩니다.

해결책

발행자가 구독자 목록을 관리하고, 상태가 바뀌면 등록된 구독자에게만 통지합니다.

이럴 때 씁니다
  • 한 객체의 변화에 다른 객체들이 반응해야 할 때
  • 반응할 대상이 런타임에 바뀔 때
얻는 것
  • 발행자를 고치지 않고 구독자를 추가합니다
  • 런타임에 관계를 맺고 끊습니다
내주는 것
  • 구독자에게 통지되는 순서가 보장되지 않습니다
현업 체감 사용 빈도
80
구현 복잡도
35
19 행동 패턴

상태 State

상태를 조건문이 아니라 클래스로 표현합니다

문제

상태가 하나 늘 때마다 모든 메서드에 분기가 추가되고, 결국 조건문이 코드 전체를 뒤덮습니다.

해결책

상태마다 클래스를 만들고, 컨텍스트는 현재 상태 객체에 행동을 위임합니다.

이럴 때 씁니다
  • 상태에 따라 행동이 크게 달라질 때
  • 상태 전이 규칙이 복잡하고 자주 바뀔 때
얻는 것
  • 상태별 코드가 분리되고 전이가 명시적으로 드러납니다
  • 새 상태를 기존 코드 수정 없이 추가합니다
내주는 것
  • 상태가 두세 개뿐이면 오히려 과한 구조입니다
현업 체감 사용 빈도
60
구현 복잡도
40
20 행동 패턴

전략 Strategy

알고리즘을 갈아 끼울 수 있게 만듭니다

문제

경로 안내에 도보·자동차·대중교통을 더할수록 하나의 클래스가 끝없이 부풀어 오르고, 한쪽 수정이 다른 쪽을 망가뜨립니다.

해결책

알고리즘을 각각 전략 클래스로 빼내고, 컨텍스트는 인터페이스를 통해 위임만 합니다.

이럴 때 씁니다
  • 같은 일을 하는 여러 방식을 런타임에 바꿔야 할 때
  • 알고리즘만 다르고 나머지가 같은 클래스가 여럿일 때
얻는 것
  • 실행 중에 알고리즘을 교체합니다
  • 구현 세부를 클라이언트에서 감춥니다
내주는 것
  • 알고리즘이 두세 개뿐이고 거의 바뀌지 않으면 클래스만 늘어납니다
현업 체감 사용 빈도
85
구현 복잡도
20
21 행동 패턴

템플릿 메서드 Template Method

뼈대는 고정하고 살만 바꿉니다

문제

문서 종류마다 분석 절차가 거의 같은데, 클래스마다 같은 코드가 통째로 복제되어 있습니다.

해결책

알고리즘 골격을 부모의 템플릿 메서드로 정의하고, 달라지는 단계만 자식이 재정의합니다.

이럴 때 씁니다
  • 절차는 같고 일부 단계만 다른 구현이 여럿일 때
  • 프레임워크가 확장 지점을 정해 주고 싶을 때
얻는 것
  • 중복 코드가 사라지고 절차가 한곳에서 관리됩니다
내주는 것
  • 골격 자체를 바꾸기 어렵고, 리스코프 치환 원칙을 위반하기 쉽습니다
현업 체감 사용 빈도
55
구현 복잡도
20
22 행동 패턴

비지터 Visitor

구조는 그대로 두고 연산만 바깥에서 더합니다

문제

도시 그래프의 모든 노드를 XML로 내보내는 기능이 필요한데, 이미 운영 중인 노드 클래스들은 손댈 수 없습니다.

해결책

새 동작을 방문자 객체에 담고, 각 노드는 accept()로 자신에게 맞는 메서드를 불러 줍니다.

이럴 때 씁니다
  • 복잡한 객체 구조 전체에 새 연산을 자주 추가해야 할 때
  • 구조와 무관한 보조 동작을 분리하고 싶을 때
얻는 것
  • 관련 없는 동작을 구조 밖으로 밀어냅니다
  • 구조를 순회하면서 정보를 누적할 수 있습니다
내주는 것
  • 계층에 클래스가 추가될 때마다 모든 방문자를 고쳐야 합니다
현업 체감 사용 빈도
20
구현 복잡도
60

5-1 · 정리

22개 패턴 한눈에 보기

생성 패턴 5

  • 01팩토리 메서드Factory Method객체를 만드는 책임을 자식 클래스에 넘깁니다
  • 02추상 팩토리Abstract Factory서로 어울리는 객체들을 한 벌씩 통째로 만듭니다
  • 03빌더Builder복잡한 객체를 단계별로 조립합니다
  • 04프로토타입Prototype새로 만들지 않고 기존 객체를 복제합니다
  • 05싱글턴Singleton인스턴스는 하나, 접근점도 하나로 고정합니다

구조 패턴 7

  • 06어댑터Adapter맞지 않는 인터페이스 사이에 통역사를 세웁니다
  • 07브리지Bridge추상과 구현을 갈라 각각 따로 키웁니다
  • 08복합체Composite부분과 전체를 똑같은 방식으로 다룹니다
  • 09데코레이터Decorator상속 대신 감싸서 기능을 더합니다
  • 10퍼사드Facade복잡한 서브시스템 앞에 문 하나를 냅니다
  • 11플라이웨이트Flyweight공통 상태는 나누고, 고유 상태만 들고 다닙니다
  • 12프록시Proxy진짜 객체 앞에 대리인을 세웁니다

행동 패턴 10

  • 13책임 연쇄Chain of Responsibility처리기를 줄 세우고 요청을 흘려보냅니다
  • 14커맨드Command요청을 객체로 만들어 보관합니다
  • 15반복자Iterator내부 구조는 감추고 순회 방법만 건넵니다
  • 16중재자Mediator서로 직접 부르지 말고 관제탑을 거칩니다
  • 17메멘토Memento상태를 봉인해 저장하고 되돌립니다
  • 18옵서버Observer관심 있는 대상에게만 자동으로 알립니다
  • 19상태State상태를 조건문이 아니라 클래스로 표현합니다
  • 20전략Strategy알고리즘을 갈아 끼울 수 있게 만듭니다
  • 21템플릿 메서드Template Method뼈대는 고정하고 살만 바꿉니다
  • 22비지터Visitor구조는 그대로 두고 연산만 바깥에서 더합니다

5-2 · 적용 가이드

무엇부터 의심해야 할까요?

패턴 이름을 먼저 떠올리지 마세요. 증상에서 출발하면 패턴은 저절로 좁혀집니다.

코드 곳곳에 new 구상클래스()가 흩어져 있다팩토리 메서드 · 추상 팩토리
생성자 매개변수가 열 개를 넘어간다빌더
같은 조건 분기가 여러 메서드에 반복된다전략 · 상태
외부 라이브러리 형식이 우리 코드와 맞지 않는다어댑터
기능 조합의 수만큼 자식 클래스가 생긴다데코레이터 · 브리지
한 객체의 변화에 여러 객체가 반응해야 한다옵서버
실행 취소나 작업 이력이 필요하다커맨드 · 메멘토
트리 구조를 재귀로 매번 헤집고 있다복합체
객체들이 서로를 직접 참조해 얽혀 있다중재자 · 퍼사드

5-3 · 주의

패턴은 도구입니다. 목표가 아닙니다.

패턴을 배운 직후가 가장 위험합니다. 모든 문제가 패턴처럼 보이기 시작합니다.

추상화를 더할수록 얻는 것과 잃는 것

추상 계층의 수 →여기가 최적점과소 설계과잉 설계
  • 먼저 문제가 나타나야 합니다“나중에 바뀔지도 모른다”는 예감만으로 패턴을 넣으면, 대부분 오지 않는 미래를 위해 오늘의 가독성을 냅니다.
  • 단순함이 기본값입니다같은 값을 하는 두 설계가 있다면 단순한 쪽이 옳습니다.
  • 이름을 남기세요패턴을 적용했다면 클래스 이름과 주석에 그 사실을 드러내세요. 다음 사람이 읽을 지도가 됩니다.

정리

패턴은 답이 아니라
질문하는 방법입니다

“이 코드에서 바뀔 것은 무엇이고, 바뀌지 않을 것은 무엇인가.”
22개 패턴은 이 질문에 대한 22가지 대답이었습니다.

01

직접 그려 보기

지금 맡고 있는 코드에서 가장 자주 열리는 파일을 하나 고르고, 그 파일이 열리는 이유를 모두 적어 보세요.

02

하나만 적용하기

증상이 명확한 곳에 패턴 하나만 적용하고, 코드 리뷰에서 그 이유를 설명해 보세요.

03

팀의 언어로 만들기

설계 논의에서 패턴 이름을 쓰기 시작하면, 그때부터 패턴은 지식이 아니라 도구가 됩니다.

원저 · Alexander Shvets 『디자인 패턴에 뛰어들기』(Refactoring.Guru) · 삽화 드미트리 자르트 · 옮긴이 황우진
이 슬라이드는 원서의 목차 구성을 참고해 교육용으로 새로 작성한 요약본입니다. 상세한 예제 코드와 설명은 원서를 참고하세요.

← → 또는 Space · ↑↓ 슬라이드 · F 전체화면