객체 지향 프로그래밍에 대해 잘 모르셨던 분이라면 첫 번째와 두 번째 포스팅 모두를 보고 오셨을 것이고, 아니라면 이 포스팅으로 바로 넘어오셨을 것이다. 이 포스팅부터는 독자분깨서 객체 지향 프로그래밍이 무엇인지 안다는 가정 하에 작성되었으므로, 글을 읽다가 이해가 안되는 부분이 있다면 첫 번째 혹은 두 번째 포스팅을 다시 보고 오시는 것을 추천드린다.
이 포스팅에서는 자바스크립트에서의 객체가 무엇이며, 자바스크립트의 prototype 기반 객체 지향 프로그래밍이 어떤 구조로 설계되었는지를 알아본다. 필자의 개인적인 의견일지는 모르나, 자바스크립트의 객체 지향 문법은 무척이나 괴랄하다. 필자도 이해하는데 매우 오래걸렸고 아직도 완벽하게 이해하고 있는지는 확실치 않으나, 일단 이해한 부분까지만이라도 글을 써 보도록 노력하겠다.
프로토타입을 이해하는데 있어 오승환님의 블로그 글에서 매우 큰 도움을 받았으며, 본 포스팅을 읽어봐도 자바스크립트에서 객체의 동작 방식이 이해가 안 된다면 한번 쯤 오승환님의 블로그 글을 정독해보는 것을 추천한다.
ES6 이전의 자바스크립트에서는 클래스라는 개념이 존재하지 않지만, 프로토타입에 대한 설명을 하기에 앞서 우선 우리에게 익숙한 클래스 개념을 사용해 자바스크립트에서의 객체에 대해 이해하고 넘어가보자.
필자는 처음 자바스크립트를 공부할 때, 객체라는 단어는 다음과 같은 두 가지 의미가 상황에 따라 다르게 사용되는 용어라고 생각했었다.
- (키, 값) 쌍을 저장하는 자바스크립트의 데이터 타입
( ex. const obj = { x: 1, y: 2 } )- 클래스의 인스턴스 ( Instance )
( ex. const obj = new Person(); )
이러한 착각때문에 대체 왜 데이터 타입의 이름을 객체라고 지어서 사람을 헷갈리게 만드는지 모르겠다고 불평하고 다녔다. 하지만 이것은 자바스크립트를 깊게 이해하지 못한 내 무지에서 비롯된 행동이었다.
우리가 아래와 같은 코드를 통해 obj라는 객체를 정의하는 것은,
const obj = {}
사실 다음과 같이 Object 클래스의 인스턴스 obj를 생성하는 것과 동일하다.
const obj = new Object();
즉, (키, 값) 쌍을 저장하는 데이터 타입인 객체는 그 자체로 Object라는 클래스의 인스턴스인 것이다. 따라서 자바스크립트에서 객체라는 단어는 하나의 개념만을 의미하는 단어이며, 그 의미는 Object 타입의 클래스 인스턴스이다.
아래 결과를 봐도 두 코드의 결과가 동일하다는 것을 알 수 있다.
클래스 기반 객체 지향 프로그래밍에 익숙한 분들을 위해 부득이하게 클래스라는 용어를 썼으나, 아래 이어질 내용에서 설명하듯이 ES6 이전의 자바스크립트에서는 클래스라는 개념이 존재하지 않는다는 것을 유념해두길 바란다.
위에 언급했다시피 자바스크립트에는 클래스라는 개념이 존재하지 않는다. 그러면 위에서 객체를 생성할 때 사용된 new Object()에서의 Object는 대체 뭘까?
클래스라는 개념이 존재하지 않는다면 클래스를 대신할 수 있는 선택지는 많지 않다. 우리가 어떤 것을 '클래스'라고 부르기 위해서는 최소한 아래 세 조건은 만족해야 한다.
한 공간에 클래스의 속성들을 포함해야 한다.
한 공간에 클래스의 메서드들을 포함해야 한다.
하나의 틀을 바탕으로 유사한 객체들을 찍어낼 수 있어야 한다.
클래스를 제외하면 그나마 비슷하다고 생각되는 개념이 한 개 밖에 없지 않는가? 바로 함수다.
// Car 함수 정의
function Car(name, speed) {
const name = name;
const speed = speed;
function Drive() {
...
}
}
함수만큼 클래스와 비슷한 형태를 가진 개념은 없을 것이다. 함수 내부에서는 여러 변수들과 함수들을 정의할 수 있으며, 함수를 한 번 정의해 놓기만 하면 여러 번 호출할 수 있기 때문이다.
이와 같은 이유로 자바스크립트에서는 객체를 생성하기 위해 함수를 사용한다. 즉, Car라는 타입의 객체를 찍어내기 위해서 다음과 같이 Car 함수를 정의할 수 있다.
function Car(name, speed) {
this.name = name;
this.speed = speed;
this.Drive = function() {
...
}
}
const myCar = new Car("Tiko", 132);
위와 같은 코드를 작성함으로써 자바스크립트에서 Car 타입의 객체인 myCar를 생성할 수 있다.
그런데 위와 같은 코드는 하나의 문제를 가지고 있다. 우리가 클래스 기반 객체 지향 프로그래밍에서 클래스에 메서드를 정의하면, 해당 클래스 타입의 모든 객체들은 하나의 메서드를 공유하게 된다. 만약 Car 클래스에 Drive라는 메서드가 존재한다면, Car 타입 객체가 몇 개 생성되던 간에 메모리에는 Car 클래스의 Drive 메서드 하나만 존재하게 되며 모든 Car 타입 객체들은 Drive 메서드를 호출할 때 마다 동일한 메모리에 접근하여 메서드를 호출하게 된다.
하지만, 위와 같은 자바스크립트 코드는 다르다.
this.Drive = function() {
...
}
이 코드는 Car 타입 객체가 생성될 때 마다 Drive라는 변수를 생성하고, 이 변수에 익명 함수를 저장하는 형태로 동작한다. 즉, 객체가 생성될 때 마다 새로운 익명 함수가 생성되어 Drive에 저장되는 것이므로 객체가 2000개 생성되면 익명 함수도 2000개가 생성되게 된다. 메모리 낭비가 심할 것이고, 무엇보다 진정한 의미에서의 객체 지향을 구현한 것이 아니게 된다.
자바스크립트에서 동일한 타입의 모든 객체가 하나의 메서드를 공유하도록 코드를 구현하려면, 위의 Car 함수 정의에서 Drive 메서드 부분을 다음과 같이 고쳐야 한다.
function Car(name, speed) {
this.name = name;
this.speed = speed;
}
Car.prototype.Drive = function () { <= 변경된 부분
...
}
const myCar = new Car("Tiko", 132);
위와 같은 코드를 사용하면 모든 Car 타입 객체들이 하나의 Drive 메서드를 공유하게 된다.
자바스크립트가 prototype 기반의 객체 지향 프로그래밍 언어라더니, prototype이라는 용어가 드디어 코드에 등장했다. 이제 드디어 prototype이 무엇인지 알아볼 때가 되었다.
자바스크립트가 prototype 기반의 객체 지향 프로그래밍 언어라는 것은 알겠는데, 대체 prototype이라는게 뭘 의미하는걸까?
prototype이라는 영단어를 해석하면 '원형(原型)'으로, 원형의 사전적 의미는 다음과 같다.
원형(原型): 같거나 비슷한 여러 개가 만들어져 나온 본바탕.
즉, 동일하거나 비슷한 것들을 찍어낼 때 그 기준이 되는 설계도가 원형, 즉 prototype이라고 할 수 있다.
위에서 Car 함수를 정의했을 때를 생각해보자. 우리는 하나의 Drive 메서드를 모든 Car 타입 객체들이 공유하게 만들기 위해 Car.prototype
이라는 곳에 Drive 메서드를 정의했다.
Car의 prototype이라는 변수는 모든 Car 타입 객체들의 기준이 되는 단 하나의 '원형' 객체를 가리킨다. 우리가 아래 코드처럼 Car 함수를 정의하면, Car 함수에는 Drive라는 메소드가 존재하지 않는다.
대신 Car.prototype이 가리키는 원형 객체를 보면 이 객체에 Drive 메서드가 정의된 것을 볼 수 있다.
자바스크립트는 myCar.Drive()
코드로 Car 타입 객체에 대해 Drive 메서드를 호출할 때, 해당 객체에 Drive 메서드가 존재하지 않는다면 그 객체의 원형을 가리키는 prototype으로 이동하여 Drive 메서드가 존재하는지 확인하고, 존재한다면 원형 객체의 Drive 메서드를 호출한다. 이런 방식으로 동작하기 때문에 Car 타입의 모든 객체들이 동일한 원형 객체의 Drive 메서드를 호출하게 되는 것이다.
Car.prototype이 원형 객체를 가리킨다고 위에서 언급했다. 그럼 원형 객체라는 것은 언제 만들어진 것이고, Car.prototype이라는건 어떻게 원형 객체를 가리키게 된 것일까? 우리는 단지 Car이라는 함수를 정의했을 뿐이고, Car에 prototype이라는 이름의 변수를 선언한 적도 없으며 Car의 원형 객체라는 것도 만든 적이 없는데 말이다.
자바스크립트에서는 우리가 어떤 함수를 정의하던 간에 해당 함수에 대한 Prototype 객체 (Prototype Object, 원형 객체)를 자동으로 생성해주며, 함수에 대해 '함수.prototype'이라는 변수도 생성한 후 이 변수가 Prototype 객체를 가리키도록 연결해준다.
함수 안에서 this 키워드를 쓰는지 혹은 prototype 변수를 실제로 사용하는지의 여부는 상관 없다. 그냥 일반적인 함수 호출을 사용하기 위해 함수를 정의했다고 하더라도 자바스크립트는 무조건 해당 함수에 대한 Prototype 객체를 생성한다. 아래 이미지가 이 사실을 증명해준다.
위 이미지에서 볼 수 있듯이 Prototype 객체는 생성될 때 기본적으로 constructor
메서드와 __proto__
속성을 가진다. 이제 constructor와 __proto__가 각각 무엇을 의미하는지 알아보자.
constructor 함수는 클래스 기반 객체 지향 프로그래밍에서도 등장하는 익숙한 개념인 생성자로, 우리가 객체 생성을 요청할 때 호출되어 실제 객체를 만드는 역할을 하는 함수이다. 즉 우리가 new Car()
라는 코드를 호출해 Car 타입의 객체 생성을 요청하면, Car.prototype.constructor 함수가 자동으로 호출되어 객체를 생성해준다.
사실 자바스크립트에서는 Prototype 객체의 constructor가 기존 함수 자체를 가리키기 때문에, Car.prototype.constructor
는 function Car(name, speed)
를 가리킨다. 따라서 new 키워드를 사용했을 때 Car.prototype.constructor 함수가 호출되는 것은 Car 함수를 Car("Benz", 230)
처럼 직접 호출하는 것과 다를 바가 없다.
단지, new 키워드가 함수 내부에서 사용한 this를 전역 객체인 window가 아니라 실제로 생성된 객체를 가리키도록 만들어주므로, 자바스크립트의 객체 생성에 있어 진짜 중요한 역할은 생성자가 아니라 new 키워드가 처리해준다고 볼 수 있다.
자바스크립트의 생성자에 대한 보다 상세한 설명이 필요하다면 이 StackOverflow 글을 참고하자.
constructor 메서드는 오직 '함수의 Prototype 객체'만이 가진다. 하지만 __proto__는 자바스크립트의 '모든 객체'에 대해 자동으로 추가되는 속성이다. 아래 코드를 보면, Car 함수의 Prototype 객체가 constructor와 __proto__ 모두를 포함하는 것과 달리 myCar 객체는 오직 __proto__ 속성만을 가진다는 것을 알 수 있다.
위 글에서 Car.prototype이 가리키는 Prototype 객체에 Drive 함수를 정의하면, Car 타입의 모든 객체들이 Prototype 객체의 Drive 함수를 사용하게 된다고 했었다. 하지만 자바스크립트가 myCar.Drive()
라는 코드를 보고 Prototype 객체의 Drive 함수를 호출하려면, myCar 객체에서 Prototype 객체로 이동할 수 있는 방법이 있어야 한다. myCar 객체의 속성으로 prototype 속성이 있어서 myCar.prototype처럼 접근할 수 있으면 좋겠지만, myCar 객체에는 Car 클래스에서와 같은 prototype 변수가 존재하지 않는다.
이를 해결하기 위해 자바스크립트는 모든 객체의 인자로 Prototype 객체를 가리키는 __proto__라는 변수를 정의해준다. 아래 실행 결과를 보면 myCar.__proto__가 Car.prototype과 동일하게 Car 함수의 Prototype 객체를 가리킨다는 것을 알 수 있다.
따라서, myCar.Drive()
라는 코드가 실행되면 자바스크립트는 다음과 같은 과정을 거쳐 Prototype 객체의 Drive 메서드를 호출한다.
myCar 객체에 Drive 메서드가 있는지 확인한다.
myCar 객체에 Drive 메서드가 없다면, Prototype 객체에 해당 메서드가 존재하는지 확인하기 위해 __proto__ 변수가 가리키는 객체로 이동한다.
Prototype 객체에 Drive 메서드가 존재한다면, Prototype 객체의 Drive 메서드를 호출한다.
이제 자바스크립트에서 Prototype 객체가 무엇이고, 함수와 객체의 관계를 어느 정도 이해했을 것이라 생각한다. 그러면, 바로 위에서 언급한 Drive 메서드 호출의 과정 중 3번을 다시 살펴보자.
3. Prototype 객체에 Drive 메서드가 존재한다면, Prototype 객체의 Drive 메서드를 호출한다.
만약 우리가 Car.prototype에 Drive 메서드를 정의하지 않아서, Prototype 객체에도 Drive 메서드가 존재하지 않는다고 가정해보자. 그렇다면, 자바스크립트는 Car 함수의 Prototype 객체에서 탐색을 종료할까?
정답은 No다. 우리가 Car 함수만을 정의했기 때문에, Car의 Prototype 객체가 독립적으로 존재한다고 생각하기 쉽다. 하지만, 자바스크립트에서 모든 객체는 최상위 객체로 Object라는 객체를 가진다. Prototype 객체도 결국 객체이므로 최상위 객체로 Object 객체를 가지며, 따라서 Prototype 객체인 Car.prototype의 __proto__ 속성은 아래와 같이 최상위 객체인 Object의 Prototype 객체를 가리키게 된다. ( constructor 메소드를 가지고 있는 점에서 Prototype 객체임을 알 수 있다. )
이처럼 다수의 Prototype 객체들이 __proto__ 속성을 통해 계층적으로 연결되는 구조를 'Prototype들이 체인으로 묶여있는 것 같다'라고 하여, 'Prototype Chain'이라는 용어로 부른다.
그럼 다시 본 주제로 돌아가서, 자바스크립트는 어떤 함수의 Prototype 객체에서 원하는 메서드를 찾지 못한다면 해당 Prototype 객체의 __proto__ 속성이 가리키는 부모 Prototype 객체로 이동한다. 이 부모 Prototype 객체에도 Drive 메서드가 존재하지 않는다면 그 객체의 부모로 또 다시 이동해 탐색하는 작업을 반복한다. 이 작업은 Object 함수의 Prototype 객체에 도달할 때 까지 반복되며, Object.prototype 객체는 최상위 객체이므로 __proto__ 속성의 값이 null이기 때문에 더 이상 이동할 수 있는 부모 Prototype 객체가 존재하지 않아 작업을 중단한다.
위에서 예시로 든 Car 함수의 경우 우리가 특별히 부모라고 지정해 준 Prototype 객체가 존재하지 않으므로, 부모로 최상위 객체인 Object 객체를 가지게 되고 따라서 Car.prototype 객체의 __proto__ 속성은 최상위 객체의 Prototype 객체인 Object.prototype을 바로 가리키게 된다. 따라서, 자바스크립트는 Object.prototype에 Drive 함수가 있는지 마지막으로 확인한 후, 없다면 함수가 존재하지 않는다는 에러를 발생시킨다.
자바스크립트의 Prototype 개념은 배워야 할 것도 많고 굉장히 복잡한 개념이다. 지금까지 이 포스팅에서 배운 것들을 하나의 그림으로 정리하면 다음과 같을 것이다.
이미지 제작을 위해 오승환님 블로그를 참고함
이로써 자바스크립트가 prototype 기반의 객체 지향 프로그래밍이라는 의미가 무엇이며, 객체가 어떻게 동작하는지 그리고 prototype이라는 것이 무엇인지에 대해 알아보았다. 남은 두 포스팅에서는 본 포스팅에서 배운 자바스크립트 객체에 대한 이해를 바탕으로, 실제 자바스크립트 코드 상에서 어떻게 객체를 사용할 수 있는지에 대해 알아보겠다.