JavaScript에서의 OOP, Inheritance와 Prototype Chain과 Class 에 대한 개념 정리 및 이해

박한준·2019년 7월 30일
6

dev foundation

목록 보기
4/5
post-thumbnail

OOP에서의 Inheritance(상속)

일상 생활에서 객체(Object)라는 개념은 매우 흔하다.

저명한 물리학자 리처드 파인만이 인류의 멸망을 앞두고 다음 세대에 한마디를 남긴다면 어떤 말을 남길 것인지에 대한 질문에 다음과 같이 답했다.

"모든 것은 원자로 이루어져 있다."

원자가 모여 어떠한 객체가 되고 또 어떠한 객체들이 모여 더 큰 객체가 된다. 우리 인간만 하더라도 인간이라는 객체 속에 머리, 몸통, 다리 객체가 소속되어 있다.

위의 리차드 파인만의 말은 실제 세계를 구성하는 것들이 모두 객체와의 관계로 이루어져 있다는 말과 같게 들린다.

이런 의미에서 OOP은 실제 세계를 컴퓨터로 이해해보는 노력이라고 생각한다. 프로그래밍의 기반을 객체 단위로 하여 실제 세계가 구동되는 것처럼 프로그래밍도 그와 비슷하게 구동이 되도록 하는 철학?을 가지고 있는 것 같다.

실제 생활에서 객체들은 상위/하위 관계를 가지고 있다.

진돗개라는 객체에 소속되어 있는 객체이며 먼치킨고양이라는 객체에 소속되어 있는 객체이다. 또 진돗개 중에서도 뒷 집의 개똥이진돗개라는 객체에 소속되어 있는 객체이기도 하다.

객체들의 공통적인 특성을 모아 더 큰 객체의 개념이 되고 다른 객체와 확실하게 구분되는 특성을 가지고 있다. 이를 프로그래밍으로 이용을 하자면

  • 코드의 재사용성 제공 : 객체의 특성이나 속성을 한 번 정의해 놓으면 해당 객체에 소속되어 있는 모든 객체들의 속성으로 사용할 수 있으므로 재사용성이 높아진다.
  • 유지보수의 용이 : 한 번만 정의한 것을 수정하면 모든 객체의 속성을 수정할 수 있다.
  • 신뢰성 증대 : 다양하게 사용된 속성들에 경험이 쌓이면서 신뢰가 축적되기 쉽고 그것을 다시 사용한다면 에러(버그)의 가능성이 줄어든다.

객체 간의 상위/하위 관계를 프로그래밍으로 표현할 수 있다.

객체에 대해 일정한 속성, 규칙, 행동 등의 정의하는 행위가 가능하도록 JavaScript에서는 객체 간의 관계를 표현하기 위해 상속(Inheritance)라는 개념이 있다.

  • 부모 클래스 : 속성, 규칙, 행동 등을 물려주는 클래스
  • 자식 클래스 : 부모 클래스로부터 상속을 받아 새롭게 정의 되는 클래스

라는 클래스는 진돗개 라는 클래스에게 모든 개들이 가지는 속성들을 물려준다. 먹을 것을 좋아한다던가… 먹을 것을 좋아한다던가… 먹을 것을 좋아한다던가.. 그리고 진돗개는 또한 개의 속성을 물려받음과 동시에 진돗개만의 특성이 정의 된다. 충성심이 강하다는 진돗개만이 가지는 속성으로 볼 수 있다.

JavaScript에서의 상속(Inheritance)

JavaScript에서는 클래스간 상속의 관계를 만들기 위해,

  1. Prototype Chain
  2. Class

이렇게 두 가지 방법을 사용할 수 있다.

앞서 OOP관련 포스팅에서 예시로 들었던 몬스터를 보면서 설명하겠다.

Prototype Chain

JavaScript에서 객체를 생성하는 방법은 총 3가지가 있다.

  • Literal
  • Constructor Function
  • Object.create()

각각의 경우에 프로토 타입 체인(Prototype Chain)이 어떻게 이루어져 있는지 확인해보자.

Object Literal(객체 리터럴)

var monster = {
  level : 0,
  habitat : 'forest',
  levelUp: function () {
    this.level++;
  }
};

객체 리터럴은 중괄호('{}')로 객체를 직접 정의하는 방법을 말한다. 위와 같이 객체가 정의 되는 순간. 객체는 프로토타입(Prototype)으로 Object.prototype을 가진다. 따라서 Object.prototype 에 정의된 메소드들을 사용할 수 있다. 이는 리터럴로 정의된 객체가 생성되는 것은 var monster = new Object(); 이렇게 Object라는 생성자의 명령으로 이루어지기 때문이다. 자세한 이유는 다음 생성자 함수를 통해서 확인할 수 있다.

크롬 개발자 도구에서 Object.prototype을 확인할 수 있다.

Constructor Function(생성자 함수)

function Monster () {
  this.healthPoint = 100;
  this.habitat = 'forest';
  this.level = 1;
}

Monster.prototype.levelUp = function () {
  this.level++;
}

var broMonster= new Monster();

위와 같은 형태로 constructor 함수를 만들고 new 키워드로 객체를 생성할 수 있다. Monster 함수 객체는 levelUp이라는 프로토타입 메소드를 가지고 있다. 따라서 new 로 생성된 broMonster 객체는 Monster의 프로토타입에 접근 할 수 있다. broMonster 객체를 자세히 살펴보면

//broMonster
{
  habitat : 'forest',
	healthPoint : 100,
	level : 1
}

broMonster.levelUp(); // broMonster.level = 2;

이런 형태를 가지고 있다. new 로 객체를 생성하면 함수 내의 this 객체를 output으로 가지기 때문이다. (Monster에서 this에 담긴 속성들이 객체로 담기게 된다.) 또한 broMonster객체는 Monster 생성자의 프로토타입에 있는 levelUp 이라는 메소드에도 접근이 가능하다.

Object.create

var monsterPrototype = {
  levelUp : function () {
    this.level++;
  }
}

var broMonster = Object.create(monsterPrototype);
broMonster.level = 1;
broMonster.habitat = 'forest';
broMonster.healthPoint = 100;

Object.create 메소드는 인자로 들어온 객체를 프로토타입으로 하는 새로운 객체를 생성한다. 위의 예제에서 monsterPrototype 이라는 객체를 인자로 넣었으므로, broMonster의 프로토타입 객체는 monsterPrototype 이 된다. 따라서 Constructor Function 을 사용한 것처럼, 프로토 타입 객체와 연결이 가능하다.

위와 같은 객체사이의 관계를 상속(Inheritance)이라고 정의하기에는 약간 설명이 안맞는 부분이 있다. 실제 속성이나 메소드를 다른 객체에게 '물려주는' 것이 아니라 '빌려쓰는 권한을 주는' 개념이기 때문이다.

Behavior Delegation(작동 위임?)

JavaScript에서는 부모 객체에서 생성된 객체(인스턴스)들이 prototype으로 연결이 되어 있다. 그러나 실제 구동하는 것을 보면 생성된 객체들에게 부모 객체의 prototype이 직접 전달된 것이 아니라, 부모 객체의 prototype에 접근할 수 있는 권한이 생겨서 필요할 때마다 접근하여 사용이 되는 형식으로 구현되어 있다.

따라서 정확한 의미로는 Inheritance 보다는 Behavior Delegation이라는 단어가 해당 시스템을 더 잘 표현하는 단어이다.

Prototype Inheritance

위에서 언급한 Prototype Chain은 JavaScript 엔진이나 브라우저에서 정의된 로직을 설명한 것이었다. 그러면 추가적으로 우리가 객체들의 관계를 직접 정의할 수는 없을까?

다음의 예제를 보자

몬스터의 종류에는 드래곤, 좀비, 해골이 있다.
그 중에 드래곤은 날아다닐 수 있다.
그 외에 모든 특성은 몬스터들의 공통 특성과 동일하다.

이 전에 만들었던 몬스터 생성자는 다음과 같다.

function Monster (name) {
  this.healthPoint = 100;
  this.habitat = 'forest';
  this.level = 1;
  this.name = name;
}
Monster.prototype.levelUp = function () {
  this.level++;
}

그리고 드래곤의 생성자 함수를 만들어보겠다.

function Dragon (name) {
  this.isFly = true;
}

날 수 있다는 속성만 추가하였다. 다른 속성 및 메소드들은 Monster가 가진 것들을 가져올 것이다.

프로토타입의 상속은 다음의 과정을 거친다.

1) 자식의 생성자의 프로토 타입 객체를 부모의 프로토 타입 객체를 프로토 타입으로하는 객체로 설정한다.

2) 생성된 자식 생성자의 프로토 타입 객체의 constructor를 자식 생성자로 설정한다.

3) Dragon 생성자가 실행될 때, Monster 생성자도 실행되게 하여 해당 속성을 Dragon 생성자의 this 객체에 할당한다.

위의 과정이 담긴 코드는 다음과 같다.

function Dragon (name) {
  Monster.call(this, name);
  this.isFly = true;
}

Dragon.prototype = Object.create(Monster.prototype);
Dragon.prototype.constructor = Dragon;

1) Object.create 메소드를 사용하여 부모 클래스의 프로토타입을 프로토타입으로 하는 객체를 생성하고 Dragon의 프로토타입에 할당하였다. 이 과정을 통해 Dragon으로 생성된 객체도 프로토타입 체인을 통해 Monster의 메소드에 접근할 수 있게 된다.

2) Object.create메소드를 사용하여 할당된 객체의 프로토타입에는 constructor가 사라지게 된다. 따라서 프로토타입에 있어야 하는 constructor 속성을 Dragon 생성자로 할당하였다.

3) 마지막으로 call 함수를 통해 Dragon이 실행될 때 Monster 함수가 실행되고 Monster의 this를 Dragon 생성자의 this 객체로 할당한다.

위의 과정을 통해 Monster를 부모로 갖는 Dragon 생성자 함수를 만들 수 있었다. 이러한 객체간의 관계 정의는 OOP에서 중요한 역할을 하고 있다. 객체 위주의 프로그래밍에 객체 간의 상하 관계 구분은 필수적이기 때문이다.

Class

ES2015에서는 이러한 프로토타입 상속의 과정을 더 간단하게 코딩하기 위한 방법으로 Class가 도입되었다. 위의 예제와 동일한 Class 구문은 다음과 같다.

class Monster {
  constructor(name) {
    this.healthPoint = 100;
  	this.habitat = 'forest';
  	this.level = 1;
  	this.name = name;
  }
  
  levelUp () {
    this.level++;
  };
}


class Dragon extends Monster {
  constructor(name) {
    super(name);
    this.isFly = true;
  }
}

1) Monster 클래스의 constructor 객체는 객체의 속성을 정의하는 곳이다. 생성자 함수와 동일하게 this 객체에 속성을 할당하는 방식으로 정의가 가능하다.

2) Monster 클래스의 메소드는 constructor 객체 아래에 함수식으로 적으면 된다.

3) Dragon 클래스가 Monster 클래스의 하위 객체라는 것을 정의할 때는 extends와 super 키워드를 이용하면 된다.

4) Dragon 클래스만 가지는 속성이 있다면 constructor 객체 안에 정의하면 된다.

5) Dragon 클래스만 가지는 메소드가 있다면 constructor 객체 아래에 함수식으로 정의하면 된다.

위의 class 구문과 prototype 상속 구문은 동일한 역할을 한다.

By Cyrano on July 30, 2019.

profile
개발자로 한걸음 한걸음 가고 있어요.

0개의 댓글