상속을 이용해 새로운 행동 얻기
- 고전적 상속에 대해 이야기한다. 고전적이라는 개념은 클래스라는 단어와 맞짝을 이룰 뿐, 낡은 기술이라는 뜻이 아니다. 다른 방식의 상속 시스템과 상위/하위 클래스를 통한 상속 시스템을 구분하기 위해 사용할 뿐이다.
- 기본적으로 상속이란 자동화된 메세지 전달 시스템이다. 객체가 이해하지 못한 메세지를 어디로 전달해야하는지 정의하는 것이다.
- 특정 객체가 이해할 수 없는 메세지를 전달받았을 경우 그 객체는 메세지를 다른 메세지로 전달하는데, 명시적으로 메세지를 위임하는 코드를 작성하지 않아도 두 객체 사이의 상속 관계를 정의하면 자동으로 메세지 전달이 이루어진다.
- 상속은 어느 시점에 필요할까? 예시부터 시작해보자.
class Bicycle {
constructor({ size, tapeColor }) {
this.size = size;
this.tapeColor = tapeColor;
}
getSpares() {
return {
chain: "10-speed",
tireSize: "23",
tapeColor: this.tapeColor,
};
}
}
const bike = new Bicycle({ size: "M", tapeColor: "red" });
console.log(bike.size); // 'M'
console.log(bike.getSpares()); // { chain: '10-speed', tireSize: '23', tapeColor: 'red' }
- 단순한 자전거를 표현한 위의 클래스를 넘어 산악자전거나 로드바이크를 만들어야 한다고 할 떄, 어떻게 지원하도록 만들 수 있을까?
- 이미 대부분의 행동이 구현된 구체 클래스(
Bicycle
)가 있기 때문에, 같은 클래스에 변수나 메서드를 추가하는 방식으로 구현하겠다는 생각이 들 수 있다. 안 좋은 예시를 먼저 보여주고자 하므로 계속 예제를 이어나간다.
class Bicycle {
constructor({ size, tapeColor, style, frontShock, rearShock }) {
this.size = size;
this.tapeColor = tapeColor;
this.style = style;
this.frontShock = frontShock;
this.rearShock = rearShock;
}
getSpares() {
if (this.style === "road") {
return {
chain: "10-speed",
tireSize: "23", // 밀리미터
tapeColor: this.tapeColor,
};
}
return {
chain: "10-speed",
tireSize: "2.1", // 인치
tapeColor: this.tapeColor,
};
}
}
const bike = new Bicycle({
size: "S",
tapeColor: "red",
style: "mountain",
frontShock: "Manitou",
rearShock: "Fox",
});
console.log(bike.getSpares()); // { chain: '10-speed', tireSize: '2.1', tapeColor: 'red' }
- 위의 코드에서 새로운
style
이 추가될 경우 if 문이 수정되어야 한다. 또한 각 if 문마다 기본으로 박혀있는 문자열들이 있고, 그 문자열이 매 if 문마다 반복되고 있다.
- 자기가 어떤 종류인지 파악하기 위한 if 문이 있으며, 이
style
속성을 통해 어떤 메세지를 보낼지 결정하고 있다.
- 앞 장에서 오리 타입에 대해 이야기하면서 보았던 패턴을 떠올릴 수 있을 것이다. 객체의 클래스를 확인하고 이 객체에게 어떤 메세지를 전송할지 결정하는 것 말이다.
- 메세지 송신자의 입장에서 이런 표현을 쓸 수 있을 것이다. "나는 네가 누구인지 알고 있고, 따라서 네가 무엇을 하는지도 안다."
- 여기서 숨겨진 하위 타입(하위 클래스)를 찾아내야 한다.
- 변수
style
은 Bicycle 을 서로 다른 두 종류로 구분하고 있다. 하나의 클래스가 여러개의 서로 다르지만 연관된 타입을 가지고 있다.
- 즉 밀접히 연관된 타입들이 같은 행동을 공유하고 있지만 특정한 관점에서는 다른 경우인 것이다.
- 당연한 말이지만 객체는 메세지를 수신한다. 코드가 얼마나 복잡하든 메세지를 수신하는 객체는 다음의 두 가지 방법 중 하나로 메세지를 처리한다.
- 메세지를 직접 처리하거나
- 다른 객체가 처리할 수 있도록 메세지를 넘기거나
- 상속은 두 객체 사이의 관계를 정의한다. 첫 번째 객체가 이해할 수 없는 메세지를 수신하면 다음 객체에게 자동으로 메세지를 전달하거나 위임한다. 상속은 두 객체가 이와 같은 관계를 맺도록 정의해준다.
- 생뭃학적인 상속에 빗대면 다중 상속도 떠올릴 수 있겠지만 프로그래밍 언어에 따라 다중 상속을 지원할 수도 있고, 단일 상속만 지원할 수도 있다.
- 초반에 언급했듯 고전적 상속을 통한 메세지 전달은 클래스들 사이에 이루어지는 작업이다. 오리 타입은 클래스를 가로지르기 때문에 공통의 행동을 공유하기 위해 고전적 상속을 사용하지 않는다.
- 상속을 직접적으로 이용한 적이 없더라도 메세지의 자동 전달 시스템을 이용하고 있을 것이다. 어떤 객체가 이해할 수 없는 메세지를 수신하면 그 메세지를 상위 클래스로 이어 전달하여 이 메세지를 처리할 수 있는 메서드를 구현하고 있는 상위 클래스를 찾는다. JS 라면 프로토타입 체인이 유사한 예가 될 수 있겠다.
- 이해하지 못하는 메세지가 상위클래스의 연쇄를 타고 올라간다는 사실은, 하위 클래스는 상위 클래스의 모든 행동을 가지고 있고 여기서 추가적인 행동을 더 가지고 있다는 사실을 말해준다.
- 상속을 잘못 사용한다면? - MountainBike 의 예
class MountainBike extends Bicycle {
constructor(props = {}) {
super(props);
this.frontShock = props.frontShock;
this.rearShock = props.rearShock;
}
getSpares() {
return {
...super.getSpares(),
rearShock: this.rearShock,
frontShock: this.frontShock,
};
}
}
const mountainBike = new MountainBike({
size: "S",
frontShock: "Manitou",
rearShock: "Fox",
});
console.log(mountainBike.size); // S
console.log(mountainBike.getSpares());
/**
* {
* chain: '10-speed',
* tireSize: '23', // 타이어 사이즈가 기대한것과 다름
* tapeColor: undefined, // 해당 사항 없음
* rearShock: 'Fox',
* frontShock: 'Manitou'
* }
*/
- Bicycle 클래스는 상위 클래스가 아니라 구체 클래스이기 때문에 MountainBike 의 인스턴스가 여러 속성을 뒤죽박죽으로 가지고 있는 것이 당연한 상태이다.
- Bicycle 클래스는 MountainBike의 형제 클래스에게 어울리는 행동과 부모 클래스에게 어울리는 행동을 모두 가지고 있다. 따라서 Bicycle은 MountainBike의 상위클래스일 수 없다.
- 애초에 문제는 클래스의 이름을 정할 때부터 시작되었다.
- Bicycle 이라는 클래스는 애초에 만들어질 때부터 범용적이지 않고 특수한 형태의 자전거(로드 자전거)를 기반으로 만들어졌다. 그러다보니 MountainBike 가 만들어지면서 이름만 보면 상속관계를 암시하지만 기능적으로 전혀 그렇지 않은 형태가 되어버렸다.