객체지향언어(?)로서의 Javascript 🪄 (MDN 분석, Java 언어와의 비교)

Dasol Kim·2022년 2월 10일
0

객체 지향 언어(Object Oriented Language)로서 대표적인 언어가 Java인 것은 누구나 한번쯤은 들어보았을 것이다. Java와 비슷한 이름을 가진 Javascript 역시도 객체 지향 언어의 특성을 보유하고 있다. 여기서 두 언어가 OOP(=Object Oriented Programming)을 구현하는 방식이 크게 다르다는 것에 주목해야 한다.

'class-based' Java vs 'prototype-based' Javascript

Java가 OOP(=Object Oriented Programming)을 구현하는 방식은 class-based, Javascriptprototype-based이다. 여기서 전형적인 OOP 구현은 'class-based'이기 때문에 javascript를 완전한 객체지향 언어로 보기엔 어려운 측면이 있다.

필자가 대학교 3학년때 '자바프로그래밍'이라는 과목을 수강하면서 정리해놓은 객체 지향 언어로서 특징이 있는데, 이를 요약하면 다음과 같다.

- encapsulation (캡슐화)
- inheritance (상속)
- polymorphism (다형성)

앞에서도 언급하지만 위의 3가지 특징은 class로 구현된다. 자바에서 object는 class의 인스턴스라고 할 수 있고, 모든 클래스는 최상위 클래스인 Object를 상속한다.

object가 class의 인스턴스라는 점에서 class는 하나의 '템플릿'같은 존재라고 할 수 있다. Javascript는 ES6에 이와 유사한 class라는 개념을 새로 도입했지만, class가 도입되기 전 Javascript의 기본적인 '템플릿'은 constructor라고 불리는 함수였다. 이번 포스팅을 통해 constructor function이 무엇인지, 그리고 위에서 언급한 prototype-based에서 얘기하는 prototype이 무엇인지에 대해서 자세하게 알아보자.



Constructor function

아래 드림코딩 강좌의 자바스크립트 코드 일부를 보면서 Constructor function이 어떻게 class와 비슷하게 템플릿 역할을 수행하고 있는지 이해해보자.

'use strict';
// 1. Objects
// one of the JavaScript's data types.
// a collection of related data and/or functionality.
// object = { key : value };
const obj1 = {}; // 'object literal' syntax
const obj2 = new Object(); // 'object constructor' syntax

function print(person) {
  console.log(person.name);
  console.log(person.age);
}

const dasol = { name: 'dasol', age: 25 };
print(dasol);

// with JavaScript magic (dynamically typed language)
// can add properties later
dasol.hasJob = true;
console.log(dasol.hasJob); // true

// can delete properties later
delete dasol.hasJob;
console.log(dasol.hasJob); // undefined

// 2. Computed properties
// key should be always string
console.log(dasol.name);
console.log(dasol['name']);
dasol['hasJob'] = true;
console.log(dasol.hasJob);

function printValue(obj, key) {
  console.log(obj[key]); // console.log(obj.key)는 에러
}
printValue(dasol, 'name');
printValue(dasol, 'age');

// 3. Property value shorthand => Using Constructor Function
const person1 = { name: 'bob', age: 2 };
const person2 = { name: 'steve', age: 3 };
const person3 = { name: 'dave', age: 4 };
const person4 = new Person('dasol', 25); // 'Person' is a constructor function
console.log(person4); // { name: 'dasol', age: 25 }

// 4. Constructor Function
function Person(name, age) {
  // this = {};
  this.name = name;
  this.age = age;
  // return this;
}

위와 같이 Javascript는 class를 생성하지 않고도 객체를 직접 생성할 수 있다. 객체는 {}에 key-value들의 쌍 집합으로 구성한 object literals 혹은 new 키워드와 함께 Object라는 함수를 호출하는 방법을 사용할 수도 있다.
여기서 중요한 것은 자바스크립트는 Dynamically typed language이기 때문에 객체에 직접 접근하여 새로운 프로퍼티를 추가하고 삭제할 수 있다는 점이다. 참고로 자바에서는 클래스에서 정의한 프로퍼티를 원형으로 객체 인스턴스를 생성할 때 초기화 함수를 호출하여 다형성을 구현할 수 있지만 객체를 통해 추가, 삭제하는 것은 불가능하다.

Object의 객체를 개별적으로 생성할 수도 있겠지만(위의 코드에서 person1, person2, person3 처럼), 또 다른 이름의 함수를 생성하여 이를 기반으로 여러 개의 객체를 생성할 수도 있다. 그 함수가 바로 Person과 같은 형태이다. 4번에서 보여주는 것처럼 그 형태가 일반적인 함수와 비슷하지만 함수명의 첫번째 문자가 관습적으로 대문자(Person)로 시작하고, 함수의 body 안에 객체를 생성하고, 이를 리턴하는 statement가 생략되어 있다. 이러한 함수를 constructor function이라고 하며, 함수를 생성하는 동시에 new 라는 키워드를 통해 함수의 객체를 생성할 수 있게 된다. person4 객체를 이러한 방법으로 생성하였다.

📃 constructors in JavaScript provide us with something like a class definition, enabling us to define the "shape" of an object, including any methods it contains, in a single place. But prototypes can be used here, too. For example, if a method is defined on a constructor's prototype property, then all objects created using that constructor get that method via their prototype, and we don't need to define it in the constructor. (MDN)

MDN의 설명처럼, constructors, 즉 constructor function은 class와 비슷한 역할을 한다. 다만 일반적인 객체 지향 언어와 다른 점은 prototype을 사용한다는 점이다. 프로토타입은 모든 객체가 포함하고 있으며, 보통은 프로퍼티.__proto__ 와 같이 접근할 수 있다. 이를 기반으로 클래스의 상속을 비슷하게 구현할 수 있는데, prototype에 대한 자세한 설명은 바로 다음 챕터에서 보도록 하자.



prototype with constructor

필자는 MDN 문서와 함께 해당 블로그를 함께 참조하였는데, 프로토타입 기반 언어의 특성을 전반적으로 이해할 수 있었다. 앞으로 써내려갈 내용은 이들을 종합적으로 이해하여 필자가 재구성한 내용을 중심으로 작성될 것이다.

📃 the prototype chain seems like a natural way to implement inheritance. For example, if we can have a Student object whose prototype is Person, then it can inherit name and override introduceSelf()....The prototype chain's behavior is less like inheritance and more like delegation. (MDN)

우리의 목표는 위의 MDN에서 언급하는 prototype chain과 이것이 왜 inheritance보다는 delegation에 가까운지를 이해하는 것이다.

우선 Prototype은 개념적으로 prototype objectprototype link로 나누어서 생각하면 편하다. 우리가 function Person() { ... }을 통해 constructor를 생성할 때, 이와 동시에 이것과 관련된 prototype object가 생성된다. 브라우저에서 html을 실행시키고 콘솔창에 Person.을 입력하게 되면 Person의 모든 속성들을 볼 수 있게 된다. 그 중 하나가 바로 prototype(ex. Person.prototype)이라는 속성이다. prototype은 함수를 통해 생성된 인스턴스인 객체가 아닌 함수 그 자체에만 주어지는 속성인데, 이는 또 다른 객체인 prototype object를 가리키고 있다.

function Person(name, age) {
    this.name = name;
    this.age = age;
}

Person.prototype.changeName = function(newName) {
    this.name = newName;
}

위의 코드를 예로 보면 우리가 Person이라는 함수를 정의하면서 동시에 Person.Prototype(편의상 이렇게 부르기로 하자)라는 또 다른 객체가 생성된다. 그리고 Person의 속성 중 하나인 prototype을 통해 changeName이라는 메소드를 하나 추가해주었다. 객체 지향 언어의 특성 중 하나인 다형성을 실현하기 위해 보통 필드를 함수 안에 직접 정의해놓고 메소드를 prototype의 프로퍼티로서 정의해놓는다.


위의 도식화된 그림이 보여주는 것처럼 Person의 prototype은 Person.Prototype을 가리키고, 또 이것의 constructor은 Person 함수를 가리키게 된다. 여기서 prototype link의 개념이 등장하는데 바로 __proto__로 표현된 것에 해당한다. __proto__ 는 자신의 상위 Prototype을 가리키게 되며, 상위 prototype object 역시 객체이고 모든 객체는 __proto__ 를 포함하므로 결국 이러한 일련의 과정을 통해 prototype chain 이 구현된다고 할 수 있다. 즉, prototype link를 통해 상속의 개념이 구현된다고 볼 수 있다. 최상위 prototype object인 Object.Prototype의 __proto__ 는 null을 참조하고 있기 때문에 여기서 prototype chain이 끝나는 것을 알 수 있다.

const dasol = new Person("dasol", 25);
// dasol의 __proto__는 자신의 조상의 Person의 Prototype Object, 즉 Person.Prototype을 가리키고 있다.

dasol.changeName("coder");
// property chaining에 따라 dasol의 own property에 changeName 메소드가 있는지 먼저 검토하고
// 그 다음, 프로토타입 링크인 __proto__가 가리키는 Person.prototype에 changeName 메소드가 있는지 검토한다.
// 여기에 메소드가 존재하므로 해당 객체의 name 프로퍼티를 변경하게 된다.

console.log(dasol.name); // coder




Person에는 changeName이라는 메소드가 직접적으로 존재하지 않는다. 다만 위에서 prototype의 속성 중 하나로 추가적으로 정의하였기 때문에 이것의 객체를 통해 메소드에 접근할 수 있다. property chaining에 따라 객체의 own property를 먼저 검토하고 그 다음 prototype link에 따라 prototype object를 순차적으로 검토하며 최종적으로 null에 도달할 때까지 프로퍼티를 찾지 못하면 undefined를 반환한다. 이는 마치 '위임'이라는 뜻의 delegation으로 볼 수 있으며 inheritance 대신 이 용어가 더 적합하다고 볼 수 있는 근거이다.

위의 코드에서 Person의 객체인 dasol을 생성하였기 때문에 그림에 추가하였다. 편의상 Person.Prototype의 __proto__ 가 가리키는 Object.Prototype는 그림에서 생략하였다.

Person의 prototype가 Person.Prototype를 가리킨 것처럼, Person의 객체인 dasol의 __proto__ 역시 Person.Prototype을 가리킨다. 이처럼 인스턴스를 생성하면 조상 함수의 prototype object를 prototype link를 통해 가리킬 수 있게 된다.




아래 코드는 싱글 객체에 대한 Prototype Object를 직접 설정하는 방법과 constructor function에 대한 Prototype Object를 직접 설정하는 방법 총 2가지를 보여주고 있다.

// 1. 싱글 객체 인스턴스의 프로토타입 오브젝트 변경하기
// Prototype Object 역할을 하는 'greeting' object를 생성한다.
const greeting = {
	hello() {
		console.log('Hello!');
	},
};

let object = Object.create(greeting);
object.hello(); // Hello!

do {
	object = Object.getPrototypeOf(object);
	console.log(object);
} while (object);
// 콘솔창 결과를 보면 가장 먼저 Greeting Prototype, 그 다음엔 Object Prototype, 그 다음엔 null을 출력했다.
// 예상했던 결과와 같으며 이는 prototype chaining을 잘 보여주고 있음을 알 수 있다.

// 2. construction function의 프로토타입 오브젝트 변경하기
function Person(name, age) {
	this.name = name;
	this.age = age;
}

Person.prototype.changeName = function (newName) {
	this.name = newName;
};

Person.prototype = greeting;
Person.prototype.constructor = Person; // prototype object에는 constructor을 담고있기 때문에 Greeting으로 초기화해준 후에는 constructor을 기존의 Person으로 set해줘야 한다.

const stranger = new Person('unknown', 20);
stranger.hello(); // Hello!
// stranger.changeName('gabby'); // TypeError. 왜냐하면 prototype object가 Person.Prototype에서 Greeting.Prototype으로 변경되었기 때문에 참조할 수 없다.

// 추가: constructor function의 prototype 프로퍼티에 prototype object 그 자체를 할당할 수 있다.
// 이 경우에는 prototype의 constructor를 설정해 줄 필요 없음.
function CoffeeMachine() {}
CoffeeMachine.prototype.makeCoffee = () => {
    console.log('Making Coffee...');
}

function LatteMachine() {}
LatteMachine.prototype = Object.create(CoffeeMachine.prototype);
const latte = new LatteMachine();
latte.makeCoffee(); // Making coffee...



✏️ 정리하면, Javascript에서 객체를 세밀하게 구분한다면 싱글 객체 혹은 함수 객체로 나눌 수 있다. 그러나 싱글 객체 역시 Object라는 최상위 함수의 인스턴스로, 개발자가 임의로 지정한 constructor, 즉 별도의 템플릿이 아닌 new Object() 혹은 object literal을 통해서 생성된 객체이다. 싱글 객체의 프로토타입 링크는 디폴트로 Object.Prototype을 가리키고 있으며 함수 객체는 자신의 조상 함수의 Prototype 오브젝트를 가리키고 있다. constructor function과 prototype object의 관계를 이해하면 객체 지향 언어의 중요한 특징 중 하나인 상속(delegation과 유사한)을 비슷하게 구현할 수 있게 된다. 정리하면, 싱글 객체이든 함수 객체이든 이들은 모두 타입이 객체라는 점, 이 둘의 차이는 개발자가 정의한 템플릿 에서 파생이 되었는지의 유무이다. 중요한 것은 모든 객체 인스턴스는 프로토타입 링크를 통해 프로토타입 오브젝트를 가지고 있다는 사실이며 이러한 오브젝트를 통해 다양한 api를 활용할 수 있다는 사실이다!

✏️ 참고: 모든 prototype object의 prototype link는 Object.Prototype 이거나 null이다.

0개의 댓글