[코어 자바스크립트] 07 클래스

백은진·2021년 3월 4일
3

07 클래스

챕터 목표: 클래스 문법을 이해하고, 구현 방식을 익힌다.

자바스크립트는 프로토타입 기반 언어라서 '상속' 개념이 없다. 여러가지 다른 언어들은 클래스를 기반으로 하기에, 자바스크립트도 클래스와 비슷하게 동작하게끔 흉내 내는 기법들이 만들어졌고, 니즈가 상승함에 따라 ES6에는 클래스 문법이 공식적으로 추가되었다.
(ES6의 클래스에서도 프로토타입을 일정 부분에서 활용하고 있기 때문에, ES5 체제 하에서 클래스를 흉내내기 위한 구현 방식을 학습하는 것이 여전히 중요하다.)


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

클래스: 공통 요소를 지니는 집단을 분류하기 위한 개념 (추상적 혹은 구체적)
인스턴스: 어떤 클래스의 속성을 지니는 실존하는 개체 (구체적)

클래스는 상위와 하위로 나뉘어질 수 있다.
음식을 예로 들면 다음과 같다.

음식 > 고기, 채소 과일 > ...
과일 > 포도, 자몽, 오렌지, 딸기

클래스

음식이나 과일은 어떤 사물들의 공통 속성을 모아 정의한 개념이어서, 직접 만지거나 볼 수 없는 추상적인 개념이다.(클래스)

또한 음식은 과일과의 관계에서 상위(superior)의 개념이고, 과일은 음식과의 관계에서 하위(subordinate)의 개념이다. 영어단어에서 prefix를 떼어 상위클래스를 superclass, 하위클래스를 subclass로 표현한다.

음식은 과일의 상위클래스이다. 과일은 음식의 하위클래스면서 귤류의 상위클래스다. 귤류는 과일의 하위클래스다.

하위 개념은 상위 개념을 포함하면서 더욱 구체적인 개념이 추가된다.
예를 들어, 최상위 분류인 음식 클래스는 '먹을 수 있다' 정도라면, 하위의 과일 클래스는 '먹을 수 있다 + 나무에서 열린다'가 되고, 그 하위의 귤류 클래스는 '먹을 수 있다 + 나무에서 열린다 + 말랑한 껍질 속에 달고 신맛이 나는 과육이 들어있다'가 된다.

즉, 클래스는 하위로 갈수록 상위클래스의 속성을 상속하면서 더 구체적인 요건이 추가되거나 변경된다. (물론 하위클래스가 아무리 구체화되더라도 이들은 결국 추상적인 개념이다.)

인스턴스

감귤, 자몽, 천혜향 등은 음식에 속해 먹을 수 있고, 과일에 속해 나무에서 열리며, 귤류에 속해 '말랑한 껍질 속에 달고 신맛이 나는 과육이 들어있'는 구체적인 개체이다. 앞의 클래스들의 속성을 지니면서 실제로 먹을 수 있고 만질 수 있는 실존하는 개체이다.

현실과 프로그래밍에서의 접근 방식

현실 세계에서는 개체들이 이미 존재하는 상태에서 이들을 구분하기 위해 클래스 개념을 도입한다. 이미 존재하는 개체를 성질에 따라 분류해서 다양한 클래스가 생성되기 때문에 하나의 개체가 같은 레벨에 있는 서로 다른 여러 클래스의 인스턴스일 수 있다.

반면, 프로그래밍 언어 상에서는 접근 방식이 반대이다. 사용자가 먼저 여러 가지 클래스를 정의해야 하고, 클래스를 바탕으로 인스턴스를 만들 떄 비로소 어떤 개체가 클래스의 속성을 지니게 된다.
또한 한 인스턴스는 하나의 클래스만을 바탕으로 만들어진다. 다중상속을 지원하는 언어이더라도 결국 인스턴스를 생성할 때 호출할 수 있는 클래스는 오직 하나뿐이기 때문이다.

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


2. 자바스크립트의 클래스

자바스크립트는 프로토타입 기반 언어이므로 클래스의 개념이 존재하지 않는다. 그렇지만 프로토타입을 일반적인 의미에서의 클래스 관점에서 접근해보면 비슷하게 해석할 수도 있다.

예를 들어, 생성자 함수 Array를 new 연산자와 함께 호출하면 인스턴스가 생성된다. 이때 Array를 일종의 클래스라고 하면, Array의 prototype 객체 내부 요소들이 인스턴스에 상속된다고 볼 수 있다. 엄밀히는 상속이 아닌 프로토타입 체이닝에 의한 참조지만, 결과적으로는 동일하게 동작하므로 상속이라고 이해해도 무방하다. 한편 Array 내부 프로퍼티들 중 prototype 프로퍼티를 제외한 나머지는 인스턴스에 상속되지 않는다.

인스턴스에 상속되는지 (인스턴스가 참조하는지) 여부에 따라 static member와 instance member로 나뉜다. 이 분류는 다른 언어의 클래스 구성요소에 대한 정의를 차용한 것으로서 클래스 입장에서 사용 대상에 따라 구분한 것이다. 그런데 자바스크립트에서는 인스턴스에서도 직접 메서드를 정의할 수 있기 때문에 '인스턴스 메서드'라는 명칭은 혼란스러움을 야기할 수 있다. (프로토타입에 정의한 메서드를 지칭하는 건지, 인스턴스에 정의한 메서드를 지칭하는 건지 등) 따라서 위 명칭 대신에 '프로토타입 메서드'라고 부르는 편이 더 좋을 수 있다.

예제 1 ) static method, prototype method


// 생성자
var Rectangle = function (width, height) {
  this.width = width;
  this.height = height;
};

// prototype method
Rectangle.prototype.getArea = function () {
  return this.width * this.height;
};

// static method
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(rect1.isRectangle(rect1));  // error
console.log(Rectangle.isRectangle(rect1));  // true

프로토타입 객체에 할당한 메서드는 인스턴스가 마치 자신의 것처럼 호출할 수 있다. 따라서 rect1.getArea()Rectangle.prototype.getArea을 실행시킨다. 이처럼 인스턴스에서 직접 호출할 수 있는 메서드가 프로토타입 메서드이다.

이와 다르게 rect1.isRectangle(rect1)은 접근이 불가능하여 에러를 발생시킨다. 이처럼 인스턴스에서 직접 접근할 수 없는 메서드가 스태틱 메서드이다. 스태틱 메서드는 Rectangle.isRectangle(rect1)처럼 생성자 함수를 this로 지정해야만 호출할 수 있다.

추신 )

'프로그래밍 언어에서의 클래스는 사용하기에 따라 추상적일 수도 있고 구체적인 개체가 될 수도 있다'는 의미를 조금 더 풀어보자.
(일반적으로 사용하는 것과 같이) 구체적인 인스턴스가 사용할 메서드를 정의한 '틀'의 역할을 담당하는 목적을 가질 때의 클래스는 추상적인 개념이지만, 클래스 자체를 this로 하여 직접 접근해야만 하는 스태틱 메서드를 호출할 때의 클래스는 그 자체가 하나의 개체로서 취급된다.


3. 클래스 상속

3-1. 기본 구현

이번 구간 목표:
클래스 상속은 객체지향에서 가장 중요한 요소 중 하나이다. 때문에 ES5까지의 자바스크립트 커뮤니티에서는 클래스 상속을 다른 객체지향 언어에 익숙한 개발자들에게 최대한 친숙한 형태로 흉내 내는 것이 주요한 관심사였다. 이번 절에서는 프로토타입 체인을 활용해 클래스 상속을 구현하고 최대한 전통적인 객체지향 언어에서의 클래스와 비슷한 형태로까지 발전시켜 보는 것을 목표로 한다.

예제 2 )

6과의 2-4 절에서 살펴본 다중 프로토타입 체인이 바로 클래스 상속의 핵심이다.

// Grade 생성자 함수 및 인스턴스

var Grade = function () {
  var args = Array.prototype.slice.call(arguments);
  for (var i = 0; i < args.length; i++) {
    this[i] = args[i];
  }
  this.length = args.length;
};

Grade.prototype = [];
var g = new Grade(100, 80);

자바스크립트에서 클래스 상속을 구현했다는 것은 결국 프로토타입 체이닝을 잘 연결한 것과 같다. 다만, 아무리 체이닝을 잘 연결했다고 하더라도 세부적으로 완벽하게 superclass와 subclass의 구현이 이뤄진 것은 아니다.

예를 들면 예제 2에서는 length 프로퍼티가 configurable (삭제 가능)하다는 점과, Grade.prototype에 빈 배열을 참조시켰다는 점이 큰 문제이다.

예제 3) length 문제 - length 프로퍼티를 삭제한 경우

...

g.push(90);
console.log(g);  
/* Array {
  '0': 100,
  '1': 80,
  '2': 90,
  length: 3,
  __proto__: { length: 3 }
} */

delete g.length;
g.push(70);
console.log(g);
/* Array {
  '0': 70,
  '1': 80,
  '2': 90,
  length: 1,
  __proto__: { length: 1 }
} */

length 프로퍼티를 삭제하고 다시 pus를 하니, push한 값이 0번째 인덱스에 들어가고 length는 1이 되었다.

내장객체인 배열 인스턴스의 length 프로퍼티는 configurable 속성이 false라서 삭제가 불가능한 반면, Grade 클래스의 인스턴스는 배열 메서드를 상속하지만 기본적으로 일반 객체의 성질을 그대로 지니므로 삭제가 가능해서 문제가 된다.

따라서 length 값을 삭제한 후에 g.__proto__Grade.prototype이 빈 배열을 가리키고 있기 때문에 자바스크립트가 push 명령에 의해 g.length를 읽고자 했을 때 g.length가 없었고, 자바스크립트는 프로토타입 체이닝을 타고
g.__proto__.length를 읽어왔다.

이처럼 ES5까지의 클래스 상속 구현은 문제가 발생할 여지가 있었고, 이에 ES6에서는 본격적으로 클래스 문법이 도입되었다.


짚고 넘어가기 )

클래스의 추상성을 지키는 게 중요한 이유는 상위클래스의 데이터가 오염되면, 상위클래스의 공통성질을 참조하고 있는 다른 수많은 하위클래스까지 데이터 오염이 일어나기 때문이다.

추상성:

  • 본질적, 보편적, 관념적 성질. 즉, 공통성질.

추상화:

  • 개별의 사물이나 표상의 공통된 속성이나 관계 따위를 뽑아내는 것.
  • 복잡한 자료, 모듈, 시스템 등으로부터 핵심적인 개념 또는 기능을 간추려 내는 것.
  • 공통부분을 상위 클래스/인터페이스로 모으는 일.
  • 모듈의 기능을 쉽게 이용할 수 있도록 단순화하는 일.

4. ES6의 클래스 및 클래스 상속

ES6에서는 본격적으로 클래스 문법이 도입되었다.


var ES5 = function(name) {
  console.log(name)  // 'es5'
  this.name = name;
};

ES5.staticMethod = function () {
  console.log(this.name)  // 'ES5'
  return this.name + ' staticMethod';
};

ES5.prototype.method = function () {
  return this.name + ' method';
};

var es5Instance = new ES5('es5');
console.log(ES5.staticMethod());  // 'ES5 staticMethod'
console.log(es5Instance.method());  // 'es5 method'

------------------------------------------------

var ES6 = class {
  constuctor (name) {
    this.name = name;
  } 
  
  static staticMethod () {
    console.log(this.name);  // 'ES6'
    return this.name + ' staticMethod';
  }
  
  method () {
    console.log(this.name);  // undefined
    return this.name + ' method';
  }
};

var es6Instance = new ES6('es6');
console.log(ES6.staticMethod());  // 'ES6 staticMethod'
console.log(es6Instance.method());  // 'undefined method'

ES6의 constructor는 ES5에서의 생성자 함수와 동일한 역할을 수행한다.

static 키워드는 해당 메서드가 static 메서드임을 알리는 내용으로, 생성자 함수(클래스) 자신만이 호출할 수 있다.

method는 자동으로 prototype 객체 내부에 할당되는 메서드이다. 인스턴스가 프로토타입 체이닝을 통해 마치 자신의 것처럼 호출할 수 있는 메서드이다.

클래스 상속 )

예 )

var Rectangle = class {
  constructor (width, height) {
    this.width = width;
    this.height = height;
  }
  
  getArea () {
    return this.width * this.height;
  }
};

var Square = class extends Rectangle {
  constructor (width) {
    super (width, width);
  }
  
 getArea () {
    console.log('Size is : ' + super.getArea());
  } 
};

const square = new Square(5);
square.getArea();  // 'Size is : 25'

Square를 Rectangle 클래스를 상속받는 하위클래스로 만들기 위해 class 명령어 뒤에 단순히 'extends Rectagle'이라는 내용을 추가하여 상속 관계를 설정했다.

하위 클래스 내부에서 super 키워드는 함수처럼 사용되며, super는 Superclass의 constuctor를 실행한다.

constructor 메서드를 제외한 다른 메서드에서는 super 키워드를 마치 객체처럼 사용할 수 있고 이때 super는 SuperClass.prototype을 바라보는데, 호출한 메서드의 this는 'super'가 아닌 원래(SuperClass)의 this를 그대로 따른다.


5. 정리

  • 클래스는 어떤 사물의 공통 속성을 모아 정의한 추상적인 개념이고, 인스턴스는 클래스의 속성을 지니는 구체적인 사례이다.

  • superclass의 조건을 충족하면서 더욱 구체적인 조건이 추가된 것을 subclass라고 한다.

  • 클래스의 prorotype 내부에 정의된 메서드를 프로토타입 메서드라고 하며, 이들은 인스턴스가 자기것처럼 호출할 수 있다.
    클래스(생성자 함수)에 직접 정의한 메서드를 스태틱 메서드라고 하며, 이들은 클래스에 의해서만 호출할 수 있다.

  • ES6 이전에 클래스 상속을 흉내내는 방법은 다음과 같다.

    1. SubClass.prototype에 SuperClass의 인스턴스를 할당한 다음 프로퍼티를 모두 삭제하는 방법
    2. 빈 함수(Bridge)를 활용하는 방법
    3. Object.create를 이용하는 방법
  • 세 방법 모두 constructor 프로퍼티가 원래의 생성자 함수를 바라보도록 조정해야 한다.

  • super는 상위클래스에 접근할 수 있는 수단이다.

profile
💡 Software Engineer - F.E

0개의 댓글