Chapter 4

템플릿 메소드, 반복자/컴포지트, 상태 패턴

  • 4.1 커피와 홍차 만들기
  • 4.2 템플릿 메소드 패턴
  • 4.3 후크(Hook)로 유연성 확보
  • 4.4 할리우드 원칙과 실전 예시
  • 4.5 객체마을 식당 합병과 반복자 패턴
  • 4.6 Iterator 인터페이스와 단일 역할 원칙
  • 4.7 컴포지트 패턴으로 트리 구조 만들기
  • 4.8 뽑기 기계 만들기
  • 4.9 상태 패턴으로 리팩토링
  • 4.10 전략 패턴과의 차이

알고리즘의 뼈대를 잠그고, 컬렉션을 통일된 방식으로 순회하고, 조건문을 상태 객체로 바꾸는 것 -- 행동을 다루는 세 가지 패턴이야.

커피 만드는 법은 물 끓이기, 커피 우리기, 컵에 따르기, 설탕과 우유 추가야. 홍차는? 물 끓이기, 찻잎 우리기, 컵에 따르기, 레몬 추가. 놀라울 정도로 비슷하지? 물 끓이기와 컵에 따르기는 완전히 같고, "우리기"와 "첨가물 추가"는 음료에 따라 다를 뿐 개념적으로 같은 단계야. **템플릿 메소드 패턴(Template Method Pattern)**은 이 공통 알고리즘을 슈퍼클래스의 prepareRecipe()final로 잠가두고, 달라지는 brew()addCondiments()만 추상 메소드로 서브클래스에 맡기는 거야. 알고리즘의 골격(순서)은 슈퍼클래스가 통제하고, 서브클래스는 개별 단계만 채워넣어.

여기에 **후크(Hook)**를 추가하면 더 유연해져. customerWantsCondiments() 같은 기본 구현이 있는 메소드를 두면, 서브클래스가 원할 때만 오버라이드해서 알고리즘의 특정 단계를 조건부로 실행할 수 있지. 반드시 구현해야 하는 추상 메소드와 달리, 후크는 선택적이야. 이 구조 뒤에는 할리우드 원칙 — "먼저 연락하지 마세요, 저희가 연락드리겠습니다" — 이 있어. 고수준 컴포넌트(슈퍼클래스)가 저수준(서브클래스)을 호출하는 거지, 반대가 아니야. Java의 Arrays.sort()가 딱 이 패턴이야. 정렬 알고리즘 전체는 Arrays가 제어하고, 우리는 compareTo()라는 세부사항만 제공하면 돼. 프레임워크가 우리 코드를 호출하는 구조 — 이게 템플릿 메소드의 본질이야.

템플릿 메소드가 알고리즘의 골격을 다뤘다면, 이번엔 컬렉션 순회라는 일상적인 문제를 보자. 객체마을 식당과 팬케이크 하우스가 합병했는데, 한쪽은 ArrayList, 다른 쪽은 배열로 메뉴를 관리해. 웨이트리스가 두 메뉴를 출력하려면 거의 같은 순회 코드를 두 번 써야 하지. 메뉴가 3개, 4개로 늘어나면? **반복자 패턴(Iterator Pattern)**은 순회를 hasNext()next()를 가진 Iterator 인터페이스로 캡슐화해서, 웨이트리스가 ArrayList인지 배열인지 HashMap인지 전혀 모르고도 하나의 코드로 순회할 수 있게 만들어.

이 패턴 뒤에는 단일 역할 원칙(SRP) — "클래스를 바꾸는 이유는 한 가지뿐이어야 한다" — 이 있어. 컬렉션이 데이터 관리와 순회를 동시에 책임지면 두 가지 이유로 변경될 수 있거든. 순회를 Iterator에 맡기면 컬렉션은 자기 데이터 관리에만 집중할 수 있지. 여기서 한 걸음 더 나가면 **컴포지트 패턴(Composite Pattern)**이야. 메뉴 안에 서브메뉴가 들어가는 트리 구조에서, 메뉴(Composite)와 메뉴 항목(Leaf)을 MenuComponent라는 같은 타입으로 다루면, print() 한 번에 전체 트리가 재귀적으로 출력돼. Leaf에서 add()가 의미 없어지는 등 단일 역할 원칙을 일부 포기하지만, 클라이언트가 개별 객체와 복합 객체를 구분 없이 똑같이 다룰 수 있다는 투명성을 얻는 거야.

컬렉션 순회에 이어, 이번엔 조건문이 길어지는 문제를 다뤄보자. 뽑기 기계에는 동전 없음, 동전 있음, 알맹이 판매, 매진이라는 네 가지 상태가 있어. 처음에는 상태를 정수 상수로, 행동을 if-else로 구현하겠지. 작동은 하지만 "10번 중 1번 당첨" 기능을 추가하라고 하면? insertQuarter(), ejectQuarter(), turnCrank(), dispense() — 네 개 메소드의 모든 조건문을 다 수정해야 해. 빠뜨리기도 쉽고, OCP를 완전히 위반하지.

**상태 패턴(State Pattern)**은 각 상태를 별도의 클래스로 만들어. NoQuarterState, HasQuarterState, SoldState, SoldOutState — 각각 State 인터페이스를 구현하고, GumballMachine(Context)은 현재 State 객체에 동작을 위임해. insertQuarter()가 호출되면 state.insertQuarter()로 넘기기만 하면 돼. 각 상태 클래스가 자기 상태에서의 행동만 책임지고, 상태 전환도 자기가 결정하는 거야. "10번 중 1번 당첨"을 추가하려면? WinnerState 클래스 하나만 만들고 HasQuarterState에서 랜덤으로 전환하면 끝. 기존 코드를 거의 안 건드려.

재미있는 건 상태 패턴의 클래스 다이어그램이 전략 패턴과 거의 같다는 거야. 둘 다 인터페이스를 정의하고, 구체 클래스들이 구현하고, Context가 위임하는 구조지. 차이는 의도야. 전략 패턴은 클라이언트가 알고리즘을 직접 선택하는 "상속의 유연한 대안"이고, 상태 패턴은 내부 상태에 따라 행동이 자동으로 바뀌는 "조건문의 유연한 대안"이야.


정리

4장 읽고 기억할 거 세 가지:

  1. 템플릿 메소드는 알고리즘의 골격을 잠그고 세부 단계만 서브클래스에 위임해 — 할리우드 원칙에 따라 고수준이 저수준을 호출하는 구조야.
  2. 반복자 패턴은 컬렉션의 내부 구조를 숨기고, 컴포지트 패턴은 트리 구조를 투명하게 다뤄 — 단일 역할 원칙으로 순회 책임을 분리하는 게 핵심이야.
  3. 상태 패턴은 조건문의 유연한 대안이야 — 전략 패턴과 구조는 같지만, 상태는 내부에서 자동으로 바뀌고 전략은 외부에서 선택하는 차이가 있어.