본문 바로가기
SW Engineering

[언어] 자바스크립트의 프로토타입 개념에 대한 조사

by jysden 2023. 3. 2.

1. 들어가며

  이 포스팅은 [9]에 대한 확장글이다. 필자는 [9]를 흥미롭게 읽은 뒤, 더 관심을 갖게 되는 주제에 대하여, 관련된 문헌을 추가적으로 찾아 내용을 덧붙이거나 해당 문헌을 명료화하였다. 왜 자바스크립트는 자바와 다르게 클래스 기반 객체지향 프로그래밍(이하 OOP)가 아닌 프로토타입 기반 OOP를 사용할까? 이것은 무엇이고, 이것을 사용할 때의 이점은 무엇일까? JS가 프로토타입을 기반으로 설계되었고, 프로토타입이 JS에서 중요하다는 말은 많이 들어왔다. 하지만 프로토타입이란 무엇이고 이것이 엔지니어링 측면에서 주는 이점이 무엇인지에 대한 자세하고 구성적인 답변을 들어보지는 못했다. 이것에 대해 면밀히 탐구하기 전에, 먼저 관련된 용어를 먼저 정리해보자.

2. 용어 정리

객체

  • SW에서의 객체: 메모리(실제 저장공간)에 할당된 것으로 프로그램에서 사용되는 데이터 또는 식별자에 의해 참조되는 공간을 의미하며, 변수자료 구조함수 또는 메소드가 될 수 있다.
  • OOP에서의 객체: 객체지향 프로그래밍에서 객체는 클래스의 인스턴스이다.
    • 프로그래밍 언어는 변수를 이용해 객체에 접근하므로 객체와 변수라는 용어는 종종 함께 사용된다. 그러나 (변수에 대하여) 메모리가 할당되기 전까지 객체는 존재하지 않는다.

  • 객체 지향 프로그래밍(OOP): 데이터프로세스가 동일한 모듈 내부에 위치하도록 프로그래밍하는 방식
  • 생성자 함수
    • JS에서는 본래 객체를 만들 때 클래스를 이용하여 만들지 않았다. JS에서는 함수를 인스턴스화 해서 객체를 만들었다. 이때 사용되는 함수를 생성자 함수라 한다.

3. 자바스크립트는 왜 클래스 기반 OOP가 아닌 프로토타입 기반 OOP를 사용하는가?

JS는 Java에서 사용하고 있는 클래스 기반 OOP에 영향을 준 고전 범주화 이론 보다 원형(prototype) 이론이라는 더 세련된 이론에 기초하여 프로그래밍 언어를 설계한 것이다. 이 원형 이론이 주는 실무적인 측면에서의 이점은 이렇다: 실무적인 측면에서 말할 때, 원형 이론에 따라 코딩을 할 시, 클래스명 고민을 덜 하게 해주고, 객체 간 공통 속성이 무엇인지 고민을 덜 하게 해준다. (클래스 설계, 클래스 간 상속 관계는 기술 부채를 야기하곤 했다.) 예컨대, (병아리, 코리안 숏헤어, 말티즈)를 하나의 클래스로 묶는 것과 (아프리카 들개, 들고양이, 말티즈, 병아리)를 하나의 클래스로 묶는 것을 상상해보라. 클래스명을 무엇으로 할 지 고민해야 한다. 전자의 경우는 비교적 편하다. 이들은 모두 실내에서 많이 키우는 동물-종이기 때문이다. 하지만 후자의 것은 비교적 어렵다. 공통 속성이 잘 찾아지지 않기 때문이다. 이것은 자연스러운 것일 듯 하다.

고전 범주화 이론 (Aristotle)

Aristotle

배경 지식: 범주화(Categorization)란 우리가 접하는 사물, 개념, 현상을 분류하여 이해하는 방식을 일컫는다. 이것은 사실 기원전부터 연구되어온 개념이다. 고전 범주화 이론은 사실 여러가지 있지만 [7]의 이해를 따라가기 위해, 아래 같이 범주화에 대한 지배적인 입장만을 고려한다.

[what] 이 범주(categories)는 멤버들의 공통 속성에 의해 정의된다.

(예) 아래 A,B,C는 모두 삼각형. 우리는 이들이 모두 삼각형이라는 것을 어떻게 알 수 있을까? 아래 3 개의 삼각형은 모두 세 점이 일직선 위에 있지 않고, and 두 변의 길이의 합이 한 변의 길이 보다 크다는 조건을 만족하기 때문. → 우리는 삼각형 자체에 대한 본질적 속성(성립 조건, 또는 공통 속성)을 알고 있다.

[한계] (실제 세계에서 생각해보면) 새로운 데이터가 많아지면 고전 범주화 방식은 범주화 과정에서 곤경을 겪을 수 있다. 실제로, 수학이나 논리학 바깥에서 사물, 개념 등을 보면 그것의 본질적 속성(공통 속성)을 알기 어려울 때가 많다. 게임이란 무엇일까? 게임이 게임이기 위한 조건, 즉 게임의 본질적 속성은 존재하지 않는 것 같다.

비트겐슈타인의 지적

게임이라는 하나의 범주 내 여러 개체는 일부의 속성만을 공유하는 것 같고, 엄격한 공통 속성은 찾을 수 없는 것 같다.

고전 범주화 이론을 통해 설명되는 클래스 개념

이전 시대에, 심지어 현재에도 많이 사용되고 있는 JAVA는 주지하듯이 클래스 기반 객체지향 프로그래밍을 따른다. [10]에 따르면 이것은 클래스 - 객체 간 예화(instantiation) 관계 구조를 채택한다는 점에서 서양의 전통적인 세계관의 영향을 받은 언어라고 간주할 수 있고, 또한 여러 개체 혹은 서브 클래스를 한 층 더 추상화 하여 클래스화 한다는 점에서 위에서 살펴본 고전 범주화 이론을 채택한다고 볼 수 있다.

Relation(Ideas - Appearance) ↔  Relation(Class - Object)

가족 유사성 이론 (Wittgenstein)

[What] 모든 멤버가 그것들을 포괄하는 범주를 정의하는 공통 속성을 갖고 있지 않아도 그 멤버들은 하나의 가족으로서 서로 관련될 수 있다. 가족 유사성 개념의 기본 발상은 어떤 가족의 성원들이 어떤 점들에서 서로 닮았지만 다른 점들에서는 닮지 않았다는 사실에 의존한다.

위 9 명의 남자는 모두 한 가족이다. 이들은 9 명 모두에게 적용되는 공통 속성을 갖고 있지는 않지만 우리는 이들을 하나의 가족이라고 받아들일 수 있다. 다시 게임 사례를 보자. 비트겐슈타인에 따르면, 게임에 대한 공통 속성은 없다.

각 게임은 서로 “복잡한 유사성의 망"으로 연결되어 있을 뿐이다. 예컨대, 테니스, 배구, 축구는 모두 공을 이용하는 스포츠라는 점에서 부분적으로 유사하다. 반면에, 카드 게임은 이들과의 유사성이 거의 없어 보인다.

원형 이론 (Rosch)

로쉬의 원형 이론은 가족 유사성 이론으로부터 영향을 받았다고 한다. 원형 이론에 따르면, 범주란 전형적인 보기인 ‘원형'과의 유사성 정도를 통해 결정된다. (1) 범주의 구성원들은 공통 속성이 없다, (2) 범주 간 경계는 모호하다, (3) 범주의 구성원들은 원형을 중심으로 상대적으로 등급화된 내적 구조를 가진다.

사례1

예컨대, 위 그림에서, Prototypes 행에 있는 원판과 상대적으로 유사한 다른 원판들이 아래로 줄지어 있다. 이들은 프로토타입과 얼마나 유사한지에 대해 등급화된 내적 구조를 갖는다. 하지만 위 사례는 비교적 범주 간 경계가 분명한 경우이다.

사례2

또 다른 예인 사례2는 Robin과의 유사성 정도에 따라 각 범주의 원형 간 거리가 주어진다. 예컨대, Robin은 작고 날 수 있고 꼬리가 길다는 점에서 Dove와 유사한 반면, 날지 못하고 큰 타조와 다르다. 이들 범주 간 경계는 사례1과 달리 모호한 편인 반면, 이들은 전형에 대한 유사성 정도에 따라 등급화된 내적 구조를 갖고 있다.

가족 유사성 이론과 원형 이론의 차이

이들은 다른 이론이다. 이들을 가르는 중요한 차이는 원형 이론에서는 기준이 되는 원형이 있다는 것이다. 반면에, 가족 유사성 이론은 그런 기준이 없다. 나아가, 당연히도, 가족 유사성 이론은 그런 원형을 출발점으로 범주 구성원들의 상대적 등급화도 시도하지 않는다.

4. 프로토타입 이론을 통해 설명되는 프로토타입 OOP

프로토타입 이론에서 주목할 점은 객체가 공통 속성의 규정에 의해 분류되는 것이 아니라, 원형에 의해 분류된다는 것이다. 이것은 우리가 효율적으로 객체 간 관계를 설계할 수 있게 해준다. 왜냐하면 우리는 하나의 객체(참새)를 정의한 뒤, 그것의 속성을 다른 객체들(비둘기, 제비 등)에 상속 또는 체이닝하기만 하면 된다. 프로토타입 OOP 에서는 객체들 간의 분류 작업은 추상적인 클래스 차원에서의 복잡한 상속 관계를 통해 고려되지 않는다. 여기서는 객체 수준에서 객체를 수정하고 발전시킬 수 있다. 프로토타입 개념을 이용하여 OOP를 하면, 우리는 객체 간 분류를 하지 않고 단지 그것들 간의 유사성만을 고려한다. 즉, JS에서는 선험적인 수준의 클래스 간 관계 모델링이 요구되지 않는다. → “프로토타입 언어에서는 ‘분류’를 우선하지 않는다. 생성된 객체 위주로 유사성을 정의한다.”[9]

하지만 이 점은 사실 프로토타입 OOP가 지닌 단점일 수 있다. 실제로, 이 논제를 처음으로 소개한 [8]에서는 프로토타입 OOP가 클래스 OOP의 한계를 극복하지는 못한다고 말한다. 이 프로토타입 OOP는 객체의 재사용성을 증대시키고, 다이나믹하게 객체들을 연결시켜주기 위한 대안적인 프로그래밍 방법론일 뿐일 수 있다.

예컨대, 아래 코드와 같이, 프로토타입 OOP를 이용하면, 포유류라는 선험적 클래스 수준에서의 관계 모델링을 하지 않고서도, Dog-Whale-Cat를 관련지을 수 있다. 그 방법은, 일례를 들자면, 먼저 프로그래머가 Dog라는 객체를 만든 뒤, Whale도 Dog와 유사한 일부 특성이 있다고 판단되고 이들을 관련 짓는 것이 유의미하다고 판단하면, Whale에 Dog를 상속 시키고 Dog와 다른 속성 값을 갖는 부분은 원하는 값(’sea’)으로 바꿔주는 것이다.

function Dog(){
    this.redBlood = true;
    this.suckling = true;
    this.habitat = 'land';
}

const dog = new Dog()

function Whale(){
}

Whale.prototype = dog

const whale = new Whale()
console.log(whale.redBlood, whale.suckling, whale.habitat) // true true land

whale.habitat = 'sea'// 원하는 값 바꿔줌
console.log(whale.redBlood, whale.suckling, whale.habitat) //true true sea

function Cat(){}
const cat = new Dog()
console.log(cat.redBlood, cat.suckling, cat.habitat) // true true land

5. 자바스크립트에서의 프로토타입

정의: 프로토 타입이란 다른 객체와 공유되는 속성을 제공하는 또 다른 객체를 뜻한다. 

  • 객체 A, 또 다른 객체 a,b,c,d가 있다.
    • A의 프로토타입 Z를 또 다른 객체 a,b,c,d에 체이닝할 때
      • (ex) Z.prototype = a.prototype
    • 프로토타입 Z는 A,a,b,c,d 내 속성을 담고 있는 객체가 된다.

프로토타입의 메커니즘

자바스크립트에서의 객체(Object)는 내부적으로 (1) 생성자의 프로토타입 속성을 참조하여, 그 생성자 함수로부터 (2)생성된 여러 객체들 간 프로토타입 내 속성을 공유한다.

function Person(name, blog){
    this.name = name
    this.blog = blog
}

Person.prototype.getName = function(){ // ------ [L5]
    return this.name;
} 

let david = new Person("david","jysden.com") // ------ [L9]  
let tony = new Person('tony','none')

console.log(david.getName()) // david
console.log(tony.getName()) // tony
  • Point (1),(2) 는 L5, L9에 해당한다.
    • 즉, 위 코드 내 L9, L10에서 생성된 (1) 객체는 david, tony는 내부적으로 이 prototype을 참조하여 prototype 객체가 가지고 있는 (2) getName() 함수(느슨한 의미로 속성; 원래는 메소드)를 사용할 수 있다.
      • 가능한 이유
        • L5에서, Person.prototype constructor.prototype으로 접근이 가능하기 때문에, L5에 의해 Person 생성자 함수에 getName 메서드가 추가되었기 때문이다. 그러면, Person으로부터 인스턴스화된 모든 객체는 Person 내 메서드 (느슨한 의미로 속성)를 공유받을 수 있다.

디버깅을 통해 명료화해보기

L5의 실행에 의해, 아래 prototype getName 메서드가 추가됨을 확인할 수 있다.

[DEBUG로 확인]

L9의 실행에 의해, 생성된 david 객체는 “__proto__” 안에서, 해당 생성자 함수로부터 만들어진 모든 객체(david,tony, ...)가 공유받을 prototype 이 만들어진다.

요약 및 정리

주목할 기본적인 점은 아래 “proto” 내 constructor는 생성자 함수 자체(Person)라는 점이다. 이 사실은 아래 논의에서 더 분명해진다.

객체와 프로토타입 내부 속성 간 함축적 연결

  • 왜 함축적 연결인가?
    • 객체는 프로토타입 내부 속성으로 접근 가능하지만 속성값을 변경할 수는 없다는 의미에서 이들 간 연결 관계는 함축적이다.
      • [접근 측면] 인스턴스화된 객체(david)은 생성자 함수 Person의 prototype 내 getName 메서드로 접근할 수 있다.
      • [수정 가능성 측면] 하지만 객체 david으로 getName의 속성값을 변경할 수는 없다.

함축적 연결에 대한 가능한 반론

아래 코드를 보면, david.gender가 male에서 female로 바뀌었다. 이것은 객체에서 프로토타입 내부 속성을 변경했음을 함의하는 것이 아닌가?

function Person(){};

let david = new Person();
let ethan = new Person();

Person.prototype.gender = 'male'; // -- [6]
console.log(david.gender) // male
david.gender = 'female' // -- [8]
console.log(david.gender) // female

답변

그렇지 않다. 아래 디버깅 결과를 보자. 아래를 참고할 때, L8에서, david.gender=’female’ 는 Person.prototype.gender 내 할당된 ‘male’을 변경하는 것이 아니라, 새로 추가된 원시 타입의 david.gender 값을 ‘male’에서 ‘female’로 바꾸는 것일 뿐임을 알 수 있다.

 

프로토타입 체이닝

프로토타입 체이닝이란 프로토타입 체이닝이란 객체들의 연쇄라고 요약될 수 있다. 프로토타입 체이닝으로 얻을 수 있는 효과는 이렇다: 위 정의에서 본 “프로토타입 체인”에 의해, 가령 A 객체 내 프로토타입 F를 상속받는 어떤 객체도 F 의 속성에 접근할 수 있다. (지금 용어 ‘속성’은 메서드, 변수 등을 모두 포함하는 객체 내에 포함되는 무언가를 지칭하기 위해 사용되고 있다.)

function Car(){
    this.wheel = 4;
    this.beep = 'beep'
}

Car.prototype.go = function(){
    console.log(this.beep)
} // -- [ Car의 프로토타입에 go method 추가] 

function Truck(){
    this.wheel = 6;
    this.beep = 'HOOONK!'
}

Truck.prototype = new Car() // -- L15 [프로토타입 체이닝: 프로토타입을 이용한 상속]

let truck = new Truck(); // -- 인스턴스화

truck.go() // HOOONK!
  • 위 예시에서 보면, L15에서 프로토타입 체이닝이 일어나는 것을 볼 수 있다. 이와 같이, 트럭의 프로토타입에 자동차 객체를 체이닝시키면, Truck 생성자 함수에 명시적으로 go 메서드를 추가하지 않더라도, Car가 가지고 있는 프로토타입 내 메서드인 go를 Truck에서도 사용할 수 있다.
  • Truck 객체는 어떻게 go 메서드의 존재를 알았는가? [객체 내 속성 탐색 순서]

 

6. 프로토타입의 장단점

[장점] 생성자를 사용하여 다수의 객체를 만들고자 할 때, 프로토타입을 사용하면 아래 두 가지 이점을 얻을 수 있다.

  • 메모리 효율성
    • 생성자 안에서 속성으로 부여하는 방식은 객체를 생성할 때마다 새로운 function을 생성한다.
      • → 적은 수의 객체만을 생성할 시 생성자 안에서 속성을 부여해도 괜찮다.
      • → 많은 수의 객체를 생성해야 할 시 프로토타입을 이용하는 것이 좋다.
        • 프로토타입은 모든 객체가 한 객체를 공유하고 있어서 메모리 효율적이다.
        function Person(name,blog){
            this.name = name
            this.blog = blog
            this.getName = function(){
                return this.name
            }
            this.getBlog = function(){
                return this.blog
            }
        }
        
        let david = new Person('david','jysden.com')
        let john = new Person('john', 'asdf.com')
        console.log(david.getBlog()) // jysden.com
        
  • 유지 보수 용이성
    • 많은 수의 객체를 생성한 뒤 그것들의 속성을 수정해야 할 시 프로토타입을 이용하면, 프로토타입 속성만을 수정하면 그 속성을 공유하는 모든 객체의 속성 모두를 한꺼번에 수정할 수 있다 (시간 효율적)

[단점] 하지만 프로토타입을 사용하면 아래 두 가지 단점을 얻을 수 있다.

  • (1) 프로토타입은 그 자체로 이해하기 어렵다.
    • 코드를 읽는 사람이 프로토타입에 대한 거부감이 들 수 있다.
  • (2) 프로토타입 체인을 따라서 속성 탐색 시간이 늘어날 수 있다.
    • 속성 탐색: [해당 객체(ex. Truck)의 속성인지 탐색] → [속성 x 시, 해당 객체의 constructor.prototype 탐색] → [여전히 없을 시, 다시 상속해준 객체(ex. Car)의 prototype 탐색] ...
      • 따라서, 자주 접근해서 참조할 속성은 객체 자체의 속성으로 추가하여 프로토타입 체인 검색 시간을 최소화 하는 것이 프로토타입 체이닝을 활용하는 현명한 방법일 것.
        • 처음에 객체를 만들 당시, 그 객체를 만드는 생성자 함수 또는 클래스 내에 속성을 추가하라

 

References

[1] 양성익, “속 깊은 자바스크립트”, 루비페이퍼, 2016년

[2] https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Classes

[3] http://www.tcpschool.com/javascript/js_object_prototype

[4] https://devbox.tistory.com/entry/JavaScript-생성자와-new

[5] https://www.zerocho.com/category/JavaScript/post/573c2acf91575c17008ad2fc

[6] https://jwprogramming.tistory.com/121

[7] Lakoff, G. (1987): Women, fire and dangerous things: What categories reveal about the mind, London.

[8] Taivalsaari, Antero. "Classes vs. Prototypes Some Philosophical and Historical Observations." Journal of Object-Oriented Programming 10.7 (1996): 44-50

[9] https://medium.com/@limsungmook/자바스크립트는-왜-프로토타입을-선택했을까-997f985adb42

[10] 최용화. "플라톤의 이데아론과 객체지향 패러다임 비교 연구." (2008)

[11] 윤석만 ( Seokman Yoon ). 2003. “원형이론”과 언어의미. 언어와 언어학, 31(0): 95-114