테마 선택

📚 지식 노트

AI·하드웨어·주변기기·게임·컴퓨터 시스템·이론 컴퓨터 과학·수학·개발과 운영·보안·응용 분야에 대해 알아 두면 좋은 내용을 정리한 노트다. 위에서 분야를 고르고 아래에서 노트를 고른다.

디자인 패턴이란: 건축에서 소프트웨어로

건축가 알렉산더의 패턴 언어에서 GoF 23가지까지, 패턴이 무엇이고 언제 쓰고 언제 쓰지 말아야 하는지 정리한다.

목차 (15)
  1. 건축가 알렉산더와 『A Pattern Language』
  2. 패턴 언어: 패턴은 혼자 쓰이지 않는다
  3. 소프트웨어로 건너오다: 1987년 OOPSLA
  4. GoF와 23가지 패턴
  5. 패턴 문서는 무엇으로 이뤄지나
  6. 원칙 1: 구현이 아니라 인터페이스에 맞춰 프로그래밍한다
  7. 원칙 2: 상속보다 객체 합성
  8. 클래스 패턴과 객체 패턴
  9. 패턴은 언제 쓰나: 문제에서 출발한다
  10. 남용: 패턴병과 쓸데없는 추상
  11. 비판과 의의: 언어가 패턴을 지운다
  12. 안티패턴: 흔하지만 해로운 해법
  13. 공부하는 법
  14. 흔한 오해
  15. 핵심 자료

건축가 알렉산더와 『A Pattern Language』

디자인 패턴이라는 말은 소프트웨어가 아니라 건축에서 왔다. 오스트리아 빈에서 태어난 건축가이자 디자인 이론가 크리스토퍼 알렉산더(1936~2022)는 1963년부터 40년 가까이 버클리에서 건축을 가르쳤고, 동료들과 함께 1977년 『A Pattern Language』를 냈다. 이 책은 도시와 동네, 건물, 방을 짓는 데서 되풀이되는 문제와 해법 253개를 하나씩 적은 것이다.

왼쪽 카드에 맥락(사람이 오가는 동네 길가), 문제(편히 머물며 지나는 사람을 볼 곳이 없다), 해법(길로 열린 작은 카페, 길 쪽 테이블, 안쪽의 조용한 자리)을 적었다. 오른쪽은 길과 카페, 길 쪽 테이블을 위에서 본 평면도다패턴 카드: 길가 카페(88번)그 해법을 그린 평면도맥락사람이 오가는 동네 길가문제편히 앉아 머물면서지나는 사람을 볼 곳이 없다해법길로 열린 작은 카페를 두고테이블 몇 개는 길 쪽에,안쪽엔 조용한 자리를 둔다↑ 이 패턴을 품는 더 큰 패턴↓ 이 패턴을 채우는 더 작은 패턴패턴은 번호로 서로를 가리킨다안쪽: 조용한 자리길로 열린 문길 쪽 테이블길 쪽 테이블사람이 지나는 길평면도는 이 노트가 직접 그린 예시다
패턴 한 장을 카드로 옮기고(왼쪽), 그 해법을 평면도로 그렸다(오른쪽). 카드의 문구와 그림은 이 노트가 직접 정리한 것이다.

패턴 하나는 세 부분으로 읽을 수 있다. 맥락(어떤 자리에서), 문제(무엇이 거듭 어긋나는가), 해법(그 어긋남을 푸는 배치의 뼈대)이다. 해법은 치수가 정해진 도면이 아니라 뼈대여서, 같은 패턴으로 지은 카페도 동네마다 모양이 다르다. 책은 패턴을 확정된 진리가 아니라 가설로 다루고, 별표 수로 얼마나 확신하는지를 표시했다.

알렉산더는 이 생각을 실제 계획에도 썼다. 오리건 대학교는 그의 패턴 기반 계획 방법(『The Oregon Experiment』, 1975)을 받아들였고, 지금도 그것이 대학의 공식 계획 도구다. 그는 200채가 넘는 건물을 직접 설계하고 시공까지 맡았다.

패턴 언어: 패턴은 혼자 쓰이지 않는다

책의 제목이 "패턴 모음"이 아니라 "패턴 언어"인 까닭은 패턴끼리 이어져 있어서다. 각 패턴은 본문에서 다른 패턴을 괄호 속 번호로 가리킨다. 저자들은 253개 패턴이 모여 하나의 언어를 이룬다고 썼다. 낱말이 문법으로 이어져 문장이 되듯, 큰 패턴이 맥락을 주고 작은 패턴이 그 안을 채운다.

세 층의 패턴이 선으로 이어진 그물. 맨 위 동네 규모에 동네의 중심 거리와 걸어 다니는 길, 가운데 건물 규모에 길가 카페와 모퉁이 가게와 작은 광장, 맨 아래 자리 규모에 밖이 보이는 자리, 안쪽 조용한 구석, 여러 모양의 의자가 있다. 길가 카페에 닿는 선이 강조되어 있다동네건물자리동네의 중심 거리걸어 다니는 길길가 카페모퉁이 가게작은 광장밖이 보이는 자리안쪽 조용한 구석여러 모양의 의자선을 따라 위로 가면 더 큰 맥락이고, 아래로 가면 해법을 채우는 세부다이름은 설명용 예시다. 책의 253개 패턴도 번호로 서로를 가리키며 이렇게 이어진다
큰 규모의 패턴이 작은 규모의 패턴으로 채워지며 그물처럼 이어진다. 길가 카페 하나만 봐도 위로 두 패턴, 아래로 세 패턴에 닿는다. 이름은 설명용 예시다.

2년 뒤에 나온 『The Timeless Way of Building』(1979)은 이 언어의 철학 편이다. 나중에 나왔지만 사실상 『A Pattern Language』의 서론에 해당한다. 알렉산더는 살아 있는 건물과 동네가 지닌, 한 낱말로 정의할 수 없는 성질을 "이름 없는 특질"(quality without a name)이라 부르고, 여러 낱말로 그 둘레를 그리며 다가간다.

소프트웨어 쪽 패턴에서 이 생각의 두 가지가 그대로 살아남았다. 하나는 이름이 곧 어휘라는 것이다. "여기는 옵저버로 하자"는 한마디가 클래스 그림 한 장을 대신한다. 다른 하나는 패턴끼리의 관계다. GoF 책도 패턴마다 "관련 패턴" 항목을 두어 함께 쓰거나 헷갈리기 쉬운 패턴을 잇는다.

소프트웨어로 건너오다: 1987년 OOPSLA

패턴 언어를 프로그래밍에 처음 옮겨 본 것은 켄트 벡과 워드 커닝엄이다. 두 사람은 1987년 OOPSLA(객체 지향 프로그래밍 학회) 워크숍에 짧은 보고 「Using Pattern Languages for Object-Oriented Programs」를 냈다. Smalltalk로 창 기반 화면을 설계하는 패턴 다섯 개(Window Per Task, Few Panes Per Window, Standard Panes, Short Menus, Nouns and Verbs)를 영향이 큰 것부터 차례로 놓은 작은 언어였다.

1975년부터 2000년까지의 연표. 1977년 패턴 언어, 1979년 The Timeless Way of Building, 1987년 벡과 커닝엄의 OOPSLA 발표, 1990년 OOPSLA 모임, 1994년 GoF의 책, 1995년 코닉의 안티패턴, 1996년 노빅의 발표, 1998년 책 AntiPatterns19801990왼쪽 짙은 막대는 건축의 시기, 나머지는 소프트웨어다1977패턴 언어1979The Timeless Way1987벡, 커닝엄1990OOPSLA 모임1994GoF 23가지1995코닉: 안티패턴1996노빅 발표1998『AntiPatterns』
패턴의 연표. 건축의 두 책(1977, 1979)에서 소프트웨어의 첫 시도(1987), GoF(1994), 그 뒤의 반성(1995~1998)까지다.

보고에 따르면 Smalltalk 화면 구조를 모르는 업무 전문가들이 하루 연습 뒤에 쓸 만한 화면을 설계했다. 저자들은 사용자가 자기 프로그램을 직접 써야 한다는 주장과 함께, 객체 지향 프로그래밍 전체에 대한 더 큰 패턴 언어를 짓는 중이라고 적었다. 커닝엄은 뒤에 첫 위키(WikiWikiWeb)를 만들었는데, 이것도 알렉산더의 패턴 생각에서 직접 영감을 받은 것이다.

GoF와 23가지 패턴

패턴을 프로그래머의 상식으로 만든 것은 1994년 책 『Design Patterns: Elements of Reusable Object-Oriented Software』다. 저자는 에리히 감마, 리처드 헬름, 랄프 존슨, 존 블리시디스 넷이고, 넷을 묶어 GoF(Gang of Four)라 부른다. 1990년 OOPSLA의 자유 모임에서 출발해 1994년 OOPSLA에서 처음 선보였고, 2005년 ACM SIGPLAN 프로그래밍 언어 업적상을 받았다.

책은 패턴 23가지를 목적에 따라 셋으로 나눈다. 생성(5개)은 객체를 만드는 일, 구조(7개)는 클래스와 객체를 엮는 일, 행위(11개)는 객체 사이의 책임과 대화를 다룬다.

GoF 패턴 23가지를 생성 5개, 구조 7개, 행위 11개의 세 열로 나눈 표. 팩터리 메서드, 어댑터, 인터프리터, 템플릿 메서드는 상속으로 엮는 클래스 범위라 강조되어 있고, 어댑터는 클래스와 객체 두 방식이 다 있다생성 5추상 팩터리빌더팩터리 메서드클래스프로토타입싱글턴구조 7어댑터클래스, 객체브리지컴포지트데코레이터퍼사드플라이웨이트프록시행위 11책임 연쇄커맨드인터프리터클래스이터레이터중재자메멘토옵저버상태전략템플릿 메서드클래스방문자클래스 범위: 상속으로 엮어 컴파일 때 정해진다객체 범위: 합성으로 엮어 실행 중에 바꿀 수 있다
23가지를 분류별로 나눴다. 강조 색은 상속으로 엮는 클래스 범위 패턴이고, 나머지는 합성으로 엮는 객체 범위다. 목록은 계산 함수와 같은 표에서 그렸다.

아래 표의 "바뀌게 두는 것"은 각 패턴이 변경을 받아 내려고 열어 두는 자리를 이 노트의 말로 적은 것이다. 패턴을 고를 때 가장 쓸모 있는 열이다. 이름을 누르면 그 패턴을 자세히 다룬 글로 간다.

분류패턴범위바뀌게 두는 것
생성 5추상 팩터리 (Abstract Factory)객체함께 쓰는 제품 묶음
빌더 (Builder)객체복잡한 객체를 조립하는 과정
팩터리 메서드 (Factory Method)클래스만들 구체 클래스(하위 클래스가 정함)
프로토타입 (Prototype)객체만들 구체 클래스(본보기를 복제)
싱글턴 (Singleton)객체하나뿐인 인스턴스와 그 접근
구조 7어댑터 (Adapter)둘 다맞지 않는 인터페이스
브리지 (Bridge)객체추상 쪽과 구현 쪽을 따로
컴포지트 (Composite)객체부분과 전체의 트리 구조
데코레이터 (Decorator)객체하위 클래스 없이 더하는 책임
퍼사드 (Facade)객체하위 시스템 앞의 창구
플라이웨이트 (Flyweight)객체많은 객체의 저장 비용
프록시 (Proxy)객체객체에 닿는 방법과 시점
행위 11책임 연쇄 (Chain of Responsibility)객체요청을 처리할 객체
커맨드 (Command)객체요청을 언제 어떻게 실행하나
인터프리터 (Interpreter)클래스작은 언어의 문법과 해석
이터레이터 (Iterator)객체모음의 원소를 도는 방법
중재자 (Mediator)객체객체끼리 어떻게 대화하나
메멘토 (Memento)객체밖에 저장해 둘 내부 상태
옵저버 (Observer)객체변화를 알아야 할 객체들
상태 (State)객체상태에 따른 행동
전략 (Strategy)객체알고리즘 전체
템플릿 메서드 (Template Method)클래스알고리즘의 일부 단계
방문자 (Visitor)객체클래스를 고치지 않고 더할 연산
  • 생성 패턴: 객체를 누가, 언제, 어떻게 만들지 감춘다.
  • 구조 패턴: 클래스와 객체를 엮어 더 큰 구조를 만든다.
  • 행위 패턴: 객체 사이의 책임과 대화를 나눈다.

하위 글은 생성 패턴, 구조 패턴 1(어댑터, 브리지, 컴포지트, 데코레이터), 구조 패턴 2(퍼사드, 플라이웨이트, 프록시), 행위 패턴 1(전략, 옵저버, 커맨드, 상태, 템플릿 메서드), 행위 패턴 2(나머지 여섯)로 나뉜다. GoF 이후에 자리 잡은 의존성 주입, 엔터프라이즈 패턴, 동시성 패턴은 GoF 너머에 있다.

패턴 문서는 무엇으로 이뤄지나

알렉산더의 카드가 맥락, 문제, 해법이었다면 GoF는 이것을 프로그래머에게 맞게 더 잘게 나눴다. 패턴마다 같은 항목을 같은 순서로 써서, 처음 보는 패턴도 어디를 읽으면 무엇이 나오는지 안다.

패턴 문서의 열두 항목을 세 묶음으로 나눈 그림. 무엇이고 왜: 이름과 별칭, 의도, 동기, 적용 가능성. 어떻게: 구조, 참여자, 협력, 구현, 예시 코드. 그래서: 결과, 알려진 쓰임, 관련 패턴무엇이고 왜이름과 별칭함께 부를 낱말의도한두 문장 요약동기문제가 생기는 장면적용 가능성언제 쓰나어떻게구조클래스 그림참여자맡는 역할들협력역할끼리 주고받기구현짤 때의 요령과 함정예시 코드짧은 코드그래서결과얻는 것과 치르는 것알려진 쓰임실제 시스템의 예관련 패턴닮거나 함께 쓰는 것결과(trade-off)가 빠진 패턴 설명은 반쪽이다. 쓰면 무엇을 잃는지도 적는다
GoF 책이 패턴마다 쓰는 항목을 세 묶음으로 나눴다. 각 항목 밑의 한 줄은 그 항목이 답하는 질문이다.
  • 의도는 패턴을 한두 문장으로 줄인 것이다. 처음 훑을 때는 의도만 읽어도 후보를 고를 수 있다.
  • 동기는 문제가 실제로 터지는 장면 하나를 보여 준다. 패턴이 왜 필요한지 납득이 가는 곳이다.
  • 구조와 참여자는 클래스 그림과 역할 이름(예: Subject, Observer)이다. 실제 코드의 클래스 이름은 달라도 역할은 같다.
  • 결과는 얻는 것과 치르는 것이다. 간접 층이 하나 늘고, 클래스가 늘고, 흐름을 따라가기 어려워지는 대가가 여기 적힌다.

이 형식이 유일한 표준은 아니다. 패턴을 적는 형식은 여럿이고, 이 사이트의 하위 글도 의도, 구조 그림, 예시 코드, 쓰는 자리, 함정 순으로 줄여 쓴다.

원칙 1: 구현이 아니라 인터페이스에 맞춰 프로그래밍한다

GoF는 1장에서 23가지 패턴 밑에 깔린 원칙 둘을 먼저 꺼낸다. 첫째가 "구현이 아니라 인터페이스에 맞춰 프로그래밍하라"(Program to an interface, not an implementation)다. 변수와 매개변수의 타입을 구체 클래스가 아니라 추상 타입(인터페이스, 추상 클래스)으로 두라는 뜻이다. 그러면 호출하는 쪽은 "무엇을 할 수 있는가"만 알고 "어떻게 하는가"는 모른다.

왼쪽은 ReportPrinter가 구체 클래스 ArrayList를 받아 그것에만 묶인 모습, 오른쪽은 ReportPrinter가 List 인터페이스를 받고 ArrayList와 LinkedList가 그 인터페이스를 구현하는 모습이다. 아래에 그림 기호 범례가 있다구현에 맞춤인터페이스에 맞춤ReportPrinter+ print(ArrayList items)ArrayListLinkedList나 불변 목록을 넘기려면ReportPrinter를 고쳐야 한다ReportPrinter+ print(List items)«interface»ListArrayListLinkedListList를 지키는 것이면 무엇이든 받는다그림 기호(이 노트 전체)상속구현(인터페이스)보유(합성)의존(사용, 생성)
왼쪽은 ReportPrinter가 ArrayList라는 구현에 묶였고, 오른쪽은 List라는 약속에만 기댄다. 아래 기호는 이 노트의 모든 클래스 그림에 쓴다.
import java.util.ArrayList;
import java.util.List;

class ReportPrinter {
    // 구현에 맞춤: ArrayList만 받는다. LinkedList나 List.of(...)는 넘길 수 없다
    void printConcrete(ArrayList<String> lines) {
        lines.forEach(System.out::println);
    }

    // 인터페이스에 맞춤: List라는 약속만 지키면 무엇이든 받는다
    void print(List<String> lines) {
        lines.forEach(System.out::println);
    }
}

// 만드는 곳 한 군데만 구체 클래스를 안다
List<String> lines = new ArrayList<>();

구체 클래스 이름은 객체를 만드는 곳 한 군데에만 남는다. 그 한 군데마저 감추려는 것이 생성 패턴(팩터리 메서드, 추상 팩터리)이고, 바깥에서 꽂아 주는 것이 의존성 주입이다. 같은 생각을 모듈 사이로 넓힌 것이 SOLID의 의존성 역전 원칙(DIP)이며, 소프트웨어 공학 노트의 설계 원칙 절에 있다.

다른 언어에서는: Python과 JavaScript는 덕 타이핑이라 타입 이름을 적지 않아도 이미 "그 메서드가 있으면 된다"는 인터페이스에 맞춰 짜는 셈이다. 대신 약속이 코드에 드러나지 않으므로 Python은 typing.Protocol, TypeScript는 interface로 적어 두면 읽는 사람과 검사기가 함께 안다. Go는 인터페이스를 구현한다고 선언하지 않아도 메서드가 맞으면 저절로 만족하므로, 쓰는 쪽 패키지에 작은 인터페이스를 두는 관용구가 흔하다.

원칙 2: 상속보다 객체 합성

둘째 원칙은 "클래스 상속보다 객체 합성을 선호하라"(Favor object composition over class inheritance)다. 상속은 부모의 구현을 그대로 물려받아 빠르지만, 부모와 자식이 컴파일 때 단단히 묶인다. 합성은 다른 객체를 필드로 들고 일을 넘기는 것이라, 무엇을 들지 실행 중에 바꿀 수 있다.

상속의 약점이 가장 크게 드러나는 곳은 기능이 여러 축으로 갈릴 때다. 알림을 메일, 문자, 푸시로 보내는데 암호화, 재시도, 기록을 저마다 켜고 끌 수 있다고 하자. 조합마다 하위 클래스를 만들면 24개가 필요하다. 축을 나눠 부품으로 두면 6개다.

상속 폭발 단계 그림. 알림 채널 세 개에 부가 기능을 하나씩 더하면 하위 클래스가 3, 6, 12, 24개로 늘고, 마지막에는 Notifier가 Channel 인터페이스를 보유하고 채널 구현 셋과 감싸개 셋이 Channel을 구현하는 합성 구조로 바뀐다상속: 조합마다 하위 클래스 하나Notifier를 상속한 구체 클래스 3개 (E 암호화, R 재시도, L 기록)MailSmsPush상속: 조합마다 하위 클래스 하나Notifier를 상속한 구체 클래스 6개 (E 암호화, R 재시도, L 기록)MailMail+ESmsSms+EPushPush+E상속: 조합마다 하위 클래스 하나Notifier를 상속한 구체 클래스 12개 (E 암호화, R 재시도, L 기록)MailMail+EMail+RMail+ERSmsSms+ESms+RSms+ERPushPush+EPush+RPush+ER상속: 조합마다 하위 클래스 하나Notifier를 상속한 구체 클래스 24개 (E 암호화, R 재시도, L 기록)MailMail+EMail+RMail+ERMail+LMail+ELMail+RLMail+ERLSmsSms+ESms+RSms+ERSms+LSms+ELSms+RLSms+ERLPushPush+EPush+RPush+ERPush+LPush+ELPush+RLPush+ERL합성: 채널은 꽂고, 부가 기능은 감싼다Notifier- channel«interface»Channel+ send(to, text)MailSmsPush암호화- inner재시도- inner기록- inner감싸개도 Channel이라서로를 겹겹이 감싼다상속 클래스3합성 부품3상속 클래스6합성 부품4상속 클래스12합성 부품5상속 클래스24합성 부품6상속 클래스24합성 부품6
1 / 5

채널이 메일, 문자, 푸시 셋이면 Notifier의 하위 클래스도 셋이다. 아직은 괜찮다.

부가 기능을 하나 더할 때마다 상속 조합은 두 배로 늘고, 합성 부품은 하나씩만 는다. 마지막 단계는 합성으로 정리한 구조다. 클래스 수는 계산 함수로 구했다.
// 상속: 조합마다 하위 클래스가 하나씩 생긴다
class MailNotifier extends Notifier { ... }
class EncryptedMailNotifier extends MailNotifier { ... }
class RetryingEncryptedMailNotifier extends EncryptedMailNotifier { ... }
// ... 채널 3개 × 부가 기능 3개를 켜고 끄는 조합이면 24개
interface Channel {
    void send(String to, String text);
}

final class MailChannel implements Channel {
    public void send(String to, String text) { /* SMTP로 보낸다 */ }
}

// 부가 기능은 Channel을 감싸는 Channel이다(데코레이터)
final class Encrypting implements Channel {
    private final Channel inner;

    Encrypting(Channel inner) {
        this.inner = inner;
    }

    public void send(String to, String text) {
        inner.send(to, encrypt(text));
    }

    private String encrypt(String text) { /* 생략 */ return text; }
}

final class Notifier {
    private final Channel channel;

    Notifier(Channel channel) {
        this.channel = channel;
    }

    void alert(String to, String text) {
        channel.send(to, text);
    }
}

// 조합은 클래스가 아니라 조립으로 만든다
Notifier notifier = new Notifier(new Retrying(new Encrypting(new MailChannel())));

부가 기능을 같은 인터페이스로 감싸는 이 모양이 데코레이터이고, 채널을 갈아 끼우는 부분은 전략과 같은 생각이다. 합성에도 값이 있다. 감싸는 쪽이 안쪽 메서드마다 넘기는 코드를 손으로 써야 하고, 객체가 여러 겹이면 디버거로 따라가기가 번거롭다. GoF도 상속과 합성은 보통 함께 쓰인다고 말한다. "상속 금지"가 아니라 "먼저 합성을 떠올려 보라"는 뜻이다.

다른 언어에서는: 넘기는 코드를 줄여 주는 문법이 있다. Kotlin은 class Logged(inner: Channel) : Channel by inner처럼 by로 위임을 한 줄에 쓰고, Go는 구조체에 다른 타입을 이름 없이 넣는 임베딩으로 메서드를 올려 쓴다. Python은 데코레이터 문법(@)이 함수 감싸기를 언어로 지원한다.

클래스 패턴과 객체 패턴

GoF는 분류 말고도 패턴을 범위로 한 번 더 나눈다. 클래스 패턴은 관계를 상속으로 맺어 컴파일 때 정해지고, 객체 패턴은 합성으로 맺어 실행 중에 바뀔 수 있다. 23가지 가운데 클래스 범위는 팩터리 메서드, 어댑터, 인터프리터, 템플릿 메서드뿐이고 나머지는 모두 객체 범위다. 둘째 원칙이 패턴 목록에도 그대로 드러난 셈이다.

왼쪽 클래스 어댑터는 Printer 인터페이스를 구현하면서 LegacyPrinter를 상속한다. 오른쪽 객체 어댑터는 Printer를 구현하고 LegacyPrinter 객체를 필드로 보유해 호출을 넘긴다클래스 패턴: 상속으로 붙는다객체 패턴: 감싸서 넘긴다«interface»Printer+ print(text)LegacyPrinter+ printBytes(b)ClassAdapter+ print(text)어느 LegacyPrinter인지 컴파일 때 정해진다«interface»Printer+ print(text)LegacyPrinter+ printBytes(b)ObjectAdapter- legacyprint()가 오면printBytes()로 넘긴다하위 클래스 객체도 실행 중에 꽂는다Java는 클래스를 하나만 상속하므로 클래스 어댑터는 맞출 쪽이 인터페이스일 때만 된다
같은 어댑터를 두 방식으로 만들었다. 클래스 어댑터는 오래된 클래스를 상속하고, 객체 어댑터는 그 객체를 들고 호출을 넘긴다.
import java.nio.charset.StandardCharsets;

interface Printer {
    void print(String text);
}

class LegacyPrinter {
    void printBytes(byte[] data) { /* 오래된 드라이버 */ }
}

// 클래스 어댑터: LegacyPrinter를 상속해 붙는다
class ClassAdapter extends LegacyPrinter implements Printer {
    public void print(String text) {
        printBytes(text.getBytes(StandardCharsets.UTF_8));
    }
}

// 객체 어댑터: LegacyPrinter 객체를 감싸고 호출을 넘긴다
class ObjectAdapter implements Printer {
    private final LegacyPrinter legacy;

    ObjectAdapter(LegacyPrinter legacy) {
        this.legacy = legacy;
    }

    public void print(String text) {
        legacy.printBytes(text.getBytes(StandardCharsets.UTF_8));
    }
}
  • 클래스 어댑터는 넘기는 코드가 짧고 부모의 protected 멤버도 쓸 수 있다. 대신 LegacyPrinter의 하위 클래스가 있어도 그것까지 맞춰 주지는 못하고, 다중 상속이 없는 Java에서는 맞출 쪽(Printer)이 인터페이스여야 한다.
  • 객체 어댑터는 생성자로 받은 어떤 LegacyPrinter든, 그 하위 클래스 객체든 감쌀 수 있다. 실무에서 보는 어댑터는 대개 이쪽이다.
  • 템플릿 메서드와 팩터리 메서드가 클래스 패턴인 것도 같은 이유다. 둘 다 "하위 클래스가 빈칸을 채운다"는 상속의 구조 자체가 패턴이다.

패턴은 언제 쓰나: 문제에서 출발한다

패턴은 목록에서 고르는 것이 아니라 증상에서 거꾸로 찾아가는 것이다. 같은 분기가 여러 곳에 퍼졌다, 새 종류를 더할 때마다 기존 코드를 고친다, 테스트에서 가짜를 꽂을 수 없다 같은 아픔이 먼저 있고, 패턴은 그 아픔에 붙은 이름이다. 아래는 증상 하나에서 출발해 질문 넷으로 패턴에 닿는 과정이다.

문제에서 패턴을 고르는 단계 그림. 증상, 무엇이 바뀌나, 어느 분류, 누가 바꾸나, 정말 필요한가를 차례로 지나 결제 수단의 분기를 전략 패턴으로 옮기기로 한다증상무엇이 바뀌나어느 분류누가 바꾸나정말 필요한가증상결제 수단마다 같은 switch가 세 곳에 있다금액 계산승인 요청환불수단이 하나 늘 때마다 세 곳을 모두 고친다무엇이 바뀌나바뀌는 것은 무엇인가?만드는 대상구조, 인터페이스처리 규칙수단마다 다른 것은 결제를 처리하는 규칙(알고리즘)이다어느 분류그러면 행위 패턴을 본다전략상태템플릿 메서드행동을 갈아 끼우는 후보 셋누가 바꾸나규칙은 누가, 언제 바꾸나?바깥이 고른다객체가 스스로뼈대는 같고 단계만사용자가 수단을 고르니 바깥이 정한다: 전략정말 필요한가분기를 둘 때보다 나아지나?수단이 2개, 늘 일 없음3개 이상, 계속 는다늘어날 것이 분명하니 전략으로 옮긴다. 둘뿐이면 if로 둔다분류는 출발점일 뿐이다. 마지막 질문에서 "아니오"면 패턴 없이 두는 것이 답이다
1 / 5

증상에서 출발한다. 결제 수단(카드, 계좌 이체, 포인트)마다 다른 처리가 switch로 세 메서드에 흩어져 있다.

문제에서 패턴으로 가는 질문 네 개. 위 줄은 지금 어느 질문에 있는지, 아래는 고를 수 있는 답과 고른 답이다.
  • 무엇이 바뀌나가 가장 중요한 질문이다. 위 표의 "바뀌게 두는 것" 열이 이 질문의 답을 패턴별로 모아 둔 것이다.
  • 지금 아픈가, 아플 것 같은가를 구분한다. 이미 두 번 겪은 변경이면 패턴으로 받아 둘 값이 있고, 상상 속의 변경이면 대개 오지 않는다(YAGNI).
  • 처음부터 패턴으로 설계하기보다 단순하게 짠 뒤 리팩터링으로 패턴 쪽으로 옮겨 가는 편이 보통 결과가 좋다. 패턴 모양이 코드에서 자라나도록 둔다.

남용: 패턴병과 쓸데없는 추상

패턴을 막 배운 사람이 가장 흔히 걸리는 병은 모든 것을 패턴으로 짜는 것이다. 구현이 하나뿐인 인터페이스, 한 곳에서만 쓰는 팩터리, 무엇도 바꾸지 않는 전략이 쌓이면 코드는 유연해지는 것이 아니라 읽을 곳만 늘어난다. 패턴은 간접 층을 하나 더하는 일이고, 간접 층은 공짜가 아니다. 잘못 쓴 패턴은 쓸데없는 복잡도를 더하고, 덧붙은 간접 호출 때문에 실행 속도가 떨어지기도 한다.

왼쪽은 인사말 하나를 찍으려고 GreeterFactoryProvider, GreeterFactory 인터페이스, DefaultGreeterFactory, Greeter 인터페이스, DefaultGreeter의 다섯 단계를 거치는 코드이고, 오른쪽은 greet 함수 하나다패턴병: 간접 단계 다섯필요한 만큼GreeterFactoryProvider팩터리를 고른다GreeterFactory«interface»DefaultGreeterFactory하나뿐인 구현Greeter«interface»DefaultGreeter하나뿐인 구현바뀔 일이 없는데 바뀔 자리를 미리 다섯 개 만들었다String greet(String name) {return "안녕, " + name;}다르게 인사할 날이 오면그때 인터페이스를 뺀다추상은 두 번째 쓰임이생겼을 때 값을 한다
인사말 하나를 찍으려고 다섯 단계를 거치는 코드(왼쪽)와 함수 하나(오른쪽). 바뀔 일이 생기면 그때 인터페이스를 빼도 늦지 않다.
  • 싱글턴 남용: 전역 변수에 이름을 붙인 것이 되기 쉽다. 숨은 의존이 생기고 테스트끼리 상태를 나눠 쓴다. 감마는 2009년 인터뷰에서 책을 다시 쓴다면 싱글턴을 빼고 싶다고 했다(다른 저자들은 동의하지 않았다).
  • 이름만 패턴: 클래스 이름에 Factory, Manager, Strategy를 붙였다고 패턴이 되지 않는다. 역할과 바뀌는 자리가 맞아야 한다.
  • 패턴을 위한 패턴: 면접이나 코드 리뷰에서 "패턴을 썼다"는 것이 목표가 되면, 문제가 없는 곳에 해법이 생긴다.

비판과 의의: 언어가 패턴을 지운다

가장 널리 인용되는 비판은 피터 노빅의 1996년 5월 Object World 발표 「Design Patterns in Dynamic Programming」이다. 노빅은 GoF 책을 살펴 Dylan이나 Lisp에서는 23개 가운데 16개가 보이지 않거나(언어의 일부가 되어 의식하지 못함) 더 단순해진다고 정리했다. 모든 쓰임이 아니라 적어도 일부 쓰임에서 질적으로 단순해진다는 주장이다.

노빅 발표의 정리를 막대로 그린 그림. 일급 타입 6개, 일급 함수 4개, 매크로 2개, 메서드 조합 2개, 멀티메서드 1개, 모듈 1개로 모두 16개 패턴이 보이지 않거나 단순해진다언어 기능보이지 않거나 단순해지는 패턴일급 타입6추상 팩터리, 플라이웨이트, 팩터리 메서드상태, 프록시, 책임 연쇄일급 함수4커맨드, 전략, 템플릿 메서드방문자매크로2인터프리터, 이터레이터메서드 조합2중재자, 옵저버멀티메서드1빌더모듈1퍼사드합계 16 / 23Lisp, Dylan 기준. 모든 쓰임이 아니라 적어도 일부 쓰임에서
노빅 발표의 정리. 일급 타입과 일급 함수만으로 열 개다. 막대와 합계는 계산 함수로 셌다.

예를 들어 함수를 값으로 넘길 수 있으면 전략과 커맨드는 클래스 계층 없이 함수 하나가 된다. 클래스 자체를 값으로 넘길 수 있으면 추상 팩터리와 팩터리 메서드가 단순해진다. 여기서 나오는 교훈은 패턴의 일부가 언어에 없는 기능을 메우는 우회로라는 것이다. 폴 그레이엄도 자기 프로그램에 패턴이 보이면 문제의 신호로 여긴다고 썼다. AspectJ로 23가지를 구현해 보니 17개에서 코드 수준의 의존이 사라졌다는 연구(Hannemann, Kiczales)도 같은 방향이다.

그래도 패턴은 낡지 않았다. 구현이 언어 기능 한 줄로 줄어도 문제와 이름은 남는다. Java 람다로 짠 정렬 기준도 여전히 전략이고, 팀이 "그건 전략으로 빼자"고 말할 수 있는 어휘가 GoF의 가장 큰 유산이다. 언어가 바뀌면 구현은 바뀌고, 문제를 보는 눈은 남는다.

안티패턴: 흔하지만 해로운 해법

안티패턴(anti-pattern)이라는 말은 앤드루 코닉이 1995년 『Journal of Object-Oriented Programming』에 쓴 글 「Patterns and Antipatterns」에서 처음 썼고, GoF 책에서 영감을 받은 것이다. 안티패턴은 그냥 나쁜 습관과 두 가지로 구분된다. 흔히 쓰이고 처음에는 알맞아 보이지만 손해가 이득보다 크고, 효과가 검증된 다른 해법이 문서로 있다는 것이다. 1998년 책 『AntiPatterns』(Brown, Malveau, McCormick, Mowbray)가 이 생각을 아키텍처와 프로젝트 관리까지 넓혀 퍼뜨렸다.

안티패턴 세 가지. 갓 오브젝트는 가운데 큰 클래스 하나가 결제, 메일, 재고, 화면, 보고서, DB를 모두 붙잡고 있다. 스파게티 코드는 다섯 지점 사이를 흐름이 엉켜 오간다. 골든 해머는 늘 쓰는 도구 하나로 모양이 다른 세 문제를 모두 친다갓 오브젝트스파게티 코드골든 해머결제메일재고화면보고서DBManager메서드 300개한 클래스가 모든 것을 안다무엇을 고쳐도 이 파일을 연다12345흐름이 이리저리 뛴다어디서 오는지 알 수 없다늘 쓰는 도구검색캐시보고서모든 문제가 못으로 보인다문제 모양이 달라도 같은 해법
안티패턴 셋. 모든 것을 아는 클래스, 흐름이 엉킨 코드, 모든 문제를 같은 도구로 푸는 버릇이다.
  • 갓 오브젝트(god object): 서로 관계없는 메서드를 잔뜩 가지고 수많은 타입에 기대는, 지나치게 크고 힘센 객체다. 무엇을 고쳐도 이 클래스를 건드리게 된다. 책임별로 쪼개는 것이 처방이다.
  • 스파게티 코드: 제어 흐름이 구조 없이 이리저리 뛰는 코드다. goto를 마구 쓰던 시절의 이름이지만, 콜백과 이벤트가 얽힌 지금 코드에도 그대로 생긴다. 구조적 프로그래밍이 이것을 막으려고 나왔다.
  • 골든 해머(golden hammer): 익숙한 기술 하나를 모든 문제에 들이대는 것이다. 1966년 매슬로가 쓴 "가진 도구가 망치뿐이면 모든 것을 못처럼 다루고 싶어진다"는 말에서 왔다. 패턴을 막 배운 뒤의 패턴병도 이것의 한 갈래다.
  • 큰 진흙 공(big ball of mud): 알아볼 만한 구조가 없는 시스템이다. 마감, 사람의 교체, 오랜 땜질이 쌓여 생긴다.

공부하는 법

23가지를 차례로 외우면 금방 잊는다. 패턴은 겪어 본 문제에 붙을 때 남는다. 아래 고리를 패턴 하나씩 돈다.

패턴을 익히는 다섯 단계의 고리. 문제를 겪는다, 이름과 의도를 찾는다, 구조를 손으로 그린다, 작게 짜 본다, 언어 기능과 견준다를 돌아 다시 문제로 간다. 가운데에 한 번에 한 패턴씩이라고 적혀 있다문제를 겪는다이름과 의도를 찾는다구조를 손으로 그린다작게 짜 본다언어 기능과 견준다한 번에 한 패턴씩익숙한 라이브러리에서 다시 찾아본다외우는 것보다 "이 코드가 아픈 곳은 어디인가"를 먼저 말할 수 있는 것이 목표다
패턴 하나를 익히는 고리. 출발점은 늘 문제이고, 마지막에는 쓰는 언어가 그 패턴을 얼마나 흡수했는지 견준다.
  • 자주 쓰는 것부터: 전략, 옵저버, 데코레이터, 어댑터, 팩터리 메서드, 템플릿 메서드는 거의 모든 코드베이스에 있다. 인터프리터나 플라이웨이트는 필요한 날 찾아봐도 된다.
  • 이미 쓰는 라이브러리에서 찾는다: Java의 Comparator는 전략, Iterator는 이터레이터, 입출력 스트림을 겹겹이 감싸는 것은 데코레이터, 브라우저의 addEventListener는 옵저버다. 아는 코드에 이름을 붙이면 빨리 익는다.
  • 그림을 손으로 그린다: 역할(인터페이스, 구현, 클라이언트)과 화살표 방향을 직접 그려 보면, 어느 쪽이 어느 쪽을 모르는지가 보인다. 패턴의 핵심은 대개 그 "모름"이다.
  • 패턴 없이 먼저 짜 본다: 분기로 짠 코드에 기능을 하나 더해 보고, 같은 일을 패턴으로 다시 해 본다. 어느 쪽이 더 고치기 쉬운지 몸으로 느끼면 남용도 덜 한다.
  • 다른 언어로 짜 본다: 같은 패턴을 Java와 Python, Go로 짜 보면 무엇이 문제 자체이고 무엇이 언어의 빈칸을 메운 것인지 갈린다.

흔한 오해

  • "디자인 패턴은 GoF가 만들었다" — 패턴이라는 생각은 알렉산더의 건축에서 왔고, 소프트웨어로는 1987년 벡과 커닝엄이 먼저 옮겼다. GoF는 그것을 23가지로 정리해 널리 퍼뜨렸다.
  • "패턴은 복사해 쓰는 코드다" — 패턴은 해법의 뼈대와 이름이다. 같은 패턴도 언어와 상황에 따라 코드가 전혀 다르게 나온다.
  • "패턴을 많이 쓸수록 좋은 설계다" — 패턴마다 간접 층과 클래스가 는다. 바뀌지 않는 곳에 쓴 패턴은 비용만 남긴다.
  • "상속은 나쁘고 합성은 좋다" — GoF의 말은 "먼저 합성을 떠올려 보라"이지 상속 금지가 아니다. 진짜 "is-a" 관계이고 축이 하나뿐이면 상속이 더 간단하다.
  • "함수형 언어에는 패턴이 없다" — 일부 패턴이 언어 기능으로 줄어들 뿐, 문제는 남고 그 언어만의 패턴(고차 함수 조합, 모나드 등)이 새로 생긴다.
  • "안티패턴은 그냥 나쁜 코드다" — 안티패턴은 흔히 쓰이고 그럴듯해 보인다는 점, 그리고 검증된 대안이 있다는 점이 핵심이다. 그래서 이름을 붙여 알아보게 하는 것이다.

핵심 자료

요지는 직접 정리한 한 줄이다.

자료한 줄 요지
Alexander 외 — A Pattern Language (1977)건물과 동네를 짓는 253개 패턴. 패턴끼리 번호로 이어져 하나의 언어가 된다.
Alexander — The Timeless Way of Building (1979)패턴 언어의 철학 편. "이름 없는 특질"을 좇는다.
Beck, Cunningham — Using Pattern Languages for OO Programs (1987)패턴 언어를 프로그래밍에 처음 옮겨 본 짧은 보고. 창 설계 패턴 다섯 개.
Gamma, Helm, Johnson, Vlissides — Design Patterns (1994)23가지 패턴과 두 원칙. 지금도 패턴 이름의 표준 사전이다.
Norvig — Design Patterns in Dynamic Programming (1996)동적 언어에서는 23개 중 16개가 보이지 않거나 단순해진다는 발표.
Brown 외 — AntiPatterns (1998)흔하지만 해로운 해법과 그것을 고치는 길을 패턴 형식으로 모았다.
Wikipedia — Composition over inheritance합성의 장점과, 위임 코드를 손으로 써야 하는 비용까지 함께 정리했다.