LHJ

I'm a FE developer.

9.3 다중 상속, 믹스인, 인터페이스

10 May 2020 » js_lj

다중 상속

일부 객체지향 언어에서는 다중 상속(multiple inheritance) 이란 기능을 지원합니다.
이 기능은 클래스가 슈퍼클래스 두 개를 가지는 기능이며,
슈퍼클래스의 슈퍼클래스가 존재하는 일반적인 상속과는 다릅니다.
다중 상속에는 충돌의 위험이 있습니다.
예를 들어 어떤 클래스에 두 개의 슈퍼 클래스가 있고, 두 슈퍼클래스에 모두 greet 메서드가 있다면 서브클래스는 어느 쪽의 메서드를 상속해야 할까요?
다중 상속을 지원하지 않는 언어가 많은 이유는 이런 문제를 피하기 위해서입니다.

하지만 현실 세계를 생각해 보면 다중 상속을 적용할 수 있는 문제가 많습니다.
예를 들어 자동차는 운송 수단인 동시에 ‘보험을 들 수 있는’ 대상입니다.
주택에도 보험을 들 수 있지만, 주택은 운송 수단이 아닙니다.

인터페이스

다중 상속을 지원하지 않는 언어 중에는 인터페이스(interface) 개념을 도입해서 이런 상황에 대처하는 언어가 많습니다.
Car 클래스의 슈퍼클래스는 Vehicle 하나뿐이지만, Insurable, Container 등 여러 인터페이스를 가질 수 있습니다.

자바스크립트는 흥미로운 방식으로 이들을 절충했습니다.
자바스크립트는 프로토타입 체인에서 여러 부모를 검색하지는 않으므로 단일 상속 언어라고 해야 하지만, 어떤 면에서는 다중 상속이나 인터페이스보다 더 나은 방법을 제공합니다(물론 더 못할 때도 있습니다).

믹스인

자바스크립트가 다중 상속이 필요한 문제에 대한 해답으로 내놓은 개념이 믹스인(mixin) 입니다.
믹스인이란 기능을 필요한 만큼 섞어 놓은 것입니다.
자바스크립트는 느슨한 타입을 사용하고 대단히 관대한 언어이므로 그 어떤 기능이라도 언제든, 어떤 객체에든 추가할 수 있습니다.

그러면 자동차에 적용할 수 있는 보험 가입(insurable) 믹스인을 만듭시다.
이 믹스인은 단순하게 만들 겁니다.
보험 가입 믹스인 외에도 InsurancePolicy 클래스를 만듭니다.
보험 가입 믹스인에는 addInsurancePolicy, getInsurancePolicy 메서드가 필요하며, 편의를 위해 isInsured 메서드도 추가하겠습니다.
예제를 봅시다.

class InsurancePolicy {}
function makeInsurable(o) {
	o.addInsurancePolicy = function(p) { this.insurancePolicy = p; }
	o.getInsurancePolicy = function() { return this.insurancePolicy; }
	o.isInsured = function() { return !!this.insurancePolicy; }
}

이제 우리는 어떤 객체든 보험에 가입할 수 있습니다.
그러면 Car로 돌아와서, 무엇을 보험에 가입해야 할까요?
가장 먼저 드는 생각은 이런 것이겠지요.

makeInsurable(Car);

하지만 그렇게는 되지 않습니다.

const car1 = new Car();
car1.addInsurancePolicy(new InsurancePolicy()); // error

“addInsurancePolicy가 프로토타입 체인에 존재하지 않으니 당연하지” 라고 생각했다면, 애석하지만 틀린 답입니다.
그렇게 해도 Car를 보험에 가입할 수는 없습니다.
상식적이지도 않습니다.
자동차를 추상화한 개념을 보험에 가입할 수는 없죠.
보험에 가입하는 것은 개별 자동차입니다.
그러니 이렇게 해봅시다.

const Car = (function() {
	
	const carProps = new WeakMap();
	
	class Car {
		static getNextVin() {
			return Car.nextVin++;   // this.nextVin++ 라고 써도 되지만,
			                        // Car를 앞에 쓰면 정적 메서드라는 점을
			                        // 상기하기 쉽습니다.
		}

		constructor(make, model) {
			this.make = make;
			this.model = model;
			this.vin = Car.getNextVin();
		}

		static areSimilar(car1, car2) {
			return car1.make === car2.make && car.model === car2.model;
		}
		static areSame(car1, car2) {
			return car1.vin === car2.vin;
		}

		toString () {
			return `${this.make} ${this.model} : ${this.vin}`
		}
	}
	
	return Car;
})();

Car.nextVin = 0;

const car1 = new Car();
makeInsurable(car1);
car1.addInsurancePolicy(new InsurancePolicy()); // works

이 방법은 동작하지만, 모든 자동차에서 makeInsurable 을 호출해야 합니다.
Car 생성자에 추가할 수도 있지만, 그렇게 하면 이 기능을 모든 자동차에 복사하는 형국이 됩니다.
다행히 해결책은 쉬운 편입니다.

makeInsurable(Car.prototype);
const car1 = new Car();
car1.addInsurancePolicy(new InsurancePolicy()); // works

이제 보험 관련 메서드들은 모두 Car 클래스에 정의된 것처럼 동작합니다.
자바스크립트의 관점에서는 실제로 그렇습니다.
개발자의 관점에서는 중요한 두 클래스를 관리하기 쉽게 만들었습니다.
자동차 회사에서 Car 클래스의 개발과 관리를 담당하고, 보험 회사에서 InsurancePolicy 클래스와 makeInsurable 믹스인을 관리하게 됩니다.
두 회사의 업무가 충돌할 가능성을 완전히 없앤 건 아니지만, 모두가 거대한 Car 클래스에 달라붙어 일하는 것보다는 낫습니다.

물론 믹스인이 모든 문제를 해결해 주지는 않습니다.
보험 회사에서 shift 메서드를 만들게 된다면 Car 클래스의 동작이 이상해질 겁니다.충돌
instanceof 연산자로 보험에 가입할 수 있는 객체를 식별할 수도 없습니다.
‘addInsurancePolicy 메서드가 있다면 틀림없이 보험에 가입할 수 있다’는 식의 짐작만 가능할 뿐입니다.

심볼을 사용하면 이런 문제 일부를 경감할 수 있습니다.
보험 회사에서 매우 범용적인 메서드 이름을 계속 사용해서, 우연히 Car 클래스의 메서드와 충돌할까 봐 걱정된다고 가정합시다.
그러면 보험 회사에 키를 모두 심볼로 사용해 달라고 요청할 수 있습니다.
보험사가 제공하는 믹스인은 다음과 같은 형태가 될 겁니다.

class InsurancePolicy {}

const ADD_POLICY = Symbol();
const GET_POLICY = Symbol();
const IS_INSURED = Symbol();
const _POLICY = Symbol();

function makeInsurable(o) {
	o[ADD_POLICY] = function(p) { this[_POLICY] = p; }
	o[GET_POLICY] = function() { return this[_POLICY]; }
	o[IS_INSURED] = function() { return !!this[_POLICY]; }
}

심볼은 항상 고유하므로 믹스인이 Car 클래스의 기능과 충돌할 가능성은 없습니다.
쓰기가 조금 번거로울 수는 있겠지만, 훨씬 안전합니다.
메서드 이름에는 일반적인 문자열을 쓰고 데이터 프로퍼티에는 _POLICY 같은 심볼을 쓰는 절충안을 생각할 수도 있습니다.