이 책의 마지막 장은 새로운 기법을 가르치지 않는다. 대신 지금까지 우리가 손으로 밟아 온 설계들이 사실은 이름 붙은 디자인 패턴이었음을 되짚는다. 패턴을 먼저 배우고 코드에 적용한 게 아니라, 좋은 설계를 향해 리팩터링하다 보니 자연히 패턴에 도달한 것이다. 이것이 이 책이 패턴을 마지막에 두는 이유다 — 패턴은 출발점이 아니라 도착점의 이름이다.

패턴은 설계의 재사용이다

디자인 패턴은 반복되는 설계 문제에 대한 검증된 해법이자, 그 해법을 부르는 공통 어휘다. 코드를 복사하는 게 아니라 구조를 재사용한다. “여기에 STRATEGY를 쓰자”는 한마디가 클래스 다이어그램 한 장을 대신한다.

패턴이란 무엇인가 — 3의 규칙

패턴이 “검증된 해법”이라는 말에는 중요한 조건이 숨어 있다. 한두 번 통한 설계는 패턴이 아니다. 패턴은 서로 다른 실무 맥락에서 반복적으로 검증된 설계여야 한다. 여기서 나온 경험칙이 이른바 **3의 규칙(Rule of Three)**이다 — 어떤 해법이 세 번의 실제 상황에서 효과를 입증하기 전까지는 그것을 패턴이라 부르지 않는다.

이 규칙이 강조하는 것은 패턴의 본질이 “발명”이 아니라 “발견”이라는 점이다. 누군가 책상에서 만들어 낸 우아한 구조가 아니라, 여러 현장이 각자 부딪히다 수렴한 형태에 이름을 붙인 것이 패턴이다. 그래서 패턴에는 그것이 통하는 **맥락(context)**과 해결하려는 문제(problem), 그리고 그 문제를 푸는 **해법(solution)**이 항상 함께 붙어 다닌다. 맥락을 떼어 낸 해법만 외우면 패턴을 오용하게 된다.

판단 기준: 어떤 구조를 패턴으로 대접하기 전에 “이것이 서로 다른 맥락에서 실제로 반복해 통했는가”를 묻는다. 함정: 한 번 잘 통한 구조에 성급히 이름을 붙여 “우리 팀 패턴”으로 강요하면, 맥락이 다른 곳에까지 억지로 적용된다.

패턴의 구성 요소와 어휘로서의 가치

잘 서술된 패턴은 네 부분을 갖춘다 — 이름(name), 그것이 통하는 맥락과 문제, 그 문제를 푸는 해법, 그리고 해법을 택했을 때 감수하는 결과(consequences). 이 중 초심자가 가장 자주 빠뜨리는 것이 마지막 “결과”다. 모든 패턴에는 대가가 있다 — 유연성을 얻는 대신 간접 층이 늘고, 조합 자유를 얻는 대신 추적이 어려워진다. 결과를 함께 읽지 않으면 이득만 보고 대가를 잊는다.

패턴이 주는 가장 실질적인 이득은 어쩌면 어휘다. “이 정책 객체를 런타임에 갈아 끼우고 싶으니 알고리즘을 별도 객체로 뽑자”는 긴 설명이 “STRATEGY로 가자” 한마디로 압축된다. 패턴 이름은 설계 대화의 대역폭을 극적으로 높인다 — 구조·의도·트레이드오프가 이름 하나에 실려 전달되기 때문이다. 이 어휘의 힘 때문에라도 패턴 이름은 팀이 공유하는 정확한 의미로 써야 하며, 아무 합성이나 “STRATEGY”라 부르면 어휘가 오염된다.

판단 기준: 패턴을 인용할 때 이름과 함께 그 결과(대가)까지 팀이 공유하는지 확인한다. 함정: 이름만 통용되고 결과가 공유되지 않으면, 대가를 모른 채 패턴을 남발하게 된다.

패턴의 분류 — 규모에 따라

패턴은 다루는 문제의 규모에 따라 층위가 나뉜다.

  • 아키텍처 패턴(architecture pattern): 시스템 전체의 구조를 결정한다. 계층(Layers), MVC, 파이프-필터처럼 서브시스템의 골격과 그들 사이의 관계를 규정하는 큰 패턴이다.
  • 디자인 패턴(design pattern): 특정 설계 문제를 푸는 중간 규모의 패턴이다. 이 책이 회수한 STRATEGY·TEMPLATE METHOD·DECORATOR가 여기 속하며, GoF의 23개 패턴이 대표적이다.
  • 이디엄(idiom): 특정 프로그래밍 언어에 종속된 가장 작은 패턴이다. 언어의 관용적 표현으로, 자바의 equals/hashCode 재정의 규약이나 C++의 RAII가 그 예다.

여기에 더해 **분석 패턴(analysis pattern)**도 있다. 이는 소프트웨어 구조가 아니라 특정 도메인(회계, 거래, 관측 등)의 개념 모델에서 반복되는 형태에 붙인 이름이다. 마틴 파울러가 정리한 이 패턴들은 “무엇을 만들 것인가”의 재사용이라는 점에서 설계 패턴과 층위가 다르다.

디자인 패턴 층위 안에서도 GoF는 목적에 따라 셋으로 나눈다. 생성(creational) 패턴은 객체를 만드는 방식을 캡슐화하고(FACTORY METHOD, ABSTRACT FACTORY 등), 구조(structural) 패턴은 객체들을 조합해 더 큰 구조를 만들며(이 장의 DECORATOR, 그리고 COMPOSITE·ADAPTER 등), 행위(behavioral) 패턴은 객체들이 책임을 나눠 협력하는 방식을 다룬다(이 장의 STRATEGY·TEMPLATE METHOD 등). 이 책이 회수한 세 패턴 중 둘이 행위 패턴이라는 점은 우연이 아니다 — 책 전체가 “책임과 협력”을 좇았으니, 그 여정이 자연히 행위 패턴에 가장 자주 닿았다.

판단 기준: 패턴을 고를 때 지금 다루는 문제가 시스템 골격인지·설계 세부인지·언어 관용인지·도메인 개념인지를 먼저 자리매김하고, 디자인 패턴이라면 생성·구조·행위 중 어느 목적인지까지 좁힌다. 함정: 규모가 다른 층위를 뒤섞어 논의하면, 아키텍처 결정에 이디엄을 들이대거나 그 반대의 엇박자가 난다.

패턴 만능주의를 경계하라

패턴을 배우면 모든 문제가 패턴으로 보이는 함정에 빠진다. 이것이 패턴 만능주의다. 위험한 것은 패턴이 목적이 되어 버릴 때다 — “여기에 DECORATOR를 쓸 수 있다”는 사실이 “여기에 DECORATOR를 써야 한다”로 미끄러진다.

패턴 적용의 유일한 정당성은 그것이 해결하는 문제에 있다. 합성을 쓰는 이유를 설명하지 못한다면 STRATEGY를 쓰지 마라. 유연성이 필요 없는 곳에 유연성을 위한 패턴을 넣으면, 얻는 것 없이 간접 층과 클래스 수만 늘어 오히려 이해가 어려워진다. 패턴은 복잡도를 옮기는 도구이지 없애는 도구가 아니므로, 옮겨서 이득인 자리에서만 써야 한다.

코드 냄새와 패턴 사이에도 강박이 끼어든다. 냄새를 발견하면 반사적으로 대응 패턴을 떠올리게 되지만, 냄새가 실제로 변경을 방해하고 있는지부터 확인해야 한다. 변하지 않을 코드의 중복이나 조건문은 냄새처럼 보여도 손대지 않는 편이 낫다. 패턴은 미래의 변경을 흡수하기 위한 투자이므로, 그 변경이 실제로 올 자리에만 투자한다.

단가가 하나뿐이고 앞으로도 바뀔 계획이 없는 시스템을 생각해 보자.

// 과잉 — 변형이 없는데 STRATEGY를 심었다. 전략 인터페이스, 구현체, 주입 배선이
// 이득 없이 늘고, 단가 하나를 이해하려 세 파일을 오가야 한다.
interface RateStrategy { Money rate(); }
class FlatRateStrategy implements RateStrategy {
    public Money rate() { return Money.of(18); }
}
class Phone {
    private RateStrategy strategy;   // 갈아 끼울 일이 영영 없다
}
 
// 충분 — 변형이 없으니 상수 하나면 된다. 나중에 진짜 변형이 오면 그때 뽑는다.
class Phone {
    private static final Money RATE = Money.of(18);
}

왼쪽은 “STRATEGY를 쓸 수 있다”에 이끌린 결과이고, 오른쪽은 “지금 변형이 없다”는 사실에 충실한 결과다. 유연성은 공짜가 아니므로, 요구되지 않은 유연성은 순수한 비용이다.

판단 기준: 패턴을 넣기 전에 “이 패턴이 흡수하려는 변경이 실제로 예상되는가, 그 유연성의 대가(간접성·클래스 증가)를 치를 값어치가 있는가”를 답한다. 함정: 패턴 이름을 아는 것이 설계 실력이라 착각하면, 문제가 요구하지 않은 유연성을 곳곳에 심어 시스템을 무겁게 만든다.

이 책의 예제들은 이미 세 가지 패턴을 지나왔다.

TEMPLATE METHOD — 10장. 상속으로 핸드폰 과금을 구현했을 때, 부모가 알고리즘의 뼈대를 고정하고 변하는 한 걸음만 자식에게 맡겼다.

public abstract class BasicRatePolicy {
    public Money calculateFee(Phone phone) {   // 불변의 뼈대 = 템플릿 메서드
        Money result = Money.ZERO;
        for (Call call : phone.getCalls())
            result = result.plus(calculateCallFee(call));
        return result;
    }
    protected abstract Money calculateCallFee(Call call);   // 변하는 한 걸음만 자식이
}

순회하며 합산하는 절차는 부모가 쥐고, calculateCallFeeRegularPolicy·NightlyDiscountPolicy가 다르게 채운다. 이것이 TEMPLATE METHOD다. 자세한 전개는 10장에 있다.

STRATEGY — 11장. 상속을 합성으로 바꾸자, 변하는 알고리즘이 RatePolicy라는 별도 객체가 되어 Phone에 주입됐다.

public class Phone {
    private RatePolicy ratePolicy;   // 알고리즘을 객체로 캡슐화해 갈아 끼운다
    public Money calculateFee() {
        return ratePolicy.calculateFee(this);
    }
}

TEMPLATE METHOD가 상속으로 묶었던 “변하는 부분”을, STRATEGY는 합성으로 분리해 런타임에 교체 가능하게 만든다. 같은 문제의 상속판과 합성판이 각각 두 패턴의 이름을 갖는 셈이다.

DECORATOR — 11장. 세금·추가 할인 같은 부가 정책은 기본 정책을 감싸며 책임을 덧입혔다.

public abstract class AdditionalRatePolicy implements RatePolicy {
    private RatePolicy next;   // 다른 정책을 감싼다
    public Money calculateFee(Phone phone) {
        return afterCalculated(next.calculateFee(phone));   // 감싼 결과에 책임을 덧댄다
    }
    protected abstract Money afterCalculated(Money fee);
}

TaxablePolicyRateDiscountablePolicy를 감싸고, 그것이 다시 RegularPolicy를 감싼다. 상속으로는 조합의 폭발을 감당할 수 없던 것을, DECORATOR는 객체를 겹겹이 쌓아 유연하게 조합한다. 합성으로의 전환과 데코레이터 구조는 11장에서 자세히 다뤘다.

세 패턴의 트레이드오프

같은 과금 문제를 세 패턴이 각각 다른 방식으로 풀었다. 셋을 나란히 놓으면 무엇을 언제 쓸지가 보인다.

TEMPLATE METHOD는 변하는 부분을 상속으로 분리한다. 알고리즘의 뼈대를 부모가 쥐고 자식은 훅 메서드만 채운다. 구조가 단순하고 뼈대를 강하게 고정할 수 있는 것이 장점이지만, 변형의 축이 상속으로 고정돼 컴파일 타임에 결정된다는 것이 한계다. 한 객체가 여러 변형을 동시에 갖거나 런타임에 바꿀 수 없고, 단일 상속 언어에서는 다른 상속 기회를 소진한다.

STRATEGY는 같은 “변하는 부분”을 합성으로 분리한다. 알고리즘을 별도 객체로 뽑아 주입하므로 런타임에 교체할 수 있고, 하나의 알고리즘을 여러 문맥이 공유할 수 있다. 대가는 객체 수와 주입 배선이 늘어난다는 점, 그리고 문맥과 전략이 데이터를 주고받는 인터페이스를 설계해야 한다는 점이다. TEMPLATE METHOD의 유연성 강화판이라 볼 수 있다.

DECORATOR는 변형이 아니라 책임의 누적을 다룬다. 객체를 다른 객체로 감싸 기능을 겹겹이 덧입히며, 조합을 런타임에 자유롭게 쌓을 수 있다. 세금 위에 할인, 그 위에 또 다른 부가 정책을 순서대로 얹는 식의 조합 폭발을 상속 없이 감당하는 것이 강점이다. 대신 감싸는 순서에 따라 결과가 달라질 수 있고, 겹겹의 래퍼 때문에 디버깅 시 실제 동작 주체를 추적하기 어렵다.

판단 기준: 변형이 하나의 축이고 컴파일 타임에 고정돼도 된다면 TEMPLATE METHOD, 런타임 교체나 공유가 필요하면 STRATEGY, 여러 부가 책임을 자유롭게 조합해 쌓아야 하면 DECORATOR를 고른다. 함정: 세 패턴은 표면이 비슷해 혼동되기 쉽다 — “무엇을 분리하는가”(알고리즘 변형이냐 책임 누적이냐)와 “언제 결정되는가”(컴파일 타임이냐 런타임이냐)를 기준으로 갈라야 한다.

프레임워크는 코드의 재사용이고, 제어가 역전된다

패턴이 설계를 재사용한다면, 프레임워크는 코드 자체를 재사용한다. 라이브러리와 프레임워크의 차이는 “누가 누구를 부르는가”에 있다.

  • 라이브러리: 내 코드가 라이브러리를 부른다. 흐름의 주도권은 내게 있다.
  • 프레임워크: 프레임워크가 내 코드를 부른다. 흐름의 주도권은 프레임워크에 있다.

이 뒤바뀜을 **제어의 역전(Inversion of Control)**이라 부른다. “우리를 부르지 마세요, 우리가 당신을 부르겠습니다(Hollywood Principle).” 프레임워크는 애플리케이션의 뼈대와 흐름을 고정해 두고, 변하는 지점(확장점)만 우리에게 구현하도록 남긴다. 사실 이것은 TEMPLATE METHOD를 애플리케이션 규모로 키운 것이다 — 프레임워크가 calculateFee 같은 불변의 흐름을 쥐고, 우리는 calculateCallFee 같은 훅만 채운다.

같은 정렬 기능을 두 방식으로 견주면 차이가 선명하다. 라이브러리에서는 우리가 Collections.sort(list, comparator)를 부른다 — 언제 정렬할지, 무엇을 정렬할지를 우리가 정하고 라이브러리는 시키는 대로 답한다. 프레임워크에서는 반대다. 우리가 Comparator만 등록해 두면, 정렬이 필요한 시점을 프레임워크가 판단해 우리 compare를 불러 준다 — 흐름의 시각과 순서는 프레임워크가 쥔다. 같은 기능이라도 “내가 언제 부르는가”와 “언제 불려 가는가”가 라이브러리와 프레임워크를 가른다.

판단 기준: 흐름의 주도권을 넘겨줄 만큼 문제 영역이 안정적으로 반복되면 프레임워크가 이득이다. 함정: 아직 흐름이 불안정한데 프레임워크부터 세우면, 고정돼야 할 뼈대가 계속 흔들려 오히려 확장점이 족쇄가 된다. 프레임워크도 유사 사례가 쌓인 뒤 추출하는 게 안전하다 — 14장의 교훈과 같다.

재사용의 단위는 상위 정책이다

라이브러리는 저수준 기능을 재사용한다 — 정렬, 문자열 처리, 날짜 계산. 프레임워크가 재사용하는 것은 그보다 값진 것, 바로 **상위 정책(high-level policy)**이다. “요금은 통화들을 순회하며 조건에 맞는 구간에 단가를 곱해 합산한다”는 흐름 자체 — 애플리케이션의 뼈대가 되는 정책 — 을 재사용한다.

이것이 프레임워크가 라이브러리보다 큰 이득을 주는 이유이자 동시에 더 위험한 이유다. 상위 정책은 시스템에서 가장 안정적이어야 할 부분이고, 그것이 재사용된다는 것은 여러 애플리케이션이 그 정책 위에 세워진다는 뜻이다. 정책이 안정적이면 그 위의 모든 것이 이득을 보지만, 정책이 흔들리면 그 위의 모든 것이 함께 흔들린다. 그래서 프레임워크로 굳힐 흐름은 반드시 충분히 검증된 것이어야 한다.

할리우드 원칙(Hollywood Principle) — “먼저 연락하지 마세요, 저희가 연락하겠습니다” — 은 이 관계를 압축한다. 상위 정책(프레임워크)이 하위의 구체적 구현(우리 코드)을 호출하지, 그 반대가 아니다. 의존성의 방향이 구체에서 추상으로 뒤집히고, 우리 코드는 프레임워크가 정한 확장점의 계약만 지키면 된다.

일관성 있는 협력이 프레임워크의 씨앗이다

14장에서 만든 과금 시스템의 협력 구조는 사실 이미 작은 프레임워크였다. FeeRule이 불변의 흐름을 쥐고, FeeCondition이라는 확장점을 우리에게 남겼다. 여기서 상위 정책을 별도 모듈로 떼어 내고 확장점을 공개하면, 이 과금 로직은 여러 통신사 애플리케이션이 공유하는 프레임워크로 승격된다.

// [프레임워크] 상위 정책 — 재사용의 단위. 흐름을 고정하고 확장점만 연다.
public final class BillingFramework {
    private final List<FeeRule> rules;
 
    public BillingFramework(List<FeeRule> rules) {
        this.rules = rules;
    }
 
    // 불변의 흐름: 모든 통화에 모든 규칙을 적용해 합산한다.
    // 이 흐름이 곧 재사용되는 상위 정책이다.
    public Money calculate(Phone phone) {
        return phone.getCalls().stream()
                .flatMap(call -> rules.stream().map(rule -> rule.calculateFee(call)))
                .reduce(Money.ZERO, Money::plus);
    }
}
 
// [애플리케이션] 확장점 구현 — 각 통신사가 채우는 부분.
// 프레임워크가 이 FeeCondition 을 "불러 준다" (할리우드 원칙).
public class HolidayFeeCondition implements FeeCondition {
    @Override
    public List<DateTimeInterval> findTimeIntervals(Call call) {
        return call.getInterval().splitByHolidays();   // 우리 도메인의 새 조건
    }
}
 
// 조립: 프레임워크에 우리 규칙을 꽂아 넣으면 끝.
var framework = new BillingFramework(List.of(
        new FeeRule(new HolidayFeeCondition(), holidayRate),
        new FeeRule(new TimeOfDayFeeCondition(), nightRate)));

승격 과정에서 새로 짠 코드는 거의 없다. BillingFramework가 흐름을 쥐고 우리는 FeeCondition만 구현하는데, 이는 14장에서 이미 하던 일 그대로다. 일관성 있는 협력을 잘 세워 두면, 프레임워크로의 전환은 “협력 구조를 모듈 경계로 감싸고 확장점을 공개하는” 마무리 작업으로 줄어든다. 좋은 협력은 자라서 프레임워크가 된다.

판단 기준: 협력 구조에서 불변의 흐름과 변하는 확장점이 이미 분리돼 있고 그 흐름이 여러 애플리케이션에 재사용될 만하다면, 프레임워크로 승격할 씨앗이 익은 것이다. 함정: 확장점이 충분히 안정되기 전에 모듈 경계로 봉인하면, 나중에 흐름을 고쳐야 할 때 프레임워크를 쓰는 모든 애플리케이션이 함께 깨진다.

절차에서 책임으로, 책임에서 협력으로

이 책 전체를 한 문장으로 되짚으면, 그것은 관점의 이동이었다.

  • 2장의 영화 예매는 객체지향답게 시작했다가, 4장에서 데이터 중심으로 일부러 망가졌다. 데이터를 먼저 정하고 절차가 그것을 주무르는 설계는, 변경이 여기저기로 번졌다.
  • 5장의 책임 주도 설계는 질문을 뒤집었다. “이 데이터로 무엇을 하지”가 아니라 “이 책임은 누가 져야 하지”. 데이터가 아니라 책임이 객체를 나누는 기준이 되자, 변경이 한 객체 안에 갇혔다.
  • 그리고 상속(10장)에서 합성(11장)으로, 다시 일관성 있는 협력(14장)으로 나아가며, 설계의 초점은 개별 객체에서 객체들 사이의 협력으로 옮겨 갔다.

절차 → 책임 → 협력. 좋은 객체지향 설계는 똑똑한 객체 하나를 만드는 일이 아니라, 적절한 책임을 나눠 가진 객체들이 매끄럽게 협력하게 만드는 일이었다. 디자인 패턴은 그 협력의 반복되는 좋은 형태에 붙인 이름이고, 프레임워크는 그 좋은 협력을 코드로 굳혀 재사용하는 것이다. 우리가 패턴을 향해 리팩터링한 게 아니라, 책임과 협력을 좇다 보니 패턴에 도착했다 — 이 책이 처음부터 끝까지 하려던 말이 바로 그것이다.

정리

  • 패턴은 설계(구조)의 재사용이자 공통 어휘다. 코드를 복사하지 않고 검증된 구조를 재사용한다.
  • 이 책의 예제들은 이미 패턴이었다 — 10장의 상속은 TEMPLATE METHOD, 11장의 합성은 STRATEGY와 DECORATOR.
  • 프레임워크는 코드의 재사용이며, 흐름의 주도권이 프레임워크로 넘어가는 제어의 역전이 핵심이다. TEMPLATE METHOD를 애플리케이션 규모로 키운 것이다.
  • 패턴도 프레임워크도 유사 사례가 쌓인 뒤 추출하는 게 안전하다. 선제적 적용은 과설계이자 족쇄다.
  • 책 전체의 여정은 절차 → 책임 → 협력이었다. 좋은 설계는 똑똑한 객체가 아니라 잘 나뉜 책임들의 매끄러운 협력이다.