4. ES6 이전의 자바스크립트에서는 OOP를 어떻게 구현했을까?

piecemaker·2020년 8월 12일
1
post-thumbnail

시리즈의 이전 세 포스팅들이 모두 개념에 대한 이해를 중심으로 작성되었다면, 본 포스팅부터는 실제로 자바스크립트에서 객체 지향 프로그래밍을 어떤 방식으로 사용해야 하는지에 대해 코드 중심으로 배워본다.

우선 본 포스팅에서는 ES6 이전의 자바스크립트가 지원했던 'prototype' 문법을 사용한 객체 지향 프로그래밍에 대해 다뤄볼 것이며, 특히 객체 지향 프로그래밍의 핵심인 '상속'을 어떻게 구현하는지에 대해 중점적으로 알아볼 것이다.

객체 생성하기

이전 포스팅에서도 설명했지만, prototype 기반 객체 지향 프로그래밍에서 '클래스'의 개념을 대신하기 위해 채택된 것이 바로 '함수'다. 따라서 Car이라는 클래스를 정의한 후 Car 타입 객체를 생성하고 싶다면, 우선 다음과 같이 Car 함수를 생성한 후

function Car(name, speed) {
    this.name = name;
    this.speed = speed;
}

다음과 같이 new 키워드를 사용해 Car 타입의 객체인 myCar를 생성할 수 있다.

const myCar = new Car("Tico", 132);

하지만 단순히 속성만을 가지는 클래스가 아니라 메서드를 정의하고 싶다면 prototype의 개념을 사용해야 하는데, 이는 같은 타입을 가지는 모든 객체들이 동일한 하나의 메서드를 사용하도록 하기 위함이다. Car 타입 객체가 Drive라는 메소드를 가지도록 하려면 다음과 같이 Car 함수의 Prototype 객체에 Drive 메서드를 추가해야 한다.

function Car(name, speed) {
    this.name = name;
    this.speed = speed;
}

Car.prototype.Drive = function () {
    return this.speed * 60;
}

상속 구현하기

사실 ES6 이전 문법에서도 단순히 객체의 속성과 메서드를 정의하고 객체를 생성하는 부분은 크게 어렵지 않다. 유일하게 주의해야 할 점은 메서드를 정의할 때 함수의 Prototype 객체에 정의해야 한다는 점이다.

하지만, 상속을 구현하는데 있어서는 ES6 이후의 'class' 문법에 비해 난이도가 상당히 높은 편이다. 우선 prototype이 무엇인지에 대한 완벽한 이해가 뒷받침되어야 하며, 자잘하게 신경써야 할 것들이 많기 때문이다. Prototype 기반으로 상속을 제대로 구현하기 위해서는 크게 총 세 단계를 거쳐야 하며, 지금부터 한 단계씩 차례로 살펴보도록 하자.

1. call 혹은 apply 함수로 부모의 속성 상속받기

가장 간단하면서 가장 먼저 해야 하는 일은 부모 함수의 속성을 자식 함수에서 상속받는 것이다. 다음과 같이 Car 함수가 존재하고, 이 Car 함수를 Benz 함수에서 상속받고 싶다고 가정하자.

function Car(name, speed) {
   this.name = name;
   this.speed = speed;
}

Car.prototype.Drive = function() {
    return this.speed * 60;
}

이 때 우리가 다음과 같이 Benz 함수에서 Car 함수에 대해 call 혹은 apply 함수를 호출하면,

function Benz(name, speed) {
    Car.apply(this, arguments); // Car.call(this, name, speed);
}

new 키워드를 통해 Benz 타입의 객체를 생성할 때 this생성한 Benz 타입 객체를 가리키게 되어 해당 객체에 name 및 speed 속성이 생기게 된다.

자바스크립트에서 call과 apply가 무슨 역할을 하는 함수인지 모르시는 분들은 다음 MDN 문서 callapply를 참고하도록 하자.

2. Object.create 함수로 부모의 메서드 상속받기

call이나 apply를 사용하면 부모 함수의 속성을 상속받을 수 있으나, 부모 함수에 정의된 메서드까지 상속받을 수는 없다. 아시다시피 prototype 기반 객체 지향 프로그래밍에서는 메서드가 함수의 Prototype 객체에 정의되기 때문이다. 따라서 자식 함수의 Prototype 객체가 부모 함수의 Prototype 객체의 메서드를 상속받도록 코드를 작성해야 한다.

자식 Prototype 객체에서 부모 Prototype 객체의 메서드를 가져올 수 있는 방법으로 생각해볼 수 있는 가장 쉬운 방법은 다음과 같이 자식 함수의 Prototype 객체 자체를 부모 함수의 Prototype 객체로 변경해버리는 것이다.

Benz.prototype = Car.prototype;

이렇게 코드를 작성하면 부모 함수 Car의 Prototype 객체에서 속성이나 메서드가 추가되는 등의 변경 사항이 발생하면 자식 함수인 Benz에도 이 변경사항이 즉각적으로 반영된다는 장점이 있다.

문제는 Benz의 Prototype 객체에 속성이나 메서드를 추가하는 경우다. 부모의 변화가 자식에게 바로 반영되는 것은 바람직하지만, 위와 같이 Benz의 Prototype 객체를 아예 Car의 Prototype 객체로 바꿔버리면 자식의 변화 또한 부모에 반영되어 버린다. 이는 객체 지향 프로그래밍의 개념에 위배되기 때문에 위와 같은 방법은 사용할 수 없다.


부모 함수에 정의된 메서드를 상속받는 가장 적합한 방법은 Object 객체의 create 함수를 사용하는 것이다. Object.create() 함수는 인자로 객체를 받으며, 인자로 받은 객체를 Prototype 객체로 하는 새로운 객체를 생성해 리턴해준다. 우리가 이 예시에서 Car의 Prototype 객체를 A라고 부른다고 가정하면,

function Car(name, speed) {
   this.name = name;
   this.speed = speed;
}

Car.prototype.Drive = function() {
    return this.speed * 60;
}

function Benz(name, speed) {
    Car.apply(this, arguments);
}

Benz 함수가 상속받고자 하는 Car 부모 함수의 메서드는 A 객체에 존재한다. 따라서,A 객체를 Object.create() 함수의 인자로 주어, A 객체를 Prototype 객체로 하는 새로운 객체 B를 생성해주고 이를 Benz 함수의 Prototype 객체로 설정해주는 다음과 같은 코드를 작성하면,

Benz.prototype = Object.create(Car.prototype);

Car 함수와 Benz 함수의 구조는 다음과 같아진다.

이렇게 Benz의 Prototype 객체가 된 B 객체의 __proto__ 속성이 A 객체를 가리키게 되면서 A 객체와 B 객체 사이에 계층 구조가 생기게 되며, 이를 통해 Car 함수와 Benz 함수간에 진정한 의미의 부모-자식 관계가 생성되게 된다.

이제 Benz 객체에 대해서 Drive 메서드를 호출하면, 자바스크립트는 먼저 Benz 객체에서 Drive 메서드를 찾아보고, 없다면 Object.create로 생성된 Benz의 Prototype 객체 B에서 찾아보고, 또 없다면 B 객체의 __proto__ 속성이 가리키는 Car의 Prototype 객체 A에서 찾아본다.

Object.create() 함수를 통해 Benz 객체에서 Car Prototype 객체의 Drive 메서드를 사용할 수 있게 된 것이므로, Benz가 부모인 Car의 메서드를 상속받게 된 것이다.

3. 자식의 Prototype 객체의 constructor 변경하기

부모로부터 속성과 메서드 모두 상속 받았는데, 더 뭘 할게 남았는지 의문을 가지실 것이다. 상속은 위 두 단계를 통해 정상적으로 완료되었다. 하지만, 우리가 Object.create() 함수를 상속에 사용함으로써 한 가지 문제가 발생했다.

위에서 본 이미지를 다시 한번 보자.

Benz 함수의 Prototype 객체 B를 유심히 보자. 뭔가 이상하지 않은가?

우리는 Prototype 객체의 constructor는 기존 함수를 가리킨다는 사실을 알고 있다. 따라서 B 객체의 constructor는 Benz 함수를 가리켜야 정상이다. 하지만, 지금의 구조에서는 B 객체의 constructor가 Benz 함수가 아니라 Car 함수를 가리키고 있다. 왜 이런 문제가 발생하는 것일까?

이 문제는 우리가 B 객체를 Object.create(Car.prototype)을 통해 생성했기 때문에 발생한다. 우리가 A 객체를 Prototype 객체로 가지는 새로운 객체를 만들 때 Object.create() 함수 호출 시 Benz 함수에 대한 아무런 정보를 주지 않았기 때문에, 자바스크립트는 새로 만들어진 B라는 객체의 constructor에 무슨 값을 넣어줘야 하는지 모른다. 따라서 Object.create()는 인자로 들어온 Car.prototype의 constructor의 값을 그대로 복사해 B 객체의 constructor 값으로 설정해주며, 이 때문에 B 객체의 constructor가 A 객체의 constructor가 가리키는 Car 함수를 가리키게 된 것이다.

이를 해결하기 위해 다음과 같은 코드 바로 아래에

Benz.prototype = Object.create(Car.prototype);

다음과 같이 Benz의 Prototype 객체의 constructor가 Benz 함수를 가리키도록 수동으로 변경해주는 코드를 추가해야지 비로소 상속의 구현이 마무리된다.

Benz.prototype.constructor = Benz;

정리

이로써 ES6 이전 자바스크립트 문법으로 객체 지향 프로그래밍 기법을 사용하는 방법에 대해 모두 알아보았다. 물론 이 개념에 대해 더 깊게 공부하고 싶다면 메서드 오버라이딩을 어떻게 구현하는지 등에 대해서 더 찾아볼 수도 있을 것이다.

하지만 ES6 이후 자바스크립트 문법을 사용하는 우리는 사실상 Prototype을 직접 사용해 객체 지향을 구현할 일이 거의 없으며, 실상 구현할 일이 있다고 하더라도 기본적인 개념은 본 포스팅에서 작성한 범위에서 벗어나지 않기 때문에 궁금한 부분만 더 찾아보는 방식으로 필요할 때 마다 공부를 하는 것이 효율적이라고 생각한다.

그럼, 마지막 포스팅에서는 ES6 이후에 도입된 class 문법으로 객체 지향 프로그래밍 기법을 사용하는 방법에 대해 알아보겠다.

profile
풀스택 지망생

0개의 댓글