프로그래밍-학습기록/Javascript

[Javascript] 프로토타입으로 클래스 구현하기

leesche 2021. 2. 17. 17:53

프로토타입으로 클래스 구현하기

자바스크립트에는 '상속' 개념이 존재하지 않습니다. 하지만 클래스 기반의 다른 언어에 익숙한 개발자들의 니즈에 따라 ES6에는 클래스 문법이 추가됐습니다. 하지만 ES6 클래스에서 일정 부분은 프로토타입을 활용하고 있기 때문에, ES5 체제 하에서 클래스를 흉내내기 위한 구현 방식을 학습하는 것은 큰 의미가 있습니다.

클래스와 인스턴스의 개념 이해

상위의 개념과 하위의 개념이 있습니다. 개념을 어떤 '집단'으로 표현하면, 영어로 그것은 클래스라고도 할 수 있습니다. 다시 말해, 상위 클래스와 하위 클래스가 있습니다. 하위 클래스는 상위 클래스를 포함하면서 더 구체적인 내용(개념)이 추가됩니다.

여기서 클래스의 예는 '음식'이고 그 하위 개념(클래스)는 '과일'이며, 또 그 하위 클래스는 '귤 같은 것'입니다. 과일은 음식의 속성을 '상속' 받습니다. 즉, 과일은 음식입니다. 다시 말해, '먹을 수 있습니다'. 같은 맥락으로 '귤 같은 것'은 '과일'이며 과일의 속성을 갖습니다.

마지막으로 강조할 것은, 이 모든 클래스는 실제로 존재하는 구체적인 어떤 것이 아니라는 점입니다. 클래스는 추상적인 개념입니다.

한편 제주 한라봉, 낑깡, 유자는 모두 실제로 있는 개체들입니다. '음식'이고, '과일'이며, '귤 같은 것'입니다. 이처럼 어떤 클래스의 속성을 지니는 실존하는 개체를 일컬어 인스턴스(instance)라고 합니다.

프로그래밍 언어에서 클래스

프로그래밍 언어 상에서는 위와 같은 구분법을 알지 못하므로 사용자가 직접 여러 가지 클래스를 정의해야 하며, 클래스를 바탕으로 인스턴스를 만들 때 비로소 어떤 개체가 클래스의 속성을 지니게 됩니다.

또한 한 인스턴스는 하나의 클래스만을 바탕으로 만들어집니다. 어떤 인스턴스가 다양한 클래스에 속할 수는 있지만 이 클래스들은 모두 인스턴스 입장에서는 '직계존속'입니다. 다중 상속을 지원하는 언어이든 그렇지 않은 언어이든 결국 인스턴스를 생성할 때 호출할 수 있는 클래스는 오직 하나 뿐일 수밖에 없기 때문입니다.

따라서 프로그래밍 언어에서 클래스는 현실 세계에서의 클래스와 마찬가지로 '공통 요소를 지니는 집단을 분류하기 위한 개념'이라는 측면에서는 일치하지만 인스턴스들로부터 공통점을 발견해서 클래스를 정의하는 현실과 달리, 클래스가 먼저 정의되어야만 그로부터 공통적인 요소를 지니는 개체들을 생성할 수 있습니다. 나아가 현실세계에서 클래스는 추상적인 개념이지만, 프로그래밍 언어에서 클래스는 사용하기에 따라 추상적인 대상일 수도, 구체적일 개체가 될 수도 있습니다.

자바스크립트의 클래스

자바스크립트는 프로토타입 기반 언어이므로 클래스 개념이 존재하지 않지만, 비슷하게 해석할 수 있는 요소가 없지 않습니다.

예를 들어, Array를 new 연산자와 함께 호출하면 인스턴스가 생성됩니다.

var array = new Array();

이때 Array를 일종의 클래스라고 하면, Array의 prototype 객체 내부 요소들이 인스턴스에 '상속'된다고 볼 수 있습니다. 정확하게는 상속이 아니라 프로토타입 체이닝에 의한 참조입니다. 하지만 Array 내부 프로퍼티들 중 prototype 프로퍼티를 제외한 나머지는 인스턴스에 상속되지 않습니다.

내부 프로퍼티는 인스턴스에 상속되는지, 다시 말해 인스턴스가 생성자 함수의 프로토타입을 참조하는지 여부에 따라 스태틱 멤버(static member)와 인스턴스 멤버(instance member)로 나뉩니다. 하지만 여느 클래스 기반 언어와 달리 자바스크립트에서는 인스턴스에서도 직접 메서드를 정의할 수 있기 때문에 '인스턴스 메서드'라는 명칭은 프로토타입에 정의한 메서드를 지칭하는 것인지, 인스턴스에 정의한 메서드를 지칭하는 것인지 혼란을 야기할 수 있습니다. 따라서 이 명칭(인스턴스 메서드) 대신에 자바스크립트의 특징을 살려 '프로토타입 메서드(prototype method)라고 부르는 편이 더 좋습니다.

예제는 다음과 같습니다.

var Rectangle = function (width, height) {
  // 생성자
  this.width = width;
  this.height = height;
};
Rectangle.prototype.getArea = function () {
  // 프로토타입 메서드
  return this.width * this.height;
};

Rectangle.isRectangle = function (instance) {
  // 스태틱 메서드
  return (
    instance instanceof Rectangle && instance.width > 0 && instance.height > 0
  );
};

var rect1 = new Rectangle(3, 4);
console.log(rect1.getArea()); // 12
console.log(Rectangle.isRectangle(rect1)); // true
console.log(rect1.isRectangle(rect1)); // 오류 --> TypeError: rect1.isRectangle is not a function

클래스 상속

기본 구현

자바스크립트에서 클래스를 구현하기 위해 프로토타입 체이닝 방법을 사용합니다. 생성자의 프로토타입에 상속을 원하는 클래스(생성자)의 인스턴스를 할당하면, 그 클래스의 프로토타입에 접근하여 자신의 것인양 프로퍼티에 접근하여 사용할 수 있었습니다.

프로토타입 체인을 활용한 클래스 상속의 예를 (이어서) 구현해보겠습니다.

var Rectangle = function (width, height) {
  // 생성자
  this.width = width;
  this.height = height;
};
Rectangle.prototype.getArea = function () {
  // 프로토타입 메서드
  return this.width * this.height;
};

Rectangle.isRectangle = function (instance) {
  // 스태틱 메서드
  return (
    instance instanceof Rectangle && instance.width > 0 && instance.height > 0
  );
};

// 정사각형을 위한 생성자
var Square = function (width) {
  // 기존 사각형 생성자를 call 메서드를 통해 호출한다.
  Rectangle.call(this, width, width);
};

Square.prototype = new Rectangle();

var squareInstance = new Square(5);

console.dir(squareInstance);

출력 결과

위의 그림은 Square 생성자를 통해 만든 인스턴스 squareInstance의 구조를 출력한 결과입니다.

첫 줄의 SquaresquareInstance Square의 인스턴스라는 뜻입니다. 그리고 Square의 프로퍼티로 heightwidth5가 할당되어 있습니다.

3번째 줄은 Square.__proto__Rectangle의 인스턴스라는 뜻입니다. heightwidthundefined가 할당되어 있습니다.

여기서 이 클래스 구현 방식에 문제가 있습니다. 인스턴스의 프로퍼티에 heightwidth가 존재해야 맥락상 이치에 맞지만, Square.prototype에 값이 있습니다. 이렇다면 이후에 임의로 Square.prototype.width에 값을 설정하고 squareInstance.width의 값을 삭제한다면, squareInstance.width의 접근 결과는 undefined가 아니라, 프로토타입체이닝에 의해 다른 어떤 엉뚱한 값이 반환될 것입니다.

squareInstance.width 를 삭제했지만 ...

더 나아가 Square의 인스턴스인 squareInstance.contructorRectangle을 가리키고 있는 것도 문제입니다. Square의 인스턴스의 constructorSquare를 가리키도록 해야 합니다.

squareInstance.contructor 가  Rectangle 을 가리키고 있다.

이처럼 하위 클래스로 삼을 생성자 함수의 prototype에 상위 클래스의 인스턴스를 부여하는 것만으로도 기본적인 메서드 상속은 가능하지만, 다양한 문제가 발생할 여지가 있어 구조적으로 안정성이 떨어집니다. 그렇다면 어떻게 해결 해야 할까요?

클래스가 구체적인 데이터를 지니지 않게 하는 방법

  1. 인스턴스 생성 후 프로퍼티 제거

    클래스(프로토타입)가 구체적인 데이터를 지니지 않게 하려면,

    1. 일단 만들고,
    2. 프로퍼티를 일일이 지우고
    3. 더는 새로운 프로퍼티를 추가할 수 없게 클래스를 잠그면 됩니다.
  2. 빈 함수를 활용

    두 번째 방법은 더글라스 크락포드가 제시했습니다.

    1. 서브클래스의 프로토타입에 직접 슈퍼클래스의 인스턴스를 할당하지 않고,
      아무런 프로퍼티를 생성하지 않는, 다리(Bridge) 역할을 하는, 빈 생성자 함수를 만듭니다.

    2. 빈 생성자 함수의 프로토타입이 슈퍼클래스의 프로토타입을 바라보게끔 합니다.

    3. 서브클래스의 프로토타입에 빈 생성자 함수의 인스턴스를 할당합니다.

    4. 빈 생성자 함수(다리)에 서브클래스와 슈퍼클래스가 연결되었습니다.

      var Rectangle = function (width, height) {
        this.width = width;
        this.height = height;
      };
      Rectangle.prototype.getArea = function () {
        return this.width * this.height;
      };
      
      var Square = function (width) {
        Rectangle.call(this, width, width);
      };
      
      // 빈 함수를 활용하는 방법
      var Bridge = function () {};
      Bridge.prototype = Rectangle.prototype;
      Square.prototype = new Bridge();
      Object.freeze(Square.prototype);
      
      var squareInstance = new Square(5);
      
      console.log(squareInstance);

인스턴스를 제외한 프로토타입 체인 경로상에 구체적인 데이터가 남아있지 않게 됐습니다.

  1. 클래스 상속 및 추상화 방법(Object.create) 활용

    Object.create는 ES5에서 도입됐습니다. 이 방법은 서브클래스의 프로토타입의 __proto__가 슈퍼클래스의 프로토타입을 바라보되, 슈퍼클래스의 인스턴스가 되지는 않기 때문에 앞의 방법들 보다 안전합니다.

     // ...
     Square.prototype = Object.create(Rectangle.prototype);
     Object.freeze(Square.prototype);
     // ...

constructor 복구하기

위 세 가지 방법 모두 상속을 구현했습니다. 하지만 여전히 서브클래스 인스턴스의 constructor는 슈퍼클래스를 가리키고 있습니다. 정확하게 말하자면, 인스턴스에 constructor가 정의되어 있지 않으니 프로토타입 체이닝을 통해 슈퍼클래스의 constructor에 접근하게 되는 것입니다.

따라서 위 코드에서 SubClass.prototype.constructor = SubClass; 를 해주시면 됩니다.

정리

엄연히 말하자면, 자바스크립트는 프로토타입 기반 언어라 클래스 및 상속 개념은 존재하지 않습니다. 하지만 프로토타입을 기반으로 클래스와 비슷하게 동작하게끔 하는 다양한 기법들이 도입됐습니다.

클래스는 어떤 사물의 공통 속성을 모아 정의한 추상적인 개념이고, 인스턴스는 클래스의 속성을 지니는 구체적인 사례입니다. 상위 클래스(superclass)의 조건을 충족하면서 더욱 구체적인 조건이 추가된 것을 하위 클래스(subclass)라고 합니다.

클래스의 prototype 내부에 정의된 메서드를 프로토타입 메서드라고 합니다. 이들은 인스턴스가 마치 자신의 것인양 마음대로 호출할 수 있습니다. 한편 클래스(생성자 함수)에 직접 정의한 메서드를 스태딕 메서드라고 합니다. 이들은 인스턴스가 직접 호출할 수 없고 클래스(생성자 함수)에 의해서만 호출할 수 있습니다.

여기서 소개한 클래스 상속을 흉내내기 위한 방법은 세 가지였습니다.

  1. SubClass.prototype에 SuperClass의 인스턴스를 할당한 다음 프로퍼티를 모두 삭제하는 방법
  2. 빈 함수(Bridge)를 활용하는 방법
  3. Object.create를 이용하는 방법

이 세 방법 모두 constructor 프로퍼티가 원래의 생성자 함수를 바라보도록 조정해야 했습니다.

출처 및 참고 문헌

  • 책 <코어 자바스크립트>