munifico.github.io

munifico's anything page

Follow me on GitHub

클래스와 인스턴스 생성

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

class Car {
    construnctor() {
    }
}

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

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

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

car instanceof Car      // true
car 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 메서드도 만들었습니다. 이 메서드는 기어 변속에 사용됩니다. 이제 이 클래스를 실제로 사용해 봅시다.

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

/*
car1 : {
  make: 'Tesla',
  model: 'Model S',
  userGears: [ 'P', 'N', 'R', 'D' ],
  userGear: 'D' 
}
car2 : {
  make: 'Mazda',
  model: '3i',
  userGears: [ 'P', 'N', 'R', 'D' ],
  userGear: 'R' 
}
*/

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

Car클래스에 shift 메서드를 사용하면 잘못된 기어를 선택할 수 있는 실수를 방지할 수 있을 것 처럼 보입니다. 하지만 완벽하게 보호되는 건 아닙니다. 직접 car1.userGear = ‘X’라고 설정한다면 막을 수 없습니다.

car1.userGear = 'X'
/*
car1 : {
  make: 'Tesla',
  model: 'Model S',
  userGears: [ 'P', 'N', 'R', 'D' ],
  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 인스턴스를 사용할 수 있습니다. 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;
})();

/*
car1 : {
  make: 'Tesla',
  model: 'Model S',
  _userGears: [ 'P', 'N', 'R', 'D' ] 
}
car2 : {
  make: 'Mazda',
  model: '3i',
  _userGears: [ 'P', 'N', 'R', 'D' ] 
}

car1.userGear : D
car2.userGear : R

car1.userGear = 'X' => Invalid gear : X
*/

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

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

// Symbol을 사용해봄. 틀리면 컨펌 요청드립니다.
const Car = (function() {
    const userGear = Symbol();

    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; }
    }

    return Car;
})();

/*
car1.shift('D');
car2.shift('R');

car1 : {
  make: 'Tesla',
  model: 'Model S',
  _userGears: [ 'P', 'N', 'R', 'D' ],
  [Symbol()]: 'D' 
}
car2 : {
  make: 'Tesla',
  model: 'Model S',
  _userGears: [ 'P', 'N', 'R', 'D' ],
  [Symbol()]: 'R' 
}
*/

이전 : 객체지향 프로그래밍
다음 : 클래스는 함수다
목차