여기서부터 두 번째 예제, 핸드폰 과금 시스템이 등장한다. 요금을 계산하는 Phone과, 심야에 더 싸게 매기는 NightlyDiscountPhone. 두 클래스는 통화 목록을 순회하며 요금을 더하는 골격이 똑같다 — 전형적인 중복이다. 코드 재사용의 가장 손쉬운 도구는 상속이고, 이 장은 그 상속을 실제로 적용해 본 뒤 어디서 무너지는지까지 끝까지 따라간다. 결론을 미리 말하면, 상속은 중복을 지우는 대신 부모와 자식을 강하게 묶는다. 그 결합이 어떻게 배신하는지가 이 장의 핵심이다.

중복에서 상속으로, 그리고 세금

아래 스텝 플레이어를 순서대로 본다. 두 클래스의 중복에서 시작해, 상속으로 중복을 지우고, 세금이라는 새 요구사항 하나에 그 중복이 되살아나는 데까지 간다.

Refactoring Step 중복 — 두 클래스가 계산 골격을 통째로 복제한다
1/3
public class Phone {    private Money amount;    private Duration seconds;    private List<Call> calls = new ArrayList<>();    public Money calculateFee() {        Money result = Money.ZERO;        for (Call call : calls)            result = result.plus(amount.times(call.getDuration().getSeconds() / seconds.getSeconds()));        return result;    }}public class NightlyDiscountPhone {    private Money nightlyAmount;    private Money regularAmount;    private Duration seconds;    private List<Call> calls = new ArrayList<>();    public Money calculateFee() {        Money result = Money.ZERO;        for (Call call : calls) {            if (call.getFrom().getHour() >= 22)                result = result.plus(nightlyAmount.times(call.getDuration().getSeconds() / seconds.getSeconds()));            else                result = result.plus(regularAmount.times(call.getDuration().getSeconds() / seconds.getSeconds()));        }        return result;    }}

상속이 배신하는 지점

두 번째 스텝까지는 상속이 훌륭해 보인다. calculateFee의 골격이 부모 한 곳에만 있고, 자식은 통화당 요금(calculateCallFee)만 다르게 정의한다. 전형적인 템플릿 메서드 구조다.

문제는 세 번째 스텝이다. “요금에 세금을 붙여 달라”는 새 요구사항 하나가 들어오자, taxRate 필드와 그 초기화가 부모와 자식 양쪽에 나타났다. 자식은 부모의 생성자를 호출해야 하므로 부모의 세금을 알아야 하고, 자기 자신도 세금을 저장한다. 우리가 상속으로 지웠던 바로 그 종류의 중복이 되살아났다.

이것이 취약한 기반 클래스 문제(fragile base class) 다. 자식은 부모의 구현에 의존한다 — 어떤 필드가 있는지, 생성자가 무엇을 받는지, 메서드가 언제 호출되는지까지. 그래서 부모를 고치면 자식이 조용히 깨지거나, 세금 예처럼 자식이 부모의 변경을 강제로 따라가야 한다. 판단 기준: 상속을 쓸 때는 “부모가 바뀌면 이 자식이 함께 바뀌어야 하는가”를 물어라. 답이 자주 “예”라면 그 상속은 결합이 너무 강하다. 함정: 상속은 컴파일 시점에 부모-자식 관계를 못박는다. 실행 중에 정책을 갈아 끼우거나 여러 정책을 조합할 수 없다 — 세금과 심야 할인을 함께 적용하려는 순간 클래스가 조합만큼 폭발한다. 이 두 함정을 한 번에 푸는 답이 다음 장의 합성이다.

정리

  • 핸드폰 과금 예제 시작. PhoneNightlyDiscountPhone은 요금 계산 골격이 중복된다.
  • 상속 + 템플릿 메서드로 골격을 부모에 모으면 중복이 깔끔하게 사라진다 — 여기까지는 좋다.
  • 그러나 세금이라는 요구사항 하나에 taxRate가 부모·자식 양쪽에 생기며 중복이 부활한다. 취약한 기반 클래스 문제다.
  • 자식이 부모의 구현에 의존하기 때문이다. 상속은 결합이 강하고, 조합에 약하다. 그 대안이 합성이다.

다음장으로 11장