스물세 개를 걸어왔다. 객체를 어떻게 만들지(생성), 어떻게 엮을지(구조), 누가 무엇을 언제 할지(행위)를 한 편씩 뜯어보며, 매번 같은 질문을 던졌다 — 이 구조는 무엇을 얻으려고 무엇을 내주는가. 이제 카탈로그를 덮을 자리다. 그런데 마지막 편에서 패턴을 하나 더 소개할 수는 없다. 대신 물어야 할 것은 이것이다. 이 여정이 실제로 남긴 것은 무엇인가. 스물세 개의 클래스 다이어그램을 외운 것이 아니라면, 우리가 손에 쥔 것은 대체 무엇인가.

서론에서 숙련자와 초심자를 가르는 것은 문법이 아니라 “문제의 형태와 그것을 푸는 구조의 짝”을 알아보는 눈이라고 했다. 결론에서 확인할 것은 그 눈이 실제로 조금 밝아졌는가다. 그리고 밝아진 눈이 빠지기 쉬운 함정 — 모든 것이 못으로 보이는 병 — 을 어떻게 경계할 것인가다.

패턴이 실제로 준 세 가지

패턴을 다 익혔다고 코드가 저절로 좋아지지는 않는다. 패턴이 준 것은 코드가 아니라 사고의 도구 셋이다.

첫째는 설계 어휘다. 이것이 가장 크다. “한 객체가 바뀌면 그것에 의존하는 여러 객체가 자동으로 통지받아 갱신되게 하자”를 팀이 감시자 한 단어로 말할 수 있게 되는 순간, 화이트보드의 논의는 구현 세부에서 설계 수준으로 올라간다. “여기 전략으로 빼자”, “이 부분은 퍼사드 뒤로 감추자”, “어댑터 하나 끼우면 돼”— 이 짧은 문장들이 각각 한 문단의 설계 의도를 실어 나른다. 어휘가 없으면 매번 구조를 처음부터 말로 풀어야 하고, 그 과정에서 서로 다른 그림을 머릿속에 그린 채 합의했다고 착각한다. 이름은 사소해 보이지만, 공유된 이름은 사고의 해상도를 올린다.

둘째는 재사용 가능한 해법의 목록이다. 처음 보는 요구를 만났을 때, 원리에서부터 해법을 새로 짜내는 대신 “이거 컴포지트 모양이군”, “이건 상태 기계로 접겠는데” 하고 익숙한 골격으로 환원하는 능력이다. 서론에서 말한 “암묵적 목록”이 이제 스물세 칸 채워졌다. 새 문제를 분류 축에 얹으면(생성인가·구조인가·행위인가) 후보가 좁혀지고, 각 후보가 어떤 대가를 요구하는지도 함께 떠오른다. 매번 바닥부터 발명하지 않아도 된다는 것 — 이것이 알렉산더가 “같은 해법을 두 번 다시 똑같이 짜지 않고도 재사용한다”고 말한 바로 그것이다.

셋째, 그리고 이 서재가 가장 붙들고 싶었던 것은 트레이드오프를 보는 눈이다. 모든 패턴 노트가 “결과” 절을 생략하지 않은 이유가 여기 있다. 패턴을 익힌다는 것은 “이 구조를 쓸 줄 안다”가 아니라 “이 구조가 무엇을 얻고 무엇을 내주는지 값을 매길 줄 안다”는 뜻이다. 유연성을 얻으면 대개 간접 계층이 늘고, 클래스 수가 불어나고, 흐름이 여러 객체로 흩어져 한눈에 안 들어오게 된다. 이 대가를 계산할 줄 아는 사람만이 “지금 이 자리에서 그 유연성이 값을 하는가”를 판단할 수 있다. 패턴을 몰라서 나쁜 설계를 하는 경우보다, 패턴을 알아서 과용하는 경우가 현장에는 더 많다.

판단 기준: 패턴을 하나 안다고 말하려면 다이어그램이 아니라 “이걸 쓰면 무엇이 나빠지는가”를 댈 수 있어야 한다. 대가를 못 대면 아직 절반만 안 것이다. 함정: 패턴 개수를 지식의 척도로 삼는 것. 스물세 개를 다 외워도 트레이드오프를 못 보면, 그것은 어휘가 아니라 망치 스물세 자루를 든 것이다 — 모든 문제가 못으로 보이기 시작한다.

두 원칙 — 카탈로그를 꿴 실

서론에서 세운 두 원칙을 이제 스물세 편의 경험 위에서 다시 본다. GoF는 대부분의 패턴이 두 원리 위에 서 있다고 못 박았고, 걸어오는 내내 그 말이 사실임을 확인했다.

클래스 상속보다 객체 합성을

데코레이터가 이 원칙의 가장 순수한 얼굴이었다. 커피에 우유·시럽·휘핑을 상속으로 조합하면 MilkSyrupWhipCoffee처럼 경우의 수만큼 클래스가 폭발하지만, 감싸는 객체를 런타임에 쌓으면 부품 몇 개로 모든 조합을 만든다. 같은 뿌리에서 브리지는 추상과 구현이라는 두 축을 각각 상속으로 늘리다 곱셈으로 터지는 것을, 한 축을 합성으로 품어 덧셈으로 눌렀다. 전략은 알고리즘을, 상태는 상태를, 추상 팩토리는 제품군을 — 모두 “변하는 것을 상속의 고정성에서 합성의 가변성으로 옮긴다”는 하나의 동작을 반복했다.

핵심은 상속이 컴파일 시점에 고정된다는 것, 그리고 부모의 내부가 자식에게 노출되어 상속이 캡슐화를 깨뜨린다는 것이었다. 합성은 조합을 실행 중에 바꿀 수 있고, 품은 객체를 인터페이스 뒤에 숨겨 결합을 얕게 유지한다. 다만 이것은 계율이 아니다. 물려받으려는 것이 정체성(진짜 그 종류)이면 상속이 맞고, 기능(그 일을 할 수 있으면 됨)이면 합성이 맞다. 템플릿메서드가 상속으로 유연성을 사는 정당한 예였다 — 알고리즘 골격은 고정하고 일부 훅만 서브클래스가 채우는, 안정된 축의 상속.

구현이 아니라 인터페이스에 맞춰 프로그래밍하라

두 번째 원칙은 첫 번째를 떠받친다. 품은 객체를 구체 클래스가 아니라 인터페이스로 붙잡아야, 실행 중에 다른 구현으로 갈아 끼울 수 있기 때문이다. 전략이 알고리즘을 교체하고, 감시자가 어떤 구독자든 통지하고, 반복자가 내부 구조를 숨긴 채 순회하는 일은 모두 “클라이언트가 인터페이스에만 의존한다”는 전제 위에서만 성립했다. 클라이언트는 “무엇을 할 수 있는가”만 알고 “어떻게 하는가”는 모른다 — 그래서 구현이 바뀌어도 흔들리지 않는다.

두 원칙은 사실 한 원리의 앞뒷면이다. 변할 것을 인터페이스 뒤에 숨기고, 그 인터페이스를 합성으로 품어라. 스물세 패턴은 이 한 문장을 각기 다른 문제 위에서 변주한 것에 가깝다. 무엇을 만들지가 변하면 생성 패턴이 생성 지점을 인터페이스 뒤로 숨겼고, 어떻게 엮을지가 변하면 구조 패턴이 조립을 합성으로 풀었고, 누가 무엇을 할지가 변하면 행위 패턴이 협력을 인터페이스 너머로 밀어냈다.

판단 기준: 새 설계 앞에서 “이 시스템에서 무엇이 변할 축인가”를 먼저 그린 뒤, 그 축만 인터페이스 뒤에 숨기고 합성으로 품는다. 두 원칙은 변할 축에만 적용하는 것이다. 함정: “합성이 항상 옳다”, “인터페이스는 많을수록 좋다”로 뒤집으면 그것도 교조다. 안정된 is-a를 합성으로 풀면 위임 코드만 늘고, 구현이 하나뿐인 껍데기 계약을 곳곳에 두면 유연성이 아니라 소음이다. 오지 않을 교체를 위한 추상화는 간접성이라는 비용만 남긴다.

패턴의 그림자 — 남용이라는 실패

이제 정직해질 자리다. 패턴을 배운 사람이 가장 자주 저지르는 실수는 패턴을 몰라서가 아니라 패턴을 위한 패턴에서 온다. 새 도구를 손에 쥔 직후일수록 세상 모든 것이 그 도구의 문제로 보인다.

첫 번째 남용은 불필요한 유연성이다. 방법이 지금 하나뿐이고 앞으로 늘 일도 없는데 “확장성”을 위해 미리 전략 인터페이스를 판다. 상속 관계가 명백히 안정적인데 “결합을 줄인다”며 브리지로 두 축을 가른다. 이것은 오지 않을 변경에 거는 보험이고, 보험료는 매일 코드를 읽는 사람이 낸다. 서론에서 말했듯 “혹시 몰라서”가 도입 이유라면 아직 심을 때가 아니다.

두 번째 남용은 간접성 과잉이다. 패턴은 대부분 문제를 한 겹의 간접 계층으로 감싸 푼다 — 인터페이스 하나, 위임 한 단계. 문제는 이 겹이 쌓일 때다. 팩토리가 만든 전략을 데코레이터가 감싸 프록시 뒤에 두면, 실제로 한 줄의 계산이 일어나기까지 다섯 개 파일과 네 번의 위임을 지나야 한다. 각 패턴은 저마다 정당했을지 몰라도, 합쳐진 코드를 디버깅하는 사람은 스택 트레이스를 타고 미로를 헤맨다. 유연성의 이름으로 추가한 간접성이 오히려 이해와 변경을 가로막는 것 — 운영비용 렌즈로 보면 명백한 적자다. 퍼사드가 역설적으로 이 병의 처방이기도 하다. 필요해서 생긴 복잡성이라면 통합 창구 뒤로 감춰 최소한 입구는 단순하게 만든다.

세 번째 남용은 분기를 옮겨 놓고 없앴다고 믿는 것이다. 전략 편에서 짚었듯, switch를 세 클래스로 흩뜨려도 “어떤 전략을 고를까”라는 판단 자체는 사라지지 않는다. 그 선택이 다시 거대한 if (type) new RateDiscount() else ...가 되어 있다면, 분기를 없앤 게 아니라 한 칸 옆으로 민 것이다. 책임연쇄중재자도 같은 함정을 판다 — 결합을 한곳에서 끊었는데 다른 곳에서 되살아났는지 늘 확인해야 한다. 패턴은 복잡성을 없애지 않는다. 복잡성이 놓일 자리를 바꿀 뿐이고, 그 자리가 정말 더 나은지는 매번 따져야 한다.

판단 기준: 패턴을 도입하기 전에 “이 유연성이 실제로 필요한가, 어떤 변경이 예상되어 이걸 미리 심는가”에 구체적인 답을 낼 수 있어야 한다. 답이 “혹시 몰라서”나 “이게 더 객체지향적이라서”라면 손을 멈춘다. 함정: 리뷰에서 패턴 이름이 등장하는 것을 품질의 신호로 오해하는 것. “여기 옵서버 썼어요”는 자랑이 아니라 정당화가 필요한 결정이다 — 패턴을 넣지 않는 판단도 똑같이 설계다.

언제 패턴을 걷어낼 것인가

도입만큼 중요한 것이 제거다. 코드는 살아가며 변하고, 어제 정당했던 패턴이 오늘은 순수한 부채가 되기도 한다. 예상했던 변경이 3년째 오지 않는다면, 그 축을 위해 세운 추상화는 유연성이 아니라 놀고 있는 자산이다. 전략이 하나의 구현체만 가진 채 굳어 있다면, 인터페이스를 걷고 구현을 인라인하는 것이 정직한 리팩터링이다.

이 판단은 서론에서 조영호로부터 빌려 온 태도와 정확히 같다 — 설계는 “예상되는 변경”의 축을 골라 유연성을 배치하는 선택이고, 그 예측이 틀렸다면 배치를 되돌리는 것도 설계다. 패턴을 넣는 방향으로만 리팩터링이 흐른다고 믿으면 안 된다. 안 쓰이는 패턴을 걷어내 코드를 다시 단순하게 만드는 것도, 똑같이 훈련된 눈이 하는 일이다.

판단 기준: 도입 후 일정 기간이 지나도 인터페이스의 구현체가 하나뿐이고 교체·확장이 실제로 일어나지 않았다면, 그 추상화를 걷어낼 후보로 올린다. 함정: 한 번 세운 구조를 신성시하는 것. “이미 이렇게 되어 있으니”는 유지의 이유가 못 된다 — 지금 이 코드가 그 유연성을 쓰고 있는지만 묻는다.

패턴을 넘어서 — 이어지는 길

GoF의 카탈로그는 종착지가 아니라 출발점이다. 이 책이 연 문 너머로 세 갈래 길이 뻗는다.

하나는 리팩터링이다. 마틴 파울러는 나쁜 냄새(코드 스멜)를 맡고 그것을 작은 단계로 고쳐 나가는 기술을 정리하면서, 그 목적지의 상당수가 GoF 패턴임을 보였다. 거대한 switch의 냄새는 전략이나 상태로, 중복된 조건 분기는 팩토리메서드로, 깊은 상속 계층은 브리지데코레이터로 흘러간다. 패턴이 “어떤 구조가 좋은가”의 목록이라면, 리팩터링은 “지금 코드에서 그 구조로 어떻게 안전하게 이동하는가”의 절차다. 둘은 목적지와 경로의 관계다 — 패턴만 알면 갈 곳은 아는데 가는 법을 모르고, 리팩터링만 알면 움직일 줄은 아는데 어디로 갈지를 모른다.

둘은 안티패턴이다. 패턴이 “이렇게 하면 대개 좋다”의 목록이라면, 안티패턴은 “이렇게 하면 대개 나쁘다”의 목록이다. 그리고 그 목록의 상당 부분이 바로 패턴의 남용에서 나온다 — 모든 클래스를 싱글턴으로 만드는 “싱글턴 남용”, 아무 이득 없이 위임만 하는 껍데기를 쌓는 “포이즌드 어니언”(간접성 과잉), 유연성의 이름으로 아무도 안 쓰는 확장점을 곳곳에 파는 과잉 설계. 안티패턴을 배우는 것은 패턴을 배우는 것의 그림자 면이다. 무엇이 좋은지와 무엇이 나쁜지를 함께 알아야 판단이 완성된다.

셋은 언어의 진화다. GoF가 책을 쓴 1994년의 C++와 스몰토크에는 일급 함수가 없었다. 그래서 “알고리즘 하나를 값처럼 주고받는다”는 발상을 실현하려면 인터페이스 하나와 클래스 여럿이라는 무거운 장치가 필요했다 — 그것이 전략이고 커맨드였다. 그러나 오늘의 자바·코틀린·자바스크립트에는 람다와 메서드 참조가 있다. Collections.sort(list, (a, b) -> a.age - b.age)에서 그 람다가 바로 전략이다. 별도의 Comparator 구현 클래스를 쓰지 않고도 알고리즘을 값으로 넘긴다.

// GoF 시대의 전략 — 인터페이스 하나 + 구현 클래스
button.setOnClick(new ClickHandler() {
    public void handle() { save(); }
});
 
// 오늘의 전략 — 함수가 곧 전략이다
button.setOnClick(() -> save());

커맨드도 마찬가지다. “요청을 객체로 캡슐화”하는 목적이 실행 취소나 로깅이 아니라 단지 “나중에 실행할 동작을 넘기는 것”뿐이라면, 이제는 Runnable 하나, 아니 람다 하나로 족하다. 이것은 패턴이 틀렸다는 뜻이 아니다. 패턴이 언어 기능으로 흡수되었다는 뜻이다. 한 시대의 디자인 패턴은 다음 시대의 언어 문법이 된다 — 반복자가 for-each로, 팩토리의 일부가 의존성 주입 컨테이너로 녹아든 것처럼. 그래서 패턴을 배우는 진짜 이득은 특정 구현을 외우는 데 있지 않다. 언어가 아직 문법으로 주지 않는 유연성을 손으로 지어 올릴 줄 아는 사고, 그리고 언어가 이미 준 것을 알아보고 무거운 장치를 걷어낼 줄 아는 눈 — 그 둘이 남는다.

판단 기준: 전략·커맨드처럼 상태 없이 메서드 하나뿐인 패턴을 만나면, 먼저 언어가 그것을 람다·함수 타입으로 이미 주지 않는지 확인한다. 준다면 클래스를 짓지 말고 함수를 넘긴다. 함정: “GoF에 나왔으니 클래스로 구현해야 한다”는 교조. 패턴은 구조의 의도이지 특정 문법이 아니다 — 같은 의도를 더 가벼운 문법으로 이룰 수 있으면 그것이 그 패턴의 오늘날 정답이다.

닫으며

서론을 이렇게 열었다 — 패턴은 도달해야 할 목표가 아니라 문제를 만났을 때 꺼내 쓰는 어휘이자 도구이며, 모든 도구가 그렇듯 맞는 자리에서만 값을 한다. 스물세 편을 걷고 난 지금, 그 문장을 조금 더 무겁게 다시 쓸 수 있다.

패턴을 익힌다는 것은 스물세 개의 해법을 손에 넣는 일이 아니라, 모든 설계 결정을 “무엇을 얻고 무엇을 내주는가”로 저울질하는 습관을 몸에 새기는 일이다. 그 저울이 있으면 새 패턴을 만나도, 언어가 진화해 옛 패턴을 흡수해도, 심지어 카탈로그에 없는 문제를 만나도 흔들리지 않는다. 얻는 것과 내주는 것을 세어 순이득을 판단하는 눈 — 이 서재가 스물세 번 반복해 “결과” 절을 쓴 이유가 오직 이 눈 하나를 남기기 위해서였다.

패턴을 넘어선다는 것은 패턴을 버린다는 뜻이 아니다. 패턴을 도구로 부리되 도구에 부림당하지 않는 것, 넣을 때와 걷을 때를 똑같은 냉정함으로 판단하는 것, 그리고 언젠가 이 스물세 개조차 언어의 문법 속으로 녹아 사라질 때에도 그 밑에 흐르던 두 원칙만은 알아보는 것 — 거기서부터가 패턴 다음의 설계다.