blog logo
Published on

상속 다루기





들어가며

이번 장에서는 객체 지향 프로그래밍에서 가장 유명한 특성인 상속을 다룬다. 책에서는 상속은 유용하지만 반대로 오용하기도 쉽다고 말하고 있다. 그래서 최악인 경우가 되었을 때 비로소 깨닫는 경우도 많다고 한다. 책에서 매우 중요하게 다뤄야한다고 소개하고 있는 만큼 상속 관련해서 리팩터링을 어떻게 할지가 매우 궁금해졌다.

그리고 또 궁금한 점은 사실 상속은 자바스크립트가 객체 지향을 기반으로 나온 언어가 아니기 때문에 자바스크립트안에서는 상속이라는 단어가 매우 친근(?)하게 다가오지 않는다. 그래서 상속 관련해서 리팩터링을 어떤식으로 내용을 담았는지 많은 호기심을 가지고 책을 읽게 되었다.



12.1 메서드 올리기

리팩터링 전

class Employee {...}

class Salesperson extends Employee {
  get name() {
    // ...
	}
}

class Engineer extends Employee {
  get name() {
    // ...
	}
}
리팩터링 후

class Employee {
  get name() {
    // ...
	}
}

class Salesperson extends Employee {...}

class Engineer extends Employee {...}

배경

중복된 코드 제거는 중요하다. 중복된 코드가 물론 기능 상에 큰 이슈를 가져오지는 않는다. 그래서 무서운 것이라고 생각한다. 코드를 작성하면서 에러가 나지 않지 않는 이상, 무엇이 잘못됐는지를 모를 수 있기 때문이다. 그리고 개발하고 있는 프로젝트의 규모가 커지면 커질수록 중복된 코드를 알아차리기란 당연히 쉽지 않을 것이다.

중복된 코드가 발생했을 경우 어떻게 하면 좋을까? 바로 위의 예시처럼 메서드 올리기를 사용하면 되지만, 항상 모든 상황이 저렇게 쉬운 상황은 아닐 것이다. 그러면 메서드 올리기를 매번 잘 사용하려면 어떻게 해야할까? 책에 나와있는 절차대로 진행해보면 좋을 것 같다.


  1. 먼저 같은 일을 수행하는 메서드(또는 함수)를 찾는다.
// class version

get annualCost() {
	return this.monthlyCost * 12;
}

get totalAnnualCost() {
	return this.monthlyCost * 12;
}
// function version

const getAnnualCost = (monthlyCost: number) => {
  return monthlyCost * 12;
};

const totalAnnualCost = (monthlyCost: number) => {
  return monthlyCost * 12;
};

두 메서드(또는 함수)에서 참조(또는 매개변수)하는 monthlyCost() 는 슈퍼 클래스에는 정의되어 있지 않고 각 서브 클래스에 모두 공통으로 존재한다.

현재는 동적 언어인 자바스크립트라서 괜찮지만, 정적 언어였다면 슈퍼클래스에 추상 메서드를 정의해야 한다.


  1. 서브 클래스(또는 각 함수)중 하나의 메서드를 복사해 슈퍼 클래스(또는 다른 함수)에 붙여 넣는다.
// class version

class Party {
	get annualCost() {
		return this.monthlyCost * 12;
  }
}

// 만약 가져다가 쓸 경우
class AnotherCost extends Party {...}

class TotalCost extends Party {...}
// function version

const getAnnualCost = (monthlyCost: number) => {
  return monthlyCost * 12;
};

// 만약 가져다가 쓸 경우
const TotalCost = (anotherCost: number) => {
  return getAnnualCost() + anotherCost;
};


12.2 필드 올리기

리팩터링 전 // 책에서는 자바 코드로 예시가 되어 있어서 일부 변경함.

class Employee {...}

class Salesperson extends Employee {
  private this.name = 'ayaan';
  private this.age = 20;
}

class Engineer extends Employee {
  private this.name = 'ayaan';
  private this.age = 20;
}
리팩터링 후

class Employee {
  private this.name = 'ayaan';
  private this.age = 20;
}

class Salesperson extends Employee {...}

class Engineer extends Employee {...}

배경

서브클래스들이 독립적으로 개발되었거나 뒤늦게 하나의 계층구조로 리팩터링된 경우라면 일부 기능이 중복될 가능성이 있다고 한다. 특히 위의 예시에 있는 필드들이 중복되기 쉽다고 한다.

먼저 코드를 분석하고 비슷한 방식으로 쓰인다고 판단되면 슈퍼클래스로 끌어올리는 것을 권장한다. 그리고 책에서 간단하게 두 가지 중복을 줄일 수 있는 방법을 소개한다.

첫째, 데이터 중복 선언을 없앨 수 있다.
둘째, 해당 필드를 사용하는 동작을 서브클래스에서 슈퍼클래스로 옮길 수 있다.



12.3 생성자 본문 올리기

리팩터링 전

class Party {...}

class Employee extends Party {
  constructor(name, id, monthlyCost) {
		super();
    this._id = id;
    this._name = name;
		this.monthlyConst = monthlyCost;
  }
}
리팩터링 후

class Party {
	constructor(name) {
    this._name = name;
  }
}

class Employee extends Party {
  constructor(id, monthlyCost) {
		super(name);
    this._id = id;
		this.monthlyConst = monthlyCost;
  }
}

배경

생성자는 다루기 까다롭다. 일반 메서드와 많이 다르기 때문에, 책의 저자는 생성자에게 많은 제약을 두는 편이라고 한다. 생성자는 할 수 있는 일과 호출 순서에 제약이 있기 때문에 리팩터링을 할 때 조금 다른 방식으로 접근을 한다.

💡 리팩터링 절차

1. 슈퍼클래스에 생성자가 없다면 하나 정의한다.
2. 문장 슬라이드하기로 공통 문장 모두를 super() 호출 직후로 옮긴다.
3. 공통 코드를 슈퍼클래스에 추가하고 서브클래스들에게서는 제거한다.
4. 테스트한다.
5. 생성자 시작 부분으로 옮길 수 없는 공통 코드에는 함수 추출하기와 메서드 올리기 기법을 차례로 적용한다.

다음 코드에서 시작해보자.

class Party {}

class Employee extends Party {
  constructor(name, id, monthlyCost) {
    super();
    this._id = id;
    this._name = name;
    this._monthlyCost = monthlyCost;
  }
}

class Department extends Party {
  constructor(name, staff) {
    super();
    this._name = name;
    this.staff = staff;
  }
}

여기서 공통 코드는 this._name = name 이 부분이다.

그리고 리팩터링 하기 전 먼저 테스트 코드를 작성해준다.

describe("Employee 클래스의 값을 테스트한다.", () => {
  beforeEach(() => {
    const employee = new Employee("ayaan", "ayaan-maxst", 1);
  });

  it("name data", () => {
    expect(employee._name).toBe("ayaan");
  });

  it("id data", () => {
    expect(employee._id).toBe("ayaan-maxst");
  });

  it("monthlyCost data", () => {
    expect(employee._monthlyCost).toBe(1);
  });
});

(1번) 생성자가 없다면 하나 정의하고, 공통코드를 (2번) super() 바로 밑으로 문장 슬라이드를 한다.

class Party {
  constructor() {}
}

class Employee extends Party {
  constructor(name, id, monthlyCost) {
    super();
    this._name = name; // super 바로 밑으로 문장 슬라이드
    this._id = id;
    this._monthlyCost = monthlyCost;
  }
}

class Department extends Party {
  constructor(name, staff) {
    super();
    this._name = name;
    this.staff = staff;
  }
}

(3번) 슈퍼클래스에 공통 코드를 추가했다면 이제 서브 클래스에 있는 코드들을 제거해준다. 그리고 슈퍼 클래스 → 서브 클래스를 super() 로 건네준다.

class Party {
  constructor() {;
  }
}

class Employee extends Party {
  constructor(name, id, monthlyCost) {
		super(name);
		~~this._name = name; // super 바로 밑으로 문장 슬라이드~~
		this._id = id;
		this._monthlyCost = monthlyCost;
  }
}

class Department extends Party {
  constructor(name, staff) {
		super(name);
    ~~this._name = name;~~
		this.staff = staff;
  }
}

(4번) 리팩터링 시작하기 전 작성했던 테스트 코드를 실행해본다.

// 시작하기 전 작성했던 테스트 코드
describe("Employee 클래스의 값을 테스트한다.", () => {
  beforeEach(() => {
    const employee = new Employee("ayaan", "ayaan-maxst", 1);
  });

  it("name data", () => {
    expect(employee._name).toBe("ayaan");
  });

  it("id data", () => {
    expect(employee._id).toBe("ayaan-maxst");
  });

  it("monthlyCost data", () => {
    expect(employee._monthlyCost).toBe(1);
  });
});

슈퍼클래스를 통해 서브 클래스가 공통 코드를 전달받고, 이후에 서브 클래스가 테스트 코드를 통과했다면 다음 단계로 넘어가도 좋다.

(5번) 이제 슈퍼클래스 생성자에 매개변수로 name 을 건넨다.

class Party {
  constructor(name) {
    this._name = name;
  }
}


12.4 메서드 내리기

// 리팩터링 전

class Employee {
  get quota {...}
}

class engineer extends Employee {...}

class Salesperson extends Employee {...}
// 리팩터링 후

class Employee {...}

class engineer extends Employee {...}

class Salesperson extends Employee {
  get quota {...}
}

배경

특정 서브 클래스 하나와만 관련된 메서드는 슈퍼클래스에서 제거 후 해당 서브 클래스로 추가해 주는 편이 깔끔하다. 다만 이 리팩터링은 해당 기능을 제공하는 서브 클래스가 정확히 무엇인지를 알고 있을 때만 적용이 가능하다.

💡 리팩터링 절차

1. 대상 메서드를 모든 서브 클래스에 복사한다.
2. 슈퍼클래스에서 그 메서드를 제거한다
3. 테스트한다.
4. 이 메서드를 사용하지 않는 모든 서브클래스에서 제거한다.
5. 테스트한다.


12.5 필드 내리기

// 리팩터링 전
// 책에서는 자바 코드로 예시가 되어 있어서 일부 변경함.

class Employee {
  private this._quota = 'quota';

class engineer extends Employee {...}

class Salesperson extends Employee {...}
// 리팩터링 후

class Employee {...}

class engineer extends Employee {...}

class Salesperson extends Employee {
  private this._quota = 'quota';
}

배경

서브 클래스 하나에서만 사용하는 필드는 해당 서브클래스로 옮긴다.