Chapter 12

상속 다루기

  • 12.1 메서드 올리기
  • 12.2 필드 올리기
  • 12.3 생성자 본문 올리기
  • 12.4 메서드 내리기
  • 12.5 필드 내리기
  • 12.6 타입 코드를 서브클래스로 바꾸기
  • 12.7 서브클래스 제거하기
  • 12.8 슈퍼클래스 추출하기
  • 12.9 계층 합치기
  • 12.10 서브클래스를 위임으로 바꾸기
  • 12.11 슈퍼클래스를 위임으로 바꾸기

상속은 강력하지만 오용하기 쉬운 메커니즘이야. 12장은 상속을 다루는 마지막 챕터로, 상속 구조를 조정하는 기법과 상속 대신 위임을 선택해야 할 때를 다뤄.

**메서드 올리기(Pull Up Method)**는 서브클래스들에 동일한 메서드가 있으면 슈퍼클래스로 올리는 거야. 중복이니까. 한쪽을 수정할 때 다른 쪽도 수정해야 하는 문제를 없앨 수 있지. 메서드가 비슷하지만 완전히 같지는 않을 때가 까다로운데, 이때는 함수 매개변수화하기를 적용해서 동일하게 만든 다음 올려. 또는 "템플릿 메서드 만들기" — 공통 뼈대를 슈퍼클래스에 두고 달라지는 부분만 서브클래스에서 오버라이드하게 만들어. **필드 올리기(Pull Up Field)**도 같은 원리야. 서브클래스들에 같은 필드가 있으면 슈퍼클래스로 올려. 중복을 제거하고, 해당 필드를 사용하는 동작을 슈퍼클래스로 올길 수 있는 기반을 마련하는 거지.

**생성자 본문 올리기(Pull Up Constructor Body)**는 서브클래스들의 생성자에 공통 코드가 있으면 슈퍼클래스 생성자로 올리는 거야. 생성자는 일반 메서드와 다르게 제약이 있어서 단순히 메서드 올리기를 적용하기 어렵거든.

// Before
class Manager extends Employee {
  constructor(name, id, grade) {
    super();
    this._name = name;
    this._id = id;
    this._grade = grade;
  }
}

// After
class Employee {
  constructor(name, id) {
    this._name = name;
    this._id = id;
  }
}
class Manager extends Employee {
  constructor(name, id, grade) {
    super(name, id);
    this._grade = grade;
  }
}

반대 방향도 있어. **메서드 내리기(Push Down Method)**는 슈퍼클래스의 메서드가 특정 서브클래스에서만 의미 있는 경우 해당 서브클래스로 내리는 거야. 슈퍼클래스가 알 필요 없는 지식을 줄여서 깔끔하게 만들지. **필드 내리기(Push Down Field)**도 같은 원리야.

**타입 코드를 서브클래스로 바꾸기(Replace Type Code with Subclasses)**는 타입을 나타내는 필드(type === "engineer" 같은)를 서브클래스로 바꾸는 거야. 서브클래스로 바꾸면 두 가지 이점이 있어 — **"다형성을 활용할 수 있다"**는 거랑 **"타입별로 다른 필드나 메서드를 가질 수 있다"**는 거지. 직접 상속이 이미 쓰이고 있으면 전략 패턴처럼 별도 클래스 계층에 적용해. 반대로 **서브클래스 제거하기(Remove Subclass)**는 서브클래스가 하는 일이 거의 없어서 존재 가치가 없을 때, 슈퍼클래스의 필드로 대체하는 거야.

**슈퍼클래스 추출하기(Extract Superclass)**는 비슷한 두 클래스가 있을 때 공통 부분을 슈퍼클래스로 올리는 거야. 상속은 프로그램이 성장하면서 뒤늦게 도입되는 경우가 많거든. 대안으로 클래스 추출하기를 적용해서 위임으로 처리할 수도 있지만, 상속이 더 간단한 경우가 많아. 나중에 필요하면 슈퍼클래스를 위임으로 바꾸면 되니까. **계층 합치기(Collapse Hierarchy)**는 슈퍼클래스와 서브클래스가 너무 비슷해서 구분할 이유가 없어졌을 때 하나로 합치는 거야.

이 챕터에서 가장 중요한 기법은 **서브클래스를 위임으로 바꾸기(Replace Subclass with Delegate)**야. **"상속 대신 구성(composition)을 사용하라"**는 객체지향의 고전적 조언을 실현하는 리팩터링이지. 상속의 단점은 명확해 — "한 번만 쓸 수 있다." 단일 상속 언어에서는 한 가지 기준으로만 서브클래스를 나눌 수 있어. "사람"을 "나이대별"로도 나누고 "소득 수준별"로도 나누고 싶으면 상속으로는 불가능하지. 그리고 "클래스 간 결합도가 높다." 부모를 수정하면 자식이 깨질 수 있어. 위임으로 바꾸면 여러 기준으로 동시에 다양한 조합이 가능하고, 실행 시점에 동적으로 바꿀 수 있고, 결합도가 낮아져.

**슈퍼클래스를 위임으로 바꾸기(Replace Superclass with Delegate)**도 같은 맥락이야. 대표적인 안티패턴이 StackList를 상속하는 경우야. 스택은 리스트의 모든 기능이 필요한 게 아니거든. 중간 삽입이나 인덱스 접근은 스택에 맞지 않는데, 상속하면 이런 메서드가 다 노출돼. "자식이 부모의 모든 기능을 지원해야 한다는 건 상속의 전제 조건이다." 이 전제가 깨지면 상속이 적절하지 않아.

// Before — Stack extends List
class Stack extends List { ... }

// After — Stack이 List를 필드로 갖고, 필요한 메서드만 위임
class Stack {
  constructor() { this._storage = new List(); }
  push(value) { this._storage.append(value); }
  pop() { return this._storage.removeLast(); }
}

파울러의 실용적 접근법은 이거야 — "일단 상속을 적용해보고, 문제가 생기면 위임으로 전환하라." 상속이 잘 맞으면 더 간단하니까. 안 맞으면 이 리팩터링으로 고치면 돼.


정리

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

  1. "상속은 강력하지만 한 번만 쓸 수 있다." 여러 기준으로 변형이 필요하면 위임을 고려해
  2. "상속보다 위임이 나은 경우가 많다." 서브클래스를 위임으로, 슈퍼클래스를 위임으로 바꾸는 기법을 알아두면 언제든 전환할 수 있어
  3. "일단 상속을 시도하고, 안 맞으면 위임으로 바꿔라." 상속이 간단한 경우도 많으니, 처음부터 위임을 고집할 필요는 없어