LHJ

I'm a FE developer.

9.2.1 클래스와 인스턴스 생성

10 May 2020 » js_lj

ES6 이전에 자바스크립트에서 클래스를 만드는 건 직관적이지도 않고 무척 번거로운 일이었습니다.
ES6에서는 클래스를 만드는 간편한 새 문법을 도입했습니다.

class Car {
	constructor() {
	
	}
}

위 코드는 새 클래스 Car를 만듭니다.
아직 인스턴스(특정 자동차)는 만들어지지 않았지만 언제든 만들 수 있습니다.
인스턴스를 만들 때는 new 키워드를 사용합니다.

class Car {
	constructor() {
	
	}
}

const car1 = new Car();
const car2 = new Car();

이제 Car 클래스의 인스턴스가 두 개 생겼습니다.
Car 클래스를 더 수정하기 전에, 객체가 클래스의 인스턴스인지 확인하는 instanceof 연산자에 대해 알아봅시다.

class Car {
	constructor() {
	
	}
}

const car1 = new Car();
const car2 = new Car();

car1 instanceof Car // true
car1 instanceof Array // false

이 예제를 보면 car1은 Car의 인스턴스이고 Array의 인스턴스는 아님을 알 수 있습니다.

Car 클래스를 조금 더 흥미롭게 만들어 봅시다.
제조사(make)와 모델 데이터, 변속(shift) 기능을 추가할 겁니다.

class Car {
	constructor(make, model) {
		this.make = make;
		this.model = model;
		this.userGears = ['P', 'N', 'R', 'D'];
		this.userGear = this.userGears[0];
	}
	shift(gear) {
		if(this.userGears.indexOf(gear) < 0)
			throw new Error(`Invalid gear: ${gear}`);
		this.userGear = gear;
	}
}

여기서 this 키워드는 의도한 목적, 즉 메서드를 호출한 인스턴스를 가리키는 목적으로 쓰였습니다.
this를 일종의 플레이스홀더로 생각해도 좋습니다.
클래스를 만들 때 사용한 this 키워드는 나중에 만들 인스턴스의 플레이스홀더입니다.
메서드를 호출하는 시점에서 this가 무엇인지 알 수 있게 됩니다.
이 생성자를 실행하면 인스턴스를 만들면서 자동차의 제조사와 모델을 지정할 수 있고, 몇 가지 기본값도 있습니다.
userGears는 사용할 수 있는 기어 목록이고 gear는 현재 기어이며 사용할 수 있는 첫 번째 기어로 초기화됩니다.
생성자 외에 shift 메서드도 만들었습니다.
이 메서드는 기어 변속에 사용됩니다.
이제 이 클래스를 실제로 사용해 봅시다.

class Car {
	constructor(make, model) {
		this.make = make;
		this.model = model;
		this.userGears = ['P', 'N', 'R', 'D'];
		this.userGear = this.userGears[0];
	}
	shift(gear) {
		if(this.userGears.indexOf(gear) < 0)
			throw new Error(`Invalid gear: ${gear}`);
		this.userGear = gear;
	}
}

const car1 = new Car("Tesla", "Model S");
const car2 = new Car("Mazda", "3i");
car1.shift('D');
car2.shift('R');

이 예제에서 car1.shif(‘D’) 를 호출하면 this는 car1에 묶입니다.
마찬가지로 car2.shift(‘R’) 를 호출하면 this는 car2에 묶입니다.
다음과 같이 car1이 주행 중이고(D) car2가 후진 중임을(R) 확인할 수 있습니다.

car1.userGear // "D"
car2.userGear // "R"

Car 클래스에 shift 메서드를 사용하면 잘못된 기어를 선택하는 실수를 방지할 수 있을 것처럼 보입니다.
하지만 완벽하게 보호되는 건 아닙니다.
직접 car1.userGear = ‘X’ 라고 설정한다면 막을 수 없습니다.
대부분의 객체지향 언어에서는 메서드와 프로퍼티에 어느 수준까지 접근할 수 있는지 대단히 세밀하게 설정할 수 있는 메커니즘을 제공해서 car1.userGear = ‘X’ 같은 실수를 막을 수 있게 합니다.
하지만 자바스크립트에는 그런 메커니즘이 없고, 이는 언어의 문제로 자주 비판을 받습니다.

프로퍼티 직접 수정 방지

Car 클래스를 다음과 같이 수정하면 실수로 기어 프로퍼티를 고치지 않도록 어느 정도 막을 수 있습니다.

class Car {
	constructor(make, model) {
		this.make = make;
		this.model = model;
		this._userGears = ['P', 'N', 'R', 'D'];
		this._userGear = this._userGears[0];
	}
	
	get userGear() { return this._userGear; }
	set userGear(value) {
		if (this._userGears.indexOf(value) < 0)
			throw new Error(`Invalid gear: ${value}`);
		this._userGear = value;
	}
	
	shift(gear) {this.userGear = gear;}
}

예민한 독자라면 여전히 car1._userGear = ‘X’ 처럼 _userGear를 직접 바꿀 수 있다고 지적할 겁니다.
이 예제에서는 외부에서 접근하면 안 되는 프로퍼티 이름 앞에 밑줄을 붙이는, 소위 ‘가짜 접근 제한’을 사용했습니다.
진정한 제한이라기보다는 “아, 밑줄이 붙은 프로퍼티에 접근하려고 하네? 이건 실수로군.” 하면서 빨리 찾을 수 있도록 하는 방편이라고 봐야 합니다.

프로퍼티를 꼭 보호해야 한다면 스코프를 이용해 보호하는 WeakMap 인스턴스(10장에서 설명합니다)를 사용할 수 있습니다.
Car 클래스를 다음과 같이 고치면 기어 프로퍼티를 완벽하게 보호할 수 있습니다.

const Car = (function() {
	
	const carProps = new WeakMap();
	
	class Car {
		constructor (make, model) {
			this.make = make;
			this.model = model;
			this._userGears = ['P', 'N', 'R', 'D'];
			carProps.set(this, { userGear: this._userGears[0] });
		}
		
		get userGear() { return carProps.get(this).userGear; }
		set userGear(value) {
			if(this._userGears.indexOf(value) < 0)
				throw new Error(`Invalid gear: ${value}`);
			carProps.get(this).userGear = value;
		}
		
		shift(gear) { this.userGear = gear; }
	}
	
	return Car;
})();

여기서는 즉시 호출하는 함수 표현식을 써서 WeakMap을 클로저로 감싸고 바깥에서 접근할 수 없게 했습니다.
WeakMap은 클래스 외부에서 접근하면 안 되는 프로퍼티를 안전하게 저장합니다.

프로퍼티 이름에 심볼을 쓰는 방법도 있습니다.
이렇게 해도 어느 정도는 보호할 수 있지만, 클래스에 들어 있는 심볼 프로퍼티 역시 접근이 불가능한 것은 아니므로 이 방법에도 한계가 있다고 해야 합니다.