TDD는 설계를 대체하지 않는다 — 오히려 설계 패턴이 필요한 지점으로 자연스럽게 데려간다. 이 장은 벡이 테스트를 통과시키고 중복을 지우다 반복해 마주친 패턴들을 카탈로그로 묶은 것이다. 대부분은 GoF에 이름이 있지만, 여기서는 “리팩터링의 종착지”라는 각도에서 읽는다. 패턴이 많아 볼드 리드를 짧게 유지한다.
값과 동작을 객체로
값 객체(Value Object). 생성 후 상태가 변하지 않는 객체다. times 같은 오퍼레이션은 자신을 바꾸지 않고 새 객체를 반환한다. 별칭(aliasing) 문제가 사라져 마음 놓고 공유할 수 있다. 2장에서 Dollar의 부작용을 없애며 값 객체로 만든 그 흐름이다. 값 객체는 equals와 hashCode를 값 기준으로 재정의해야 한다(3장).
Money times(int n) {
return new Money(amount * n, currency); // 자신을 바꾸지 않고 새로 반환
}커맨드(Command). 계산을 메시지 호출이 아니라 객체로 표현한다. “할 일”을 객체로 만들면 나중에 실행하거나, 큐에 쌓거나, 취소할 수 있다. 함수를 일급 시민으로 다루는 방법이다.
수집 매개 변수(Collecting Parameter). 여러 객체에 걸친 오퍼레이션의 결과를 모을 때, 결과를 담을 객체를 인자로 넘겨 각 객체가 거기에 자기 몫을 더하게 한다. 24장에서 TestResult가 여러 테스트의 성공·실패를 모으던 방식이 정확히 이것이다.
def run(self, result): # result가 수집 매개 변수
result.test_started()
try:
method()
except Exception:
result.test_failed()판단 기준: 결과를 반환값으로 합치기가 번거로운(여러 객체·재귀 구조) 집계라면 수집 매개 변수가 깔끔하다. 함정: 값 객체를 만든다면서 세터를 남겨 두면 불변이 아니다 — 별칭 버그가 그대로 살아 있다.
조건 분기를 객체로 흡수한다
널 객체(Null Object). “없음”을 null 대신 아무것도 하지 않는 객체로 표현한다. 호출부의 if (x != null) 검사가 통째로 사라진다. 없음도 하나의 정상적인 경우로 다형성에 흡수된다.
class NullAccount extends Account {
void charge(int amount) { } // 아무것도 안 한다 — null 검사가 사라진다
}사칭 사기꾼(Imposter). 기존 객체와 같은 인터페이스를 가진 새 구현을 슬쩍 끼워 넣어 동작을 바꾼다. 널 객체와 모의 객체가 모두 이 패턴의 사례다 — 진짜인 척하는 대역이다. 호출부는 진짜인지 대역인지 모른 채 같은 메시지를 보낸다.
플러거블 객체(Pluggable Object). 두 번째 if가 등장하는 순간이 이 패턴의 신호다. 조건에 따라 갈리는 동작을, 조건 분기 대신 갈아 끼울 수 있는 객체로 바꾼다. 클린코드의 “다형성을 쓰라(G23)“와 같은 처방이다.
플러거블 셀렉터(Pluggable Selector). 객체에 따라 호출할 메서드 이름만 다를 때, 서브클래스를 여럿 만드는 대신 메서드 이름을 데이터로 저장해 동적으로 호출한다. 남용하면 흐름 추적이 어려워지는 대가가 있어, 서브클래스가 메서드 하나 때문에 과할 때만 쓴다.
판단 기준: 같은 조건 분기가 여러 곳에서 반복되면 플러거블 객체·널 객체로 흡수할 때다. 함정: 플러거블 셀렉터로 메서드를 문자열로 호출하면 IDE의 “사용처 찾기”가 안 통해 리팩터링이 위험해진다 — 꼭 필요한 곳에만.
뼈대와 변형을 분리한다
템플릿 메서드(Template Method). 오퍼레이션의 뼈대(순서)를 상위에 고정하고, 각 단계의 구체적 내용을 하위나 훅으로 위임한다. 20장에서 run 메서드가 setUp → test → tearDown 순서를 고정하고 각 훅을 위임하던 구조다. 불변인 알고리즘과 가변인 세부를 갈라 둔다.
팩토리 메서드(Factory Method). 생성자를 직접 부르는 대신 메서드로 객체를 만든다. Money.dollar(5)처럼. 반환 타입을 구현과 분리할 수 있고(7장에서 Dollar를 숨기고 Money만 노출), 생성 시점에 이름을 붙여 의도를 드러낸다.
static Money dollar(int amount) { // 생성자 대신 팩토리
return new Money(amount, "USD"); // 구현 타입을 숨긴다
}판단 기준: 생성 로직이 복잡하거나 구현 타입을 감추고 싶으면 팩토리 메서드다. 함정: 팩토리를 만들면서 생성자를 public으로 열어 두면 두 갈래 생성 경로가 생겨 캡슐화가 새다 — 생성자는 접근을 좁힌다.
부분과 전체를 같게 다룬다
컴포지트(Composite). 여러 개를 담은 묶음이, 하나짜리와 똑같은 인터페이스를 갖게 한다. TestSuite가 여러 TestCase를 담으면서도 run(result)라는 같은 메서드로 실행되던 24장의 구조다. 호출부는 하나를 다루는지 여럿을 다루는지 신경 쓰지 않는다. 28장의 “하나에서 여럿으로”가 설계로 굳으면 종종 컴포지트가 된다.
판단 기준: “하나”와 “하나 이상”을 호출부가 구분해서 다루기 시작하면 컴포지트로 통일할 때다. 함정: 묶음에만 있고 낱개엔 없는 메서드를 인터페이스에 억지로 넣으면, 낱개 쪽에서 빈 구현·예외가 생겨 추상화가 샌다.
정리
- 이 패턴들은 외워서 적용하는 목표가 아니라, 중복을 지우다 도달하는 종착지다. TDD가 이들로 데려간다.
- 반복되는 조건 분기 → 널 객체·플러거블 객체, 하나와 여럿의 통일 → 컴포지트, 알고리즘 뼈대의 고정 → 템플릿 메서드. 냄새가 패턴을 부른다.
- 앞부(2장·3장·7장·20장·24장)에서 손으로 지나온 지점들이 여기서 이름을 얻는다.
다음장으로 31장