2014년 6월 30일 월요일

javascript 상속(Inheritance) - 의사 클래스 방식과 Object.create 메소드 의 연관성

오전 6:08 Posted by jonnung 1 comment
객체지향 프로그래밍에서 상속은 너무나 자연스럽게 다뤄지는 프로그래밍 기법이다.
상속은 코드 재사용의 한 형태이고, 부모 객체의 데이터 타입 체계를 그대로 전달 받기 때문에 개발 비용을 감소 시켜주는 장점이 있다

하지만 일반적인 객체지향 프로그래밍 언어와 다르게 자바스크립트는 클래스(class)가 없다. 상속(Inheritance) 메커니즘을 구현하기 위해서는 자바스크립트의 특징이라고 할 수 있는 프로토타입(prototype)을 기반으로해서 부모 객체를 자식에게 상속 할 수 있다.
이것은 다시 말해 객체가 다른 객체로 바로 상속 된다는 것을 의미 한다.

이번 포스트에서 정리할 내용은 자바스크립트의 상속 패턴중에서 함수의 프로토타입 프로퍼티를 활용한 방식이다.

지난번 javascript 함수 - 함수 객체의 프로토타입 프로퍼티 이해하기 에서 정리한 함수의 프로토타입 프로퍼티(prototype property)을 먼저 살펴보면 좋을 것 같다.

의사 클래스 방식 Pseudoclassical


의사 클래스(Pseudo class, 슈도 클래스라고 읽음)는 가짜 클래스 정도로 이해하자. 사실 이번에 처음 알게된 표현인데 너무 어렵게 생각했던 것 같다.
자바스크립트에서의 의사 클래스 방식은 클래스가 없는 자바스크립트가 클래스를 흉내내기 위해 작성하는 패턴이라고 생각하면 된다.

자바스크립트는 프로토타입적 특성에 맞게 객체에서 다른 객체로 직접 상속하는 방법을 갖기는 하지만 생성자 함수를 통해서 객체를 생성해야하는 조금 혼란스러운 단계가 있다.

좀 더 자세히 설명하자면, 객체의 프로토타입은 해당 객체가 생성 될때 사용한 객체의 원형에 대한 연결을 담고 있기 때문에 이미 생성된 객체의 프로토타입에 다른 객체를 바로 상속할 수는 없다.
다른 객체를 새로운 객체에 상속하기 위해서는 객체가 생성되기 전에 생성자 함수에서 함수객체의 프로토타입 프로퍼티에 객체를 직접 할당하는 식으로 상속을 구현하는 것이다.
(함수 객체의 프로토타입 프로퍼티에는 자신을 통해 생성될 객체가 참조할 객체의 원형을 갖고 있다.)

자, 그럼 이제 자바스크립트의 상속을 구현하기 위해 의사 클래스 방식으로 예제를 작성해보겠다.

다음 코드는 '여자친구가 당분간 연락하지 말자'고 했을때 당분간의 정확한 기간을 계산해주는 객체를 생성한다.

var Love_anger_formula = function () {
    this.__swear_word_constant = 18;  // 욕주율
    this.get_swear_word_constant = function () {
        return this.__swear_word_constant;
    };
};

var do_not_contact = function (contact_stop_time, lie_count) {
    this.contact_stop_time = contact_stop_time;  // 연락 두절 시간
    this.lie_count = lie_count;  // 거짓말 횟수
};

// do_not_contact 함수의 prototype property 객체를 Love_anger_formula 의 인스턴스로 대체한다.
do_not_contact.prototype = new Love_anger_formula();

var how_long = new do_not_contact(24, 2);

// how_long 객체에 새로운 메소드를 추가한다.
// '당분간'의 기간을 계산한 결과를 돌려준다.
how_long.get_remain_period = function () {
    var  swear_word_constant = this.get_swear_word_constant();
    return this.contact_stop_time * this.lie_count * swear_word_constant;
};

console.log(how_long.get_remain_period());
Do_not_contact 라는 생성자 함수를 정의 하고, 이 함수의 prototype을 Love_anger_formula 의 인스턴스로 대체하는 방식으로 의사 클래스 만들어서 객체를 상속했다.

더글라스 크락포드는 이런 의사 클래스 패턴을 객체지향처럼 보이게 고안 됐지만 마치 어디 외계에서 온 패턴과 같다고 말했다.
자바스크립트에 익숙하지 않는 개발자에게 친숙한 방식이 될 수도 있겠지만, 자바스크립트의 장점을 가리게 되는 방법이라는 말도 덧붙였다.

아무튼 이렇게 생성된 객체의 최대의 문제점은 바로 모든 속성들이 public 이라는 것이다. private은 없다.
how_long.__swear_word_constant로 부모 객체의 속성에 바로 접근해서 읽거나 값을 변경할 수 있다.

또한 생성자 함수를 new 연산자로 실행하지 않으면 this는 새로운 객체에 바인딩 되지 않고, 전역 객체(window)에 연결된다. new 연산자를 빼먹어도 어떠한 에러도 발생하지 않기 때문에 매우 심각한 문제가 발생할 수 있다.

이 문제에 대한 해결책(?)으로 더글라스 크락포드가 추천하는 방법은 2가지가 있는데 new 연산자로 실행해서 객체를 생성하는 생성자 함수의 이름은 첫글자를 대문자로 표기하는 것이다.
그리고 나머지 한가지는 new 연산자 사용을 피하는 것이다.

언어의 특징을 비교하는 차원에서 위 예제 코드와 동일한 기능을 하는 파이썬(python) 코드를 작성해 보았다.
LoveAngerFormula 클래스의 __swear_word_constant 변수는 완전 안전한 private 이다.

# -*- coding: utf8 -*-


class LoveAngerFormula():
    __swear_word_constant = 18  # 욕주율

    def get_swear_word_constant(self):
        return self.__swear_word_constant


class DoNotContact(LoveAngerFormula):
    def __init__(self, contact_stop_time, lie_count):
        self.contact_stop_time = contact_stop_time  # 연락 두절 시간
        self.lie_count = lie_count  # 거짓말 횟수
        self.swear_word_constant = self.get_swear_word_constant()

    def get_remain_period(self):
        """
        '당분간 연락하지 말자' 했을때 당분간의 정확한 기간을 계산
        :return: 기간(일)
        :rtype: integer
        """
        return self.contact_stop_time * self.lie_count * self.swear_word_constant


how_long = DoNotContact(24, 2)
print(how_long.get_remain_period())

Object.create() 전달한 객체를 프로토타입으로 새로운 객체를 생성하는 메소드


Object.create() 메소드로 만들어지는 객체는 프로토타입을 직접 지정해서 생성할 수 있는 편리한 기능을 제공한다.
이런게 있는지도 몰랐었는데 이미 표준에 포함되어 있는 메소드 이다!
크롬(Chrome)과 파이어폭스(Firefox)에서는 당연히 제공하고 있었고, IE는 뒤늦게 9부터 지원한다.

Object.create() 메소드가 생기게 된 배경은 new 연산자 사용이 '자바스크립트 스럽지 못하다'는 의견이 나오게 되면서 new 연산자 사용을 자제하기 위해 만들어졌다고 한다.

그런데 이 메소드를 지원하지 않는 브라우저를 위해서 작성하는 코드를 보면 조금 웃긴 사실을 알 수 있다.

if (typeof Object.create !== 'function' ) {
    Object.create = function (obj) {
        function F();
        F.prototype = obj;
        return new F();
    }
}
결국 Object.create()메소드도 내부적으로 F()라는 빈 함수를 만들고, 위에서 살펴본 의사 클래스 방식으로 F 함수의 프로토타입 프로퍼티에 전달받은 객체를 대체한다.
그리고 new 연산자로 생성한 객체를 돌려준다.

new 연산자 사용을 자제하기 위해서 만들어진 메소드는 결국 손바닥으로 하늘을 가리는 꼴이다.

마무으리


자바스크립트의 프로토타입 특징을 활용한 상속에 대해 정리 해봤는데 조금 찝찝한 느낌을 지울 수 없는 것은 사실이다.
하지만 함수의 프로토타입 프로퍼티로 클래스를 흉내내는 의사클래스방식과 Object.create()메소드의 연관성에 대한 부분은 확실히 알고 간다는 것에 매우 만족한다.




2014년 6월 23일 월요일

javascript 함수 - 함수 객체의 프로토타입 프로퍼티 이해하기

오전 8:17 Posted by jonnung No comments
앞으로 몇번에 걸쳐서 자세히 알아볼 부분은 모든 개발 언어에서 굉장히 많이 사용하고, 자바스크립트에서도 핵심이라고 할 수 있는 함수에 대한 부분이다.
그리고 이번 정리에서는 함수 객체와 프로토타입 연결 특성과의 관계에 대해서 중점적으로 알아볼 것이다.

객체의 프로토타입(prototype)


자바스크립트에서 숫자, 문자열, 불리언(true/false), null, undefied를 제외한 모든 값은 객체이다. 생소한 표현이지만 숫자나 문자열, 불리언은 메소드를 갖기 때문에 유사객체라고도 한다. 이 유사객체에 대한 부분은 다음 기회에 좀 더 조사해 볼 것이다.

더글라스 크락포드의 'Javascript The Good Parts'에서는 함수 객체에 대해 설명한 부분에서 프로토타입과 관련해서 설명한 부분을 요약해서 정리하자면 아래와 같다.
프로토타입에 대해 어느정도 알고 있더라도 이것을 보게 되면 상당히 혼란을 일으킬 가능성이 높다.

함수는 객체이다. 객체는 프로토타입 객체(Prototype Obeject)로 숨겨진 연결을 갖는 이름/값 쌍들의 집합이다. 객체 중에서 객체 리터럴로 생성되는 객체는 Object.prototype에 연결 된다.  반면 함수 객체는 Functon.prototype에 연결 된다. Function은 다시 Object.prototype에 연결 된다.  모든 함수 객체는 prototype이라는 속성이 있다. 이 속성의 값은 함수 자신 자체를 값으로 갖는 constructor라는 속성이 있는 객체이다. 이는 Function.prototype으로의 숨겨진 연결과는 다른 의미 이다.
자바스크립트 개발을 해본 경험이 있다면 대부분 프로토타입(Prototype)에 대해서 들어봤을 것이다. 프로토타입은 클래스가 없는 자바스크립트에서 객체지향적인 프로그래밍을 하기 위해서 사용하는 특징이라고 할 수 있다.
좀 더 쉽게 말하자면 상속(Inheritance)을 구현하여 객체가 다른 객체에 속성들을 물려줄 수 있도록 하기 위해 프로토타입을 사용할 수 있는 것이다.

위 인용문의 내용을 살펴보면 크게 객체와 프로토타입과의 관계함수와 프로토타입과의 관계 부분으로 구분되는 것을 알 수 있다.

간단하게 객체를 만들어서 차이점을 확인해 보면 좋을 것 같다.
var func = function () {
    this.one = 'Hello';
    return {
        one: one,
        two: 'world'
    };
};
var obj1 = func();

var obj2 = {
    one: 'Hello',
    tho: 'world'
};

console.dir(obj1);
console.dir(obj2);
위 예제를 실행 결과로 나오는 obj1과 obj2는 완전히 똑같다. 객체 생성 방법에 대한 다른 표현 방식이 있다는 것을 보여주기 위해 똑같은 객체를 생성했을 뿐이지 사실 여기서 중요하게 봐야하는 부분은 console.dir()의 결과로 콘솔에 출력되는 내용이다.


인용문에서 언급된 '객체 리터럴로 생성된 객체는 Object.prototype에 연결된다'는 부분은 위 그림에 나타나있는 __proto__를 말한다.
어렵다. 이렇게 하면 이해하기가 쉽지 않다. 이해를 해야 사용할 수 있고, 누구에게나 설명할 수 있기 때문에 이 개념을 좀 더 명확한 예제로 확인해 볼 필요가 있다.

var Func = function () {
    this.prp = 'Jonnung';
}

var ins = new Func();

console.dir(ins);
이 예제 코드는 new 키워드를 사용해서 객체를 생성할 수 있는 생성자 패턴으로 작성된 함수이다.
생성된 ins라는 객체 내부를 살펴본 모습은 아래와 같다.

ins 객체의 __proto__.constructor를 확인 해보면 Func() 생성자 함수가 들어 있는 것을 알 수 있다. 한마디로 ins 객체는 Func() 라는 생성자 함수를 원형(프로토타입)으로 생성된 객체라고 할 수 있다.
이제 그림이 조금 명확히 보이기 시작하는 느낌이다.
위에서 객체 리터럴로 생성된 객체는 __proto__.constructor 속성에 Object()가 있던 것은 리터럴 표기법으로 생성 된 객체는 리터럴의 특성상 즉시 객체화가 되기 때문에 직접적인 객체의 원형이라고 할 수 있는 대상이 없고, 바로 최상위 원형인 Object이 원형이 되는 것이다.

함수는 객체이다.


객체의 프로토타입에 대해 정리가 끝났다고 프로토타입에 대한 이해가 끝난것은 아니다.
이전 단락의 인용문에서 말하듯 함수도 역시나 객체이다. 이제부터 혼란을 방지하기 위해 함수를 함수 객체로 표현하도록 하겠다.
이번에 살펴볼 내용은 함수객체만이 갖고 있는 프로토타입 프로퍼티(prototype property) 라는 것이다.

위 단락의 두번째 예제의 console.dir()을 객체가 아닌 생성자 함수에 대해 실행해 보도록 하겠다.

var Func = function () {
    this.prp = 'Jonnung';
}

var ins = new Func();

console.dir(Func);

위에서 객체의 내부에 __proto__가 객체의 원형인 프로토타입이라고 했는데 이 함수 객체에는 __proto__도 있고, prototype 이라는 속성도 있는 것을 확인 할 수 있다.

이 prototype property는 함수 객체만이 갖고 있는 속성이다. 그리고 위에서 설명한 객체의 프로토타입과는 의미가 전혀 다르기 때문에 절대로 착각해서는 안된다. 우리가 흔히 상속의 개념으로 생각하는 객체의 원형인 프로토타입은 전 단락에서 설명한 객체가 생성되기 위해 사용한 원형이라는 의미의 프로토타입이 맞다.

그럼 함수 객체만이 갖고 있는 이 prototype property는 무엇인가!
바로 자신을 통해 만들어질 객체들이 원형(프로토타입)으로 사용할 객체 이다.

일단 정의 자체가 다르기 때문에 혼란스러워할 이유는 없다. 그리고 이런 흐름을 증명할 수 있는 증거는 이미 위 코드의 결과에서 확인한 것이나 다름없다. (갑자기 반전스러운 말투)

마무으리


  1. Func() 생성자 함수는 자신을 원형으로 해서 생성될 객체가 참조할 객체를 프로토타입 프로퍼티(prototype property)에 갖고 있다.
  2. Func() 생성자 함수로부터 탄생한 객체 ins는 흔히 프로토타입이라고 하는 자신이 만들어질 때 원형으로 사용한 객체에 대한 연결 정보를 갖고 있다.

참고