Ch4. 클래스와 객체의 혼합
- 클래스 지향 개념은 자바스크립트 객체 체계와는 태생부터 잘 맞지 않아 지금껏 수많은 개발자가 이런 한계를 극복하고 기능을 확장하고자 노력해왔다.
4.1 클래스 이론
- 클래스와 상속은 특정 형태의 코드와 구조를 형성하며 실생활 영역의 문제를 소프트웨어로 모델링 하기 위한 방법이다. 데이터는 자신을 기반으로 하는 실행되는 작동과 연관되므로 데이터와 작동을 함께 잘 감싸는 것이 올바른 설계라고 강조한다. 컴퓨터 과학에서는 이를 자료 구조라고 표현하기도 한다.
- 클래스는 특정 자료 구조를 분류하는 용도로 쓴다.
- 다형성은 또 다른 클래스의 핵심 개념으로 부모 클래스에 뭉뚱그려 정의된 작동을 자식 클래스에서 좀 더 구체화하여 오버라이드하는 것을 뜻한다.
4.1.1 클래스 디자인 패턴
- 클래스도 순회자, 관찰자, 팩토리, 싱글턴같은 디자인 패턴 중 하나다. 자바 같은 경우는 만물이 클래스다. C/C++이나 PHP는 절차적 구문과 클래스 지향 구문을 함께 제공하므로 개발자가 스타일을 선택할 수 있다.
4.1.2 자바스크립트 클래스
- 자바스크립트에는 클래스가 없다. 그동안 개발자들이 클래스의 개념을 자바스크립트에서 구현하려고 노력하고 실제로 그렇게도 사용했지만, 그것은 클래스 디자인 패턴으로 코딩할 수 있도록 자바스크립트 체계를 억지로 고친 것에 불과하다. 실제로는 전혀 다른 방식으로 작동한다.
- 위에서 말했듯이, 클래스는 소프트웨어 디자인 패턴 중 한 가지 옵션일 뿐이니 자바스크립트에서 클래스를 쓸지 말지는 결국 자신이 결정할 문제라는 것이다.
4.2 클래스 체계
- 대부분 클래스 지향 언어의 표준 라이브러리는 스택 자료구조를 Stack 클래스에 구현해놨다. 하지만 Stack 클래스에서 실제로 어떤 작업을 수행하는 것은 아니다. Stack 클래스를 인스턴스화해야 비로소 작업을 수행할 구체적인 자료 구조가 마련된다.
4.2.1 건축
- 건축 분야에서 청사진은 건축을 위한 계획일 뿐, 실제로 이 자체가 건물이 되진 않는다. 건물을 올리는 작업은 시공사에 의뢰를 해야하는데, 시공사는 청사진에 따라 건물을 짓는다. 완공된 건물이 청사진의 물리적인 인스턴스다.
- 클래스가 바로 청사진에 해당한다. 개발자가 상호 작용할 실제 객체는 클래스에서 인스턴스화한다. 객체는 클래스에 기술된 모든 특성을 그대로 가진 사본이다. 클래스는 복사 과정을 거쳐 객체 형태로 인스턴스화한다.
4.2.2 생성자
- 인스턴스는 보통 클래스명과 같은 이름의 생성자라는 특별한 메서드로 생성한다. 생성자의 임무는 인스턴스에 필요한 정보를 초기화하는 일이다.
class CoolGuy {
specialTrick = nothing
CoolGuy(trick) {
specialTrick = trick
}
showOff() {
output("이게 내 장기다", specialTrick)
}
}
// 인스턴스 생성
Joe = new CoolGuy("카드 마술")
Joe.showOff() // 이게 내 장기다, 카드 마술
- new CoolGuy로 이 생성자가 호출된다. 생성자의 반환 값은 객체이고 showOff 메서드를 호출할 수 있다.
- 생성자는 클래스에 속한 메서드로, 클래스명과 같게 명명하는 것이 일반적이다. 새로운 인스턴스를 생성할 시에는 new 키워드를 앞에 붙여서 생성자를 호출한다.
4.3 클래스 상속
- 클래스 지향 언어에서는 첫번째 클래스를 상속받은 두 번째 클래스를 정의할 수 있다. 이 때 첫 번째 클래스를 '부모 클래스', 두 번째 클래스를 '자식 클래스' 라고 부른다.
- 부모와 자식이 다른 개체인 것처럼 실제로 자식 클래스는 부모와 별개의 클래스로 정의된다. 부모로부터 복사된 초기 버전의 작동을 고스란히 간직하고 있지만 물려받은 작동을 전혀 새로운 방식으로 오버라이드할 수 있다.
class Vehicle {
engines = 1
ignition() {
output('엔진을 켠다')
}
drive() {
ignition();
output('방향을 맞추고 앞으로 간다!')
}
}
class Car inherits Vehicle {
wheels = 4
drive() {
inherited:drive();
output(wheels, '개의 바퀴로 굴러간다!')
}
}
class SpeedBoat inherits Vehicle {
engines = 2
ignition() {
output(engines, '개의 엔진을 켠다.')
}
pilot() {
inherited:drive()
output('물살을 가르며 쾌속으로 질주한다!')
}
}
- Vehicle 클래스에는 엔진 하나와 시동 거는 방법, 주행 방법이 정의되어 있다. 하지만 내용은 추상적인 개념이다. 그래서 구체적인 탈것인 Car, Speedboat를 정의한다. Vehicle의 일반적인 특성을 물려받아 각자에게 맞는 특성을 세분화한다.
4.3.1 다형성
- 다형성은 이 책에서 다루기엔 지나치게 광범위한 주제다. 대부분의 언어에서는 위 코드의 inherited 대신 super라는 키워드를 사용하며, 이는 superclass를 현재 클래스의 부모/조상이라고 간주하는 것이다.
- 위 코드에서 Car는 Vehicle로부터 상속받은 drive() 메서드를 같은 명칭의 자체 메서드로 오버라이드한다. 그러나 이 메서드 안에서 inherited:drive() 호출은 Vehicle로부터 상속받아 오버라이드 전의 원본을 참조하고, SpeedBoat의 pilot() 메서드도 상속받은 원본 drive()를 참조한다. 이런 기법을 다형성이라고 한다.
- 같은 이름의 메서드가 상속 연쇄의 수준별로 다르게 구현되어 있고 이 중 어떤 메서드가 적절한 호출 대상인지 자동으로 선택하는 것 또한 다형성의 특징이다.
- ignition() 메서드에 흥미로운 내용이 있는데, pilot함수 내 drive함수를 호출하는데, 내부에 ignition 메서드를 호출한다. 이 때 자바스크립트 엔진은 Vehicle과 SpeedBoat 중 어느 쪽을 실행할 것인가 문제인데, 바로 인스턴스가 어느 클래스를 참조하느냐에 따라 달라진다. 즉, 어느 클래스를 참조하느냐에 따라 ignition() 메서드의 정의는 다형적이다. (모습이 변한다는 뜻이다.)
- 클래스를 상속하면 자식 클래스에는 자신의 부모 클래스를 가리키는 상대적 레퍼런스가 주어지는데, 바로 이 레퍼런스를 보통 super라고 한다.
- 자식 클래스가 마치 부모 클래스에 연결된 양 다형성을 혼동하지 말아야 한다. 클래스 상속은 한 마디로 '복사'다.
4.3.2 다중 상속
- 일부 클래스 지향 언어에서는 다중 상속이 가능하다. 다중 상속은 부모 클래스 각각의 정의가 자식 클래스로 복사된다는 의미이다. 하지만 다중 상속은 아주 골치 아픈 복잡한 문제들이 잠재되어 있다.
- 자바스크립트는 다중 상속을 지원하지 않는다.
4.4 믹스인
- 자바스크립트 객체는 상속받거나 인스턴스화해도 자동으로 복사 작업이 일어나지는 않는다. 자바스크립트는 클래스 개념 자체가 없고, 오직 객체만 있다. 그리고 객체는 다른 객체에 복사되는 게 아니라 서로 연결된다. (자세한 내용은 5장 프로토타입에 있음)
- 믹스인은 클래스 복사 기능을 흉내 낸 것이다.
4.4.1 명시적 믹스인
- 자바스크립트 엔진은 Vehicle의 작동을 Car로 알아서 복사하지 않으므로 수동으로 복사하는 유틸리티를 작성할 수 있다.
function mixin(sourceObj, targetObj) {
for (var key in sourceObj) {
if (!(key in targetObj)) {
targetObj[key] = sourceObj[key]
}
}
return targetObj
}
var Vehicle = {
engines: 1,
ignition: function () {
console.log("엔진을 켠다")
},
drive: function () {
this.ignition()
console.log("방향을 맞추고 앞으로 간다")
},
}
var Car = mixin(Vehicle, {
wheels: 4,
drive: function () {
Vehicle.drive.call(this)
console.log(this.wheels + "개의 바퀴로 돌아간다")
},
})
- 위 코드를 결과를 보면 Car에는 ignition 함수의 사본 레퍼런스와 engines 프로퍼티가 있다. drive 메서드는 이미 존재하므로 이 프로퍼티는 오버라이드 되지 않는다.
다형성 재고
- 자바스크립트는 상대적 다형성을 제공하지 않는다. 따라서 drive 메서드가 양쪽에 모두 있을때 이 둘을 구별해서 호출하려면 절대적인 레퍼런스를 이용할 수 밖에 없고 명시적으로 Vehicle 객체의 이름을 지정해서 호출한 것이다.
- 상대적 다형성을 제공하는 클래스 지향 언어에서는 클래스가 정의되는 시점에 일단 Vehicle과 Car가 연결되면 이러한 관계를 모두 한곳에서 취합하여 관리한다.
- 하지만 자바스크립트는 다형적 레퍼런스가 필요한 함수마다 명시적으로 일일히 만들어줄 수 밖에 없다. 추가로 다중 상속 작동 방식을 모방할 수 있어 복잡도와 취약성은 한층 가중된다.
- 명시적 의사다형성은 장점보다는 비용이 훨씬 더 많이 들기 때문에 가능한 한 쓰지 않는 게 좋다.
사본 혼합
- 복사가 끝나면 Car는 Vehicle과 별개로 움직인다. 다만 객체간에 함수를 일일히 복사하더라도 다른 클래스 지향 언어처럼 100% 복사는 어렵다.
- 자바스크립트 함수는 복사할 수 없다. 복사되는 것은 함수를 가리키는 사본 레퍼런스다.
- 명시적 믹스인은 코드 가독성에 도움이 될 때만 조심하여 사용하되 점점 코드가 추적하기 어려워지거나 불필요하고 난해한 객체 간 의존 관계가 양산될 기미가 보이면 사용을 중단하기 바란다.
- 더 간단한 사용 방법은 6장 작동 위임에서 소개한다.
기생 상속
- 더글러스 크록포드가 작성한 명시적 믹스인 패턴의 변형으로 명시적/암시적 특징을 모두 갖고 있다.
function Vehicle() {
this.engines = 1
}
Vehicle.prototype.ignition = function () {
console.log("엔진을 켠다")
}
Vehicle.prototype.drive = function () {
this.ignition()
console.log("방향을 맞추고 앞으로 간다")
}
function Car() {
var car = new Vehicle()
car.wheels = 4
var vehDrive = car.drive
car.drive = function () {
vehDrive.call(this)
console.log(this.wheels + "개의 바퀴로 굴러간다!")
}
return car
}
var myCar = new Car()
myCar.drive()
// 엔진을 켠다
// 방향을 맞추고 앞으로 간다!
// 4개의 바퀴로 굴러간다!
4.4.2 암시적 믹스인