2021-09-16 프로토타입

2021-09-16
  • JavaScript

프로토타입

예전부터 자바스크립트의 프로토타입(Prototype)에 대하여 검색해보면 나오는 문구들은 대체로 아래와 같았다.

“자바스크립트는 명령형, 함수형, 프로토타입 기반 객체지향 프로그래밍을 지원하는 멀티 패러다임 프로그래밍 언어다.” - 이웅모, <모던 자바스크립트 Deep Dive>, 259p

“자바스크립트는 클래스 기반 객체지향 프로그래밍 언어보다 효율적이며 더 강력한 객체지향 프로그래밍 능력을 지니고 있는 프로토타입 기반의 객체지향 프로그래밍 언어다.” - 이웅모, <모던 자바스크립트 Deep Dive>, 259p

일단은 자바나 C++ 같은 언어들과 같은 방식으로 객체지향을 구현하지 않았다고 하여 자바스크립트를 불완전한 언어로 치부하지 말라는 의도로 읽힌다. 그러면서 자바스크립트는 객체지향 뿐만 아니라 명령형, 함수형 패러다임을 모두 포함하는 정말 굉장한 언어라는 어필이 대체로 따라 붙는다.

하지만 단순히 내가 지지하고자 하는 대상을 무언가의 반대항으로서만 정의하는 것, 즉 ‘~이 아닌’ 같은 부정의 의미로서만 설명하는 건 너무 빈약하다. ‘제대로 된 언어가 아닌게 아닌’으로 설명하지 말자.

정말이지 자바스크립트의 프로토타입 기반 객체지향에 대해 설명해야 ‘다시는 한국을 무시하지 마라’ 같은 이상하고 기묘한 상황에 빠지지 않는다는 뜻이다.

짤 '다시는 자바스크립트를 무시하지 마라'

그럼 빌드업은 충분했으니 이 대단한 자바스크립트의 객체지향에 대해 알아보도록 하자.

프로토타입과 클래스를 관통하는 키워드인 객체지향에 대해 잠깐 짚고 넘어가보자. <객체지향의 사실과 오해>에서는 객체지향에 대하여 다음과 같이 정의하고 있다.

  1. 객체지향이란 시스템을 상호작용하는 자율적인 객체들의 공동체로 바라보고 객체를 이용해 시스템을 분할하는 방법이다.

  2. 자율적인 객체란 상태와 행위를 함께 지니며 스스로 자기 자신을 책임지는 객체를 의미한다.

  3. 객체는 시스템의 행위를 구현하기 위해 다른 객체와 협력한다. 각 객체는 협력 내에서 정해진 역할을 수행하며 역할은 관련된 책임의 집합이다.

  4. 객체는 다른 객체와 협력하기 위해 메시지를 전송하고, 메시지를 수신한 객체는 메시지를 처리하는 데 적합한 메서드를 자율적으로 선택한다.

이러한 정의는 일종의 가이드라인이자 규약이다. 실제 이 규칙을 어떻게 구현해야 하는지에 대한 정책을 규정하고 있지는 않다. 마치 ECMAScript에서 자바스크립트 엔진의 세부 구현사항을 정의하지 않고 각 브라우저마다 나름의 엔진을 구현하고 있어 디테일이 달라지는 것과 같다.

클래스 기반 객체지향 언어프로토타입 기반 객체지향 언어의 차이를 이와 유사하게 볼 수 있다. 객체지향이라는 철학은 공유하되 그 철학을 어떻게 구현하는지에 있어서 기술적인 차이가 있을 뿐이다. 언어 레벨에서야 적잖은 차이가 존재할 수도 있겠으나 중요한 건 본질이다.

클래스가 객체지향 프로그래밍 언어의 관점에서 매우 중요한 구성요소(construct)인 것은 분명하지만 객체지향의 핵심을 이루는 중심 개념이라고 말하기에는 무리가 있다. 자바스크립트 같은 프로토타입(prototype) 기반의 객체지향 언어에서는 클래스가 존재하지 않으며 오직 객체만이 존재한다. 프로토타입 기반의 객체지향 언어에서는 상속 역시 클래스가 아닌 객체 간의 위임(delegation) 메커니즘을 기반으로 한다.

(…) 중요한 것은 어떤 클래스가 필요한가가 아니라 어떤 객체들이 어떤 메시지를 주고받으며 협력하는가다. 클래스는 객체들의 협력 관계를 코드로 옮기는 도구에 불과하다. (…) 객체지향은 객체를 지향하는 것이지 클래스를 지향하는 것이 아니다.

조영호, <객체지향의 사실과 오해>, 37p


결국 객체지향의 핵심이 객체와 객체가 어떻게 상호 소통하도록 만드냐에 달려 있다면 뒤에서 좀 더 자세히 다룰 ES6의 class 문법이 없는 ES5 버전의 자바스크립트라고 해서 불가능할 건 없다는 것이다. 당연한 말인게 자바스크립트의 객체지향은 프로토타입 기반이고, 따라서 class 문법도 프로토타입을 기반으로 하고 있다.

프로토타입에 대해 조금 더 직접적으로 이야기해보자. 앞서 말했듯, 자바스크립트의 프로토타입은 객체와 객체가 상호 소통할 수 있도록 돕는 객체지향의 메커니즘이다. 자바스크립트는 프로토타입 메커니즘을 통한 상속을 활용해 중복을 줄이고 메모리 효율성을 추구한다.

자바스크립트는 프로토타입을 기반으로 상속을 구현하여 불필요한 중복을 제거한다.

이웅모, <모던 자바스크립트 Deep Dive>, 261p

동일한 생성자 함수에 의해 생성된 모든 인스턴스가 메서드를 중복 소유하는 것은 메모리를 불필요하게 낭비한다. 또한 인스턴스를 생성할 때마다 메서드를 생성하므로 퍼포먼스에도 악영향을 준다. 만약 10개의 인스턴스를 생성하면 내용이 동일한 메서드도 10개 생성된다.

이웅모, <모던 자바스크립트 Deep Dive>, 263p


조금 더 풀어 설명해보자. 내가 Array 생성자 함수를 사용해 만든 인스턴스가 10개 있다. 각각의 인스턴스들은 .map() 메서드를 가지고 있다. 각각의 인스턴스가 .map()이라는 동작을 해야 하므로, 직관적으로는 각각의 객체들이 해당 메서드를 갖고 있어야 할 것 같다.

하지만 이는 코드의 중복이다. 동일한 기능을 가진 코드들이 인스턴스를 찍어내는 만큼 생성되고 있기 때문이다.

따라서 .map()의 실질적인 기능을 하는 코드는 Array 생성자 함수에게 맡겨두고, 인스턴스는 생성자 함수가 가지고 있는 메서드의 주소만 참조하도록 한다. 그러면 인스턴스를 10개를 만들든, 100개를 만들든 실제 코드는 하나만 존재하고, 인스턴스들은 주소만 들고 있으므로 메모리 효율적인 구현이 된다. 그리고 이렇게 각각의 객체와 생성자 함수를 연결해주는 통로 역할을 하는 것이 바로 프로토타입이다.

나는 이러한 프로토타입의 메커니즘을 프로토스의 칼라와 같다고 설명한 적이 있다. 잘 나가다가 갑자기 무슨 스타크래프트 소리냐면…

짤 엥?

스타크래프트 세계관에서는 테란, 저그, 프로토스 세 가지 종족이 존재한다. 이 중 프로토스는 고도로 진화된 문명 종족으로 그들은 머리에 연결된 신경 다발을 통해 모든 종족이 하나의 의식을 공유한다. 이를 칼라라고 부른다.

자바스크립트의 프로토타입은 객체가 프로토타입과 연결되어 있고, 프로토타입은 생성자 함수와 연결되어 있다는 점에서 일족 간 정신을 공유하는 칼라와 유사한 점이 있다. 같은 생성자 함수로부터 생성된 인스턴스라면 생성자 함수에서 정의하고 있는 상태와 메서드를 프로토타입을 통해 참조할 수 있다는 뜻이다.

function Protoss(options) {
  this.psionicEnergy = 100
  this.tribe = options.tribe

  attack() { ... }

  stop() { ... }
}

const zealot = new Protoss({ tribe: "Fornax" })
const darkTemplar = new Protoss({ tribe: "Akilae" })
zealot.attack()
darkTemplar.attack()

모든 객체는 하나의 프로토타입을 갖는다. 그리고 모든 프로토타입은 생성자 함수와 연결되어 있다.” - 이웅모, 이웅모, <모던 자바스크립트 Deep Dive>, 264p

그러면 실질적으로 어떻게 객체와 객체가 프로토타입의 연쇄를 통해 연결되는지 그림을 통해 확인해보자.

프로토타입 https://burger-and-fries.tistory.com/14

그림에서 눈에 띄는 부분을 묶어 보자. 주황색 상자들은 생성자 함수, 노란색 상자들은 생성자 함수와 연결된 프로토타입, 파란색 상자는 객체의 인스턴스로 볼 수 있다.

위에 언급했던 것과 같이 모든 객체는 하나의 프로토타입을 갖는다. 그렇기 때문에 myArray 인스턴스는 Array.prototype과 연결되어 있다. 그리고 다시 Array.prototypeArray 생성자 함수와 연결되어 있다.

여기서 눈여겨보면 좋을 부분이 두 가지 정도 보인다. 하나는 내부 슬롯 [[prototype]]과 접근자 프로퍼티 __proto__, 다른 하나는 프로토타입 체인 최상단에 Object.prototype이 존재한다는 점이다.

“자바스크립트는 객체 기반의 프로그래밍 언어이며 자바스크립트를 이루고 있는 거의 ‘모든 것’이 객체다. 원시 타입의 값을 제외한 나머지 값들(함수, 배열, 정규 표현식 등)은 모두 객체다.”

이웅모, <모던 자바스크립트 Deep Dive>, 259p


일단 Object.prototype은 어떻게 보면 자연스러운데, 자바스크립트에서 원시값을 제외한 모든 값은 객체를 확장한 것이다. 그러므로 프로토타입 체인의 종점은 Object.prototype이 된다.

자바스크립트에는 여러 가지 내부 슬롯이 존재한다. 여기서 내부 슬롯에 대해 다루지는 않을 예정이다. 아무튼 이러한 내부 슬롯은 엔진 레벨에서 구현하는 부분이므로 개발자가 직접 접근하여 사용할 수 있는 내용은 아니다.

다만 간접적으로 접근할 수 있는 수단을 제공하기도 하는데, 프로토타입의 경우 __proto__ 접근자 프로퍼티다.

[[prototype]] 내부 슬롯에는 직접 접근할 수 없지만, 위 그림처럼 proto 접근자 프로퍼티를 통해 자신의 프로토타입, 즉 자신의 [[prototype]] 내부 슬롯이 가리키는 프로토타입에 간접적으로 접근할 수 있다. 그리고 프로토타입은 자신의 constructor 프로퍼티를 통해 생성자 함수에 접근할 수 있고, 생성자 함수는 자신의 prototype 프로퍼티를 통해 프로토타입에 접근할 수 있다.

이웅모, <모던 자바스크립트 Deep Dive>, 265p


왜 굳이 이렇게 해두었을까 이상해보이기도 하지만 결론부터 말하면 프로토타입은 참조를 통해 체인을 형성할 수 있고, 이 체인이 상호 참조하여 끝나지 않는 고리 형태가 되면 문제가 생길 수 있다. 이러한 순환 참조를 방지하기 위한 수단을 마련한 것이다.

[[prototype]] 내부 슬롯의 값, 즉 프로토타입에 접근하기 위해 접근자 프로퍼티를 사용하는 이유는 상호 참조에 의해 프로토타입 체인이 생성되는 것을 방지하기 위해서다.

(…) 프로토타입 체인은 단방향 링크드 리스트로 구현되어야 한다. 즉, 프로퍼티 검색 방향이 한쪽 방향으로만 흘러가야 한다. (…) 순환 참조하는 프로토타입 체인이 만들어지면 프로토타입 체인 종점이 존재하지 않기 때문에 프로토타입 체인에서 프로퍼티를 검색할 때 무한 루프에 빠진다. 따라서 아무런 체크 없이 무조건적으로 프로토타입을 교체할 수 없도록 proto 접근자 프로퍼티를 통해 프로토타입에 접근하고 교체하도록 구현되어 있다.

이웅모, <모던 자바스크립트 Deep Dive>, 268p

스코프 체인을 타고 올라가면서 식별자를 검색하는데, 스코프 체인의 종점이어야 할 전역 스코프가 다시 출발점의 스코프를 상위 요소로서 참조하고 있다면 이 스코프 체인에서의 식별자 검색은 영원히 끝나지 않을 것이다.


검색 이야기가 나왔으니 조금만 더 이어가보자.

스코프 체인에서 식별자를 검색하듯이, 우리가 어떤 객체의 메서드를 호출했을 때 자바스크립트 엔진은 메서드를 프로토타입 체인에서 검색한다. 해당 객체의 프로퍼티에서 먼저 찾고, 상위 프로토타입을 순차적으로 검색하며 프로퍼티를 찾게 된다.

자바스크립트는 객체의 프로퍼티(메서드 포함)에 접근하려고 할 때 해당 객체에 접근하려는 프로퍼티가 없다면 [[prototype]] 내부 슬롯의 참조를 따라 자신의 부모 역할을 하는 프로토타입의 프로퍼티를 순차적으로 검색한다. - 이웅모, <모던 자바스크립트 Deep Dive>, 286p

즉 스코프 체인은 식별자 검색을 위한 메커니즘이고, 프로토타입 체인은 상속과 프로퍼티 검색을 위한 메커니즘이라 정리할 수 있다.


참고 자료