HomeAboutMeBlogGuest
© 2025 Sejin Cha. All rights reserved.
Built with Next.js, deployed on Vercel
🐣
프론트엔드 데브코스 3기 교육생
/
📚
3기 스터디 가이드
/
📚
리팩터링 북스터디
/12 상속 다루기/
7-11장 (백민종)

7-11장 (백민종)

7. 서브클래스 제거하기서브클래싱?서브클래싱의 문제점절차예시8. 슈퍼클래스 추출하기저자가 말하는 상속절차예시9. 계층 합치기절차10. 서브클래스를 위임으로 바꾸기저자가 말하는 상속의 단점상속은 위험할까?절차예시정말 서브클래스를  위임으로 바꿔야 하는 구조일까? 🤔11. 슈퍼클래스를 위임으로 바꾸기위임의 단점절차예시

7. 서브클래스 제거하기

서브클래싱?

  • 원래 데이터 구조와는 다른 변종을 만들거나 종류에 따라 동작이 달라지게 할 수 있는 유용한 메커니즘이다.
  • 다름을 프로그래밍하는 멋진 수단

서브클래싱의 문제점

  • 시스템 규모가 커짐에 따라 서브클래스가 다른 모듈로 이동하거나 사라져서 활용되지 않을 수 있다.
  • 서브클래스를 필요로 하지 않는 방식으로 만들어진 기능에서만 사용되기도 한다.

절차

  1. 서브클래스의 생성자를 팩터리 함수로 변경한다.
    1. 서브클래스의 타입을 검사하는 코드가 있다면, 함수 추출하기와 함수 옮기기를 차례로 적용하여 슈퍼클래스로 옮긴다.
  1. 서브클래스의 타입을 나태내는 필드를 슈퍼클래스에 만든다.
  1. 서브클래스를 참조하는 메서드가 방금 만든 타입 필드를 이용하도록 수정한다.
  1. 서브클래스를 지운다.
  1. 테스트한다.

예시

class Person { constructor(name) { this._name = name; } get genderCode() { return "X"; } } class Male extends Person { get genderCode() { return "M"; } } class Femail extends Person { get genderCode() { return "F"; } }
서브클래스가 하는 일이 이게 다라면 굳이 존재할 이유가 없다.
리팩터링
function createPerson(aRecord) { // 서브클래스 팩터리 함수 switch (aRecord.gender) { case "M": return new Person(aRecord.name, "M"); case "F": return new Person(aRecord.name, "F"); default: return new Person(aRecord.name), "X"; } }
class Person { constructor(name, genderCode) { this._name = name; this._genderCode = genderCode; // 서브클래스의 타입을 나태내는 필드 생성 } get genderCode() { return this._genderCode; } } // 서브클래스 지우기

8. 슈퍼클래스 추출하기

저자가 말하는 상속

‘현실 세계의 분류 체계를 기초로 하여 부모-자식 관계를 신중하게 설계해야 한다.’ 라는 말처럼 현실 세계의 분류 체계는 상속을 적용하는데 힌트가 될 수 있지만, 상속은 슈퍼클래스로 끌어올리고 싶은 공통 요소를 찾았을 때 수행하는 사례가 잦았다.
  • 비슷한 일을 수행하는 두 클래스가 있을 때 비슷한 부분을 슈퍼클래스로 옮겨 상속 관계를 형성할 수 있다.

절차

  1. 빈 슈퍼클래스를 만든다. 원래의 클래스들이 새 클래스를 상속하도록 한다.
  1. 테스트한다.
  1. 생성자 본문 올리기, 메서드 올리기, 필드 올리기를 차례로 적용하여 공통 원소를 슈퍼클래스로 옮긴다.
  1. 서브클래스에 남은 메서드들을 검토한다. 공통되는 부분은 함수로 추출한 다음 메서드 올리기를 적용한다.
  1. 원래 클래스를 사용하는 코드를 검토하여 슈퍼 클래스의 인터페이스를 사용하게 할지 고민한다.

예시

class Employee { constructor(name, id, monthlyCost) { this._id = id; this._name = name; this._monthlyCost = monthlyCost; } get name() { return this._name; } get monthlyCost() { return this._monthlyCost; } get annualCost() { return this.monthlyCost * 12; } } class Department { constructor(name, staff) { this._name = name; this._staff = staff; } get name() { return this._name; } get totalMonthlyCost() { return this.staff .map((e) => e.monthlyCost) .reduce((sum, cost) => sum + cost); } get totalAnnualCost() { return this.totalMonthlyCost * 12; } }
이름 필드와 월간, 연간 비용 조회 기능이 공통된 두 클래스
리팩터링
class Party { constructor(name) { // 필드 올리기 this._name = name; } get name() { // 메서드 올리기 return this._name; } get annualCost() { // 함수 선언 바꾸기 & 메서드 올리기 return this.monthlyCost * 12; } } class Employee extends Party { constructor(name, id, monthlyCost) { super(name); this._id = id; this._monthlyCost = monthlyCost; } get monthlyCost() { return this._monthlyCost; } } class Department extends Party { constructor(name, staff) { super(name); this._staff = staff; } get monthlyCost() { return this.staff .map((e) => e.monthlyCost) .reduce((sum, cost) => sum + cost); } }

9. 계층 합치기

  • 슈퍼클래스와 서브클래스가 비슷해져 독립적으로 존재해야 할 이유가 사라지는 경우에 적용할 수 있다.

절차

  1. 두 클래스 중 제거할 것을 고른다.
  1. 필드 올리기와 메서드 올리기 혹은 필드 내리기와 메서드 내리기를 적용하여 모든 요소를 하나의 클래스로 옮긴다.
  1. 제거할 클래스를 참조하던 모든 코드가 남겨질 클래스를 참조하도록 고친다.
  1. 빈 클래스를 제거한다.
  1. 테스트한다.

10. 서브클래스를 위임으로 바꾸기

저자가 말하는 상속의 단점

  • 달라져야 하는 이유가 여러 개여도 상속에서는 단 하나의 이유만 선택해 기준으로 삼는다.
    • 젊은이 - 어르신
    • 부자 - 서민
  • 관계가 긴밀하여 부모를 수정했을 때 자식에서 발생할 수 있는 사이드 이펙트를 주의해야 한다.

상속은 위험할까?

클래스의 상속보다 객체 컴포지션을 사용하라!
많은 사람들이 이 원칙을 듣고 ‘상속은 위험하다’라고 받아들여서 상속을 하면 안된다고 주장한다고 한다. 이 원칙은 상속을 쓰지 말라는 말이 아닌, 과용하는데 따른 반작용으로 나온 것이다.
책의 저자는 언제든 서브클래스를 위임으로 바꿀 수 있음을 알기에, 처음에는 상속으로 접근하고 문제가 생기면 위임으로 갈아탄다고 설명한다.

절차

  1. 생성자를 호출하는 곳이 많다면 생성자를 팩터리 함수로 바꾼다.
  1. 위임으로 활용할 빈 클래스를 만든다. 이 클래스의 생성자는 서브클래스에 특화된 데이터를 전부 받아야 하며, 보통은 슈퍼클래스를 가리키는 역참조도 필요하다.
  1. 위임을 저장할 필드를 슈퍼클래스에 추가한다.
  1. 서브클래스 생성 코드를 수정하여 위임 인스턴스를 생성하고 위임 필드에 대입해 초기화한다.
  1. 서브클래스의 메서드 중 위임 클래스로 이동할 것을 고른다.
  1. 함수 옮기기를 적용해 위임 클래스로 옮긴다. 원래 메서드에서 위임하는 코드는 지우지 않는다.
  1. 서브클래스 외부에도 원래 메서드를 호출하는 코드가 있다면 서브클래스의 위임 코드를 슈퍼클래스로 옮긴다.
    1. 위임이 존재하는지를 검사하는 보호 코드로 감싸야 한다. 호출하는 외부 코드가 없다면 원래 메서드는 죽은 코드가 되므로 제거한다.
  1. 테스트한다.
  1. 서브클래스의 모든 메서드가 옮겨질 때까지 5~8 과정을 반복한다.
  1. 서브클래스들의 생성자를 호출하는 코드를 찾아서 슈퍼클래스의 생성자를 사용하도록 수정한다.
  1. 테스트한다.
  1. 서브클래스를 삭제한다.

예시

class Booking { constructor(show, date) { this._show = show; this._date = date; } get hasTalkback() { return this._show.hasOwnProperty("talback") && !this.isPeakDay; } get basePrice() { let result = this._show.price; if (this.isPeakDay) result += Math.round(result * 0.15); return result; } } class PremiumBooking extends Booking { constructor(show, date, extras) { super(show, date); this._extras = extras; } get hasTalkback() { return this._show.hasOwnProperty("talback"); // 오버라이드 } get basePrice() { return Math.round(super.basePrice + this._extras.premiumFee); // 오버라이드 } get hasDinner() { return this._extras.hasOwnProperty("dinner") && !this.isPeakDay; } }
상속이 적절하게 적용된 두 클래스

정말 서브클래스를  위임으로 바꿔야 하는 구조일까? 🤔

만약 일반 예약과 프리미엄 예약을 자유롭게 전환해야 한다면 어떨까?
  • HTTP 요청을 통해 새로운 데이터를 받아서 처리한다.
    • 새로 받아오는 방법을 사용하지 못하고, 데이터 구조를 수정해야 하는 상황 이라면?
    • 수 많은 곳에서 참조되는 예약 인스턴스를 어떻게 교체하지?
위 예시가 와닿지 않는다면, 예약을 일반과 프리미엄이라는 기준 대신 다른 기준을 가져가야 한다면 어떨까?
리팩터링
// 생성자를 팩터리 함수로 변경 function createBooking(show, date) { return new Booking(show, date); } // 예약 객체와 위임 클래스를 연결 function createPremiumBooking(show, date, extras) { const result = new Booking(show, date, extras); result._bePremium(extras); return result; }
class Booking { constructor(show, date) { this._show = show; this._date = date; } _bePremium(extras) { this._premiumDelegate = new PremiumBookingDelegate(this, extras); } get hasTalkback() { return this._premiumDelegate ? this.PremiumBookingDelegate.hasTalkback : this._show.hasOwnProperty("talkback") && !this.isPeakDay; } get basePrice() { let result = this._show.price; if (this.isPeakDay) result += Math.round(result * 0.15); return this._premiumDelegate ? this._premiumDelegate.extendBasePrice(result) : result; } get hasDinner() { return this._premiumDelegate ? this._premiumDelegate.hasDinner : undefined; } } class PremiumBookingDelegate { // 위임 클래스는 서브클래스가 사용하던 매개변수와 예약 객체로의 역참조(this)를 매개변수로 받는다. constructor(hostBooking, extras) { this._host = hostBooking; this._extras = extras; } get hasTalkback() { return this._host._show.hasOwnProperty("talkback"); } extendBasePrice(base) { return Math.round(base + this._extras.premiumFee); } }
 

11. 슈퍼클래스를 위임으로 바꾸기

위임의 단점

  • 위임의 기능을 이용할 호스트의 함수를 모두 전달 함수로 만들어야 한다.

절차

  1. 슈퍼클래스 객체를 참조하는 필드를 서브클래스에 만든다. 위임 참조를 새로운 슈퍼클래스 인스턴스로 초기화한다.
  1. 슈퍼클래스의 동작 각각에 대응하는 전달 함수를 서브클래스에 만든다.
    1. 서로 관련된 함수끼리 그룹으로 묶어 진행하며, 그룹을 하나씩 만들 때마다 테스트한다.

예시

class CatalogItem { constructor(id, title, tags) { this._id = id; this._title = title; this._tags = tags; } get id() { return this._id; } get title() { return this._title; } hasTag(arg) { return this._tags.includes(arg); } } class Scroll extends CatalogItem { constructor(id, title, tags, dateLastCleaned) { super(id, title, tags); this._dateLastCleaned = dateLastCleaned; } needsCleaning(targetDate) { const threshold = this.hasTag("revered") ? 700 : 1500; return this.daysSinceLastCleaning(targetDate) > threshold; } daysSinceLastCleaning(targetDate) { return this._dateLastCleaned.until(targetDate, ChronoUnit.DAYS); } }
물리적 대상과 논리적 대상이 다르게 표현된 상속 오류를 표현하는 두 클래스
리팩터링