숙련된 설계자와 초심자를 가르는 것은 문법 지식이 아니다. 초심자는 문제를 만나면 원리에서부터 해법을 새로 짜내려 애쓰지만, 숙련자는 어느 순간 “아, 이거 전에 봤던 그 모양이군” 하고 알아챈다. 그가 떠올리는 것은 특정 코드가 아니라 문제의 형태와 그것을 푸는 구조의 짝이다. 좋은 설계자의 머릿속에는 처음 보는 요구를 익숙한 골격으로 환원하는 목록이 들어 있고, 그 목록은 대개 말로 정리되어 있지 않다. 이 책은 그 암묵적 목록의 일부를 글로 옮긴 시도다.
패턴이라는 발상 자체는 소프트웨어에서 나오지 않았다. 건축가 크리스토퍼 알렉산더가 먼저 던졌다. 그는 도시와 건물을 관찰하며, 좋은 공간에는 반복해서 나타나는 구조가 있다는 것을 발견했다 — 이를테면 “사람이 머무는 방에는 두 방향에서 빛이 들어와야 한다” 같은. 알렉산더는 이런 반복 구조 하나하나를 패턴이라 부르며 이렇게 정의했다. 각 패턴은 우리 환경에서 되풀이되는 어떤 문제를 기술하고, 그 문제의 해결책의 핵심을 기술한다 — 그래서 같은 해법을 두 번 다시 똑같이 짜지 않고도 백만 번 재사용할 수 있다. GoF는 이 문장을 건물이 아니라 객체에 옮겨 적었을 뿐이다.
패턴은 맥락 속 반복되는 문제-해법의 쌍이다
여기서 세 낱말을 붙잡아야 한다 — 맥락, 반복, 쌍이다.
먼저 쌍이다. 패턴은 해법만도, 문제만도 아니다. “이 문제가 있을 때 → 이 구조로 풀면 → 이런 결과를 얻는다”는 삼단 결합이다. 해법만 떼어 외우면 그것은 관용구가 아니라 미신이 된다. 어떤 문제 앞에서 왜 이 구조가 답이 되는지를 함께 쥐고 있어야 패턴이다.
다음은 맥락이다. 같은 해법도 놓인 자리가 다르면 약이 되기도 독이 되기도 한다. 싱글턴은 인스턴스가 정말 하나뿐이어야 하는 자원 앞에서는 정답에 가깝지만, 그저 전역에서 편하게 꺼내 쓰고 싶다는 이유로 끌어오면 테스트를 못 하게 만드는 전역 상태 덩어리가 된다. 그래서 모든 패턴 기술에는 “언제 쓰는가(Applicability)“라는 항목이 붙는다. 맥락을 지운 패턴은 반쪽이다.
마지막이 반복이다. 패턴은 누가 발명한 것이 아니라 발굴한 것이다. 수많은 설계자가 서로 다른 프로젝트에서 같은 문제에 부딪혔고, 각자 비슷한 구조로 그것을 풀었으며, 그 구조가 살아남았다. GoF의 23개는 저자 넷이 “이렇게 짜면 좋겠다”고 제안한 것이 아니라 현장에서 이미 수십 번 검증된 것을 이름 붙여 모은 것이다. 그래서 패턴은 유행이 아니라 누적된 경험의 압축이다.
판단 기준: 어떤 설계 아이디어를 패턴이라 부르려면, 서로 다른 두세 시스템에서 실제로 되풀이해 나타났는지 물어라 — 한 번 잘 통한 트릭은 아직 패턴이 아니다. 함정: 멋져 보이는 구조를 발견하면 곧장 “새 패턴”이라 이름 붙이고 싶어진다. 반복으로 검증되지 않은 구조에 이름을 다는 것은 개인 취향을 권위로 위장하는 일이다.
패턴을 기술하는 네 요소 — 이름·문제·해법·결과
패턴을 사람에게 전달하려면 형식이 있어야 한다. GoF는 각 패턴을 촘촘한 항목들(의도·동기·구조·참여자·협력·결과·구현·샘플 코드·활용 사례…)로 기술하지만, 그 뼈대는 네 가지로 압축된다.
이름. 사소해 보이지만 가장 강력한 요소다. “감시자”라는 한 단어가 “한 객체가 바뀌면 그것에 의존하는 여러 객체가 자동으로 통지받아 갱신되는 구조”라는 문장 전체를 대신한다. 이름은 설계 어휘가 된다. 팀이 “여기 전략으로 빼자”라고 말할 수 있게 되는 순간, 화이트보드의 논의는 구현 세부가 아니라 설계 수준에서 오간다. 어휘가 사고의 해상도를 올린다.
문제. 이 패턴이 언제 적용되는가. 어떤 코드가 아플 때 이 구조가 처방이 되는가. 문제를 지운 채 해법만 알면, 멀쩡한 코드에 패턴을 억지로 이식하는 과용으로 흐른다.
해법. 문제를 푸는 구조 — 어떤 요소들이 있고, 누가 무엇을 책임지며, 어떻게 협력하는가. 중요한 것은 이것이 특정 코드가 아니라 템플릿이라는 점이다. 같은 패턴이라도 언어와 상황에 따라 살은 다르게 붙지만 뼈대는 유지된다.
결과. 이 책의 심장이다. 패턴을 적용하면 무엇을 얻고 무엇을 내주는가. 유연성을 얻는 대가로 대개 간접 계층이 늘고, 클래스 수가 불어나고, 흐름이 여러 객체로 흩어져 한눈에 안 들어오게 된다. 결과를 보지 않고 패턴을 고르는 것은 가격표를 안 보고 물건을 집는 것과 같다. 이 서재의 모든 노트가 결과 절을 생략하지 않는 이유다.
판단 기준: 패턴을 하나 익혔다고 말하려면 네 요소를 모두 댈 수 있어야 한다 — 특히 “이걸 쓰면 무엇이 나빠지는가”에 답할 수 없다면 아직 절반만 안 것이다. 함정: 대부분의 튜토리얼은 이름과 해법(클래스 다이어그램)만 가르치고 문제와 결과를 건너뛴다. 그렇게 배우면 패턴을 “쓸 줄”은 알지만 “쓸지 말지”는 판단하지 못한다.
카탈로그의 짜임 — 생성·구조·행위
GoF는 23개 패턴을 두 축으로 분류한다. 하나는 목적(무슨 일을 하는가), 하나는 범위(클래스에 적용되는가, 객체에 적용되는가)다. 이 서재는 주로 목적 축을 따라 세 갈래로 걷는다.
생성 패턴(Creational) 은 객체를 어떻게 만드느냐를 다룬다. new로 구체 클래스를 직접 부르면 그 코드는 무엇을 만드는지에 못 박힌다. 생성 패턴은 그 결정을 다른 곳으로 미루거나 감춰서, 무엇이 생성되는지를 유연하게 바꾸게 한다 — 추상 팩토리, 빌더, 팩토리 메서드, 프로토타입, 싱글턴이 여기 속한다.
구조 패턴(Structural) 은 객체와 클래스를 조립해 더 큰 구조를 만드는 방법을 다룬다. 맞지 않는 것을 이어 붙이고(어댑터), 두 축을 분리하고(브리지), 부분과 전체를 같게 다루고(컴포지트), 기능을 감싸 덧붙이는(데코레이터) 식이다. 대부분 상속이 아니라 합성 — 객체가 다른 객체를 품는 방식 — 으로 구조를 짠다.
행위 패턴(Behavioral) 은 객체들이 책임을 어떻게 나누고 어떻게 소통하느냐를 다룬다. 알고리즘을 갈아 끼우고(전략), 상태에 따라 행동을 바꾸고(상태), 요청을 객체로 캡슐화하고(커맨드), 변화를 자동으로 퍼뜨리는(감시자) 식으로, 실행 시점의 협력 흐름을 설계한다. 가장 수가 많고 가장 미묘하다.
이 분류는 서랍 정리 이상의 의미가 있다. 새 문제를 만났을 때 “이건 생성의 문제인가, 구조의 문제인가, 협력의 문제인가”를 먼저 물으면 후보군이 좁혀진다. 다만 경계는 칼로 자른 듯 선명하지 않다 — 인터프리터는 행위로 분류되지만 컴포지트(구조)와 한 몸처럼 쓰이고, 많은 패턴이 서로 협력하며 등장한다. 다음 편의 문서 편집기 사례가 그 협력을 통째로 보여 줄 것이다.
판단 기준: 문제를 분류 축에 먼저 얹어라 — “무엇을 만들지가 문제인가(생성), 어떻게 엮을지가 문제인가(구조), 누가 무엇을 언제 할지가 문제인가(행위).” 후보가 셋에서 몇 개로 줄어든다. 함정: 분류를 절대적 칸막이로 오해하면 안 된다. 패턴은 홀로 서지 않고 조합으로 쓰이며, 하나의 실제 설계에는 세 갈래가 동시에 등장한다.
이 책을 관통하는 두 원칙
23개 패턴은 제각각으로 보이지만, GoF는 그 대부분이 두 가지 원리 위에 서 있다고 못 박는다. 이 둘이 카탈로그 전체를 꿰는 실이다.
클래스 상속보다 객체 합성을
기능을 재사용하는 길은 둘이다. 상속(is-a)으로 부모의 코드를 물려받거나, 합성(has-a)으로 다른 객체를 품어 그 기능에 위임하거나.
상속은 편하다. extends 한 줄로 부모의 모든 것을 얻는다. 그러나 그 대가가 만만치 않다. 상속은 컴파일 시점에 고정되고 — 실행 중에 부모를 갈아 끼울 수 없다 — 부모의 내부 구현이 자식에게 노출되어, 부모가 바뀌면 자식이 함께 깨진다(GoF는 이것을 “상속은 캡슐화를 깨뜨린다”고 표현한다). 무엇보다 조합이 폭발한다. 커피에 우유·시럽·휘핑을 상속으로 조합하려 들면, MilkCoffee, MilkSyrupCoffee, MilkSyrupWhipCoffee… 경우의 수만큼 클래스가 늘어난다.
합성은 다르다. 필요한 기능을 가진 객체를 인터페이스로 품고, 그 객체에게 일을 넘긴다. 조합은 실행 중에 바뀔 수 있고, 품은 객체의 내부는 인터페이스 뒤에 가려 결합이 얕다.
// 상속 — 조합마다 클래스가 필요하다. 실행 중 바꿀 수 없다.
class MilkSyrupCoffee extends Coffee {
double cost() { return super.cost() + 300 + 200; }
}
// 합성 — 감싸는 객체를 실행 중에 쌓는다. 조합이 폭발하지 않는다.
interface Beverage { double cost(); }
class Coffee implements Beverage {
public double cost() { return 4000; }
}
class Milk implements Beverage {
private final Beverage base; // 다른 음료를 "품는다"
Milk(Beverage base) { this.base = base; }
public double cost() { return base.cost() + 300; }
}
// new Milk(new Syrup(new Coffee())) — 런타임에 원하는 만큼 조합한다.두 코드는 같은 값을 낸다. 그러나 상속 버전은 새 조합이 필요할 때마다 클래스를 추가해야 하고, 합성 버전은 이미 있는 부품을 실행 중에 다른 순서로 쌓기만 하면 된다. 이것이 데코레이터 패턴의 씨앗이고, 브리지·전략·컴포지트가 모두 같은 뿌리에서 자란다. “상속보다 합성”은 금욕적 계율이 아니라, 유연성이 필요한 축을 상속의 고정성에서 합성의 가변성으로 옮기라는 실용적 지침이다.
판단 기준: 물려받으려는 것이 “정체성(진짜 그 종류인가)“이면 상속이 맞고, “기능(그 일을 할 수 있으면 됨)“이면 합성이 맞다. 실행 중에 바뀌어야 하거나 여러 개를 조합해야 하면 거의 언제나 합성이다. 함정: “합성이 항상 옳다”로 뒤집으면 그것도 교조다 — 안정된 is-a 관계를 굳이 합성으로 풀면 위임 코드만 늘고 얻는 유연성은 쓰이지 않는다. 변할 축에만 합성을 놓아라.
구현이 아니라 인터페이스에 맞춰 프로그래밍하라
두 번째 원칙은 첫 번째를 떠받친다. 객체를 품거나 참조할 때, 그 대상을 구체 클래스가 아니라 인터페이스(추상 타입)로 붙잡으라는 것이다.
ArrayList<Order> orders = new ArrayList<>(); // 구현에 묶임
List<Order> orders = new ArrayList<>(); // 인터페이스에 프로그래밍차이는 미묘해 보이지만 파급이 다르다. 첫 줄은 orders를 다루는 모든 코드가 ArrayList라는 구체 구현에 매인다. 나중에 LinkedList가 나아 보여도 갈아 끼우기 어렵다. 둘째 줄은 List라는 계약에만 의존하므로, 뒤의 구현체는 무엇이든 될 수 있다. 클라이언트는 “무엇을 할 수 있는가(인터페이스)“만 알고 “어떻게 하는가(구현)“는 모른다 — 그래서 구현이 바뀌어도 클라이언트가 흔들리지 않는다.
이 원칙이 있어야 앞의 합성이 힘을 얻는다. 품은 객체를 인터페이스로 붙잡아야 실행 중에 다른 구현으로 갈아 끼울 수 있기 때문이다. 전략 패턴이 알고리즘을 교체하고, 추상 팩토리가 제품군을 바꾸고, 상태 패턴이 상태를 갈아 끼우는 일은 모두 “클라이언트가 인터페이스에만 의존한다”는 전제 위에서만 성립한다. 두 원칙은 사실 하나의 원리의 앞뒷면이다 — 변할 것을 인터페이스 뒤에 숨기고, 그 인터페이스를 합성으로 품어라.
판단 기준: 변수·매개변수·반환 타입을 선언할 때 가능한 한 넓은(추상적인) 타입을 골라라. 구체 클래스를 이름으로 부르는 곳은 오직 객체를 생성하는 지점 하나로 몰아라 — 생성 패턴이 존재하는 이유가 바로 그 지점마저 감추기 위해서다. 함정: 인터페이스를 남발해 구현이 하나뿐인 껍데기 계약을 곳곳에 만드는 것은 유연성이 아니라 소음이다. 실제로 갈릴 여지가 있는 축에만 추상화를 세워라 — 오지 않을 교체를 위한 인터페이스는 간접성이라는 비용만 남긴다.
패턴은 목적지가 아니라 어휘다
마지막으로 이 책 전체를 읽는 태도를 못 박아 둔다. 패턴은 도달해야 할 목표가 아니다. “이 코드에 패턴을 몇 개나 넣었는가”는 좋은 질문이 아니다. 패턴은 문제를 만났을 때 꺼내 쓰는 어휘이자 도구이며, 모든 도구가 그렇듯 맞는 자리에서만 값을 한다.
그래서 각 패턴 노트는 언제나 두 방향으로 열려 있다. 한쪽에는 “이 문제라면 이 구조가 처방이다”라는 적용의 문이 있고, 다른 쪽에는 “그러나 이럴 때 이 패턴을 끌어오면 대가만 치른다”는 과용의 경고가 있다. 패턴의 가장 흔한 실패는 패턴을 몰라서가 아니라 패턴을 위한 패턴 — 단순한 문제에 화려한 구조를 얹어 놓고 유연성이라 부르는 것 — 에서 온다.
판단 기준: 패턴을 도입하기 전에 “이 유연성이 실제로 필요한가, 어떤 변경이 예상되어 이걸 미리 심는가”를 답할 수 있어야 한다. 답이 “혹시 몰라서”라면 아직 심을 때가 아니다. 함정: 배운 패턴은 도처에서 그 패턴의 문제를 보게 만든다(망치를 든 사람에게는 모든 것이 못으로 보인다). 새 패턴을 익힌 직후일수록 과용을 경계하라 — 패턴을 넣지 않는 판단도 설계다.
이제 준비가 끝났다. 다음 편에서는 문서 편집기 하나를 실제로 지어 보며, 여덟 개의 패턴이 홀로가 아니라 서로 협력하며 하나의 설계를 이루는 모습을 본다. 카탈로그의 낱개가 아니라 살아 움직이는 조합으로서의 패턴을, 거기서 처음 만난다.
다음으로 문서편집기