아디봉의.net

[자바스크립트] 코딩기법과핵심패턴: 코드재사용패턴#1 (펌) 본문

JAVASCRIPT

[자바스크립트] 코딩기법과핵심패턴: 코드재사용패턴#1 (펌)

아디봉 2013. 5. 25. 13:33
자바스크립트 코딩기법과 핵심패턴 제 6장 코드 재사용 패턴 #1  

이 책에서는 자바스크립트에서 코드 재사용 패턴은 상속, 다른 객체와 합성, 믹스-인 객체 사용, 메서드 빌려쓰기등으로 소개하고 있다. 코드 재사용 작업을 접근할 때, GoF의 충고인 '클래스 상속보다 객체 합성을 우선시하라'를 생각하는게 중요하다. 

클래스 방식 vs 새로운 방식의 상속 패턴 
대다수의 프로그래밍 언어는 객체의 설계도로 클래스 개념이 있지만 자바스크립트에서는 클래스가 없다. 자바스크립트의 객체는 단순히 키-값의 쌍일뿐이며 언제든지 생성하고 변경할 수 있다. 클래스가 없기 때문에 다른 패턴으로 상속을 구현할 수 있다. 함수와 프로토타입, 클로저등의 개념을 적절히 섞는다면 다른 언어의 클래스및 상속패턴등을 흉내낼 수 있다. 하지만 자바스크립트는 그 특성상 코드재사용을 위해 꼭 클래스 상속 개념을 사용할 필요가 없다. 그 외에 다양한 방법으로 사용할 수 있기 때문에 이런 클래스 방식으로 상속을 구현하지 않고 새로운 방식으로 발전시킬 수 있다. 이 장에서는 먼저 전통적인 클래스 방식으로 코드 재사용을 알아보고 자바스크립트의 특징을 살린 새로운 방식으로의 재사용 패턴을 소개하고 있다. 클래스 상속이 필요없다고 해서 소홀히 보면 안된다. 이 부분을 잘 봐두면 자바스크립트의 함수, 프로토타입, 클로저 개념들의 활용을 더 명확히 학습할 수 있기 때문이다. 


클래스 방식의 상속 패턴 #1 - 기본 패턴 
자바스크립트는 상속을 직접적으로 지원하지 않기 때문에 모두 구현해야한다. 클래스 방식이라고 하지만 클래스가 없고 생성자 함수만 있을 뿐이다. 
01.//상속을 담당해 주는 함수
02.function inherit(C, P) {
03.C.prototype = new P();
04.}
05. 
06.//부모 생성자
07.function Parent(name) {
08.this.name = name || 'jidolstar';
09.}
10. 
11.//생성자의 프로토타입에 기능을 추가
12.Parent.prototype.say = function () {
13.return this.name;
14.}
15. 
16.//아무 내용이 없는 자식 생성자 
17.function Child(name) {}
18. 
19.//여기서 상속이 일어남
20.inherit(Child, Parent);
21.//아래처럼 사용
22.var kid = new Child();
23.kid.say(); //jidolstar

이 패턴은 가장 널리 쓰이는 기본적인 방법으로 Parent() 생성자를 사용해 객체를 생성한 다음 이 객체를 Child()의 프로토타입에 할당하는 것이다. inherit(C, P) 함수가 그것을 담당해주고 있다. 여기서 prototype 프로퍼티가 함수가 아니라 객체를 가리키는 것이 중요하다. 또 new 연산자가 있음을 잘 봐두자. 결국 프로토타입 체인에 의해 부모의 인스턴스 기능을 물려받는다.

하지만 이 패턴은 단점이 있다.

  • 부모 객체의 this에 추가된 객체 자신의 프로퍼티와 프로토타입 프로토퍼티를 모두 물려받는다.  
  • 이 inherit() 함수는 인자를 처리하지 못한다. 즉, 자식 생성자에 인자가 들어와도 부모 생성자에 전달 못한다.
    1.var s = new Child('joykim');
    2.s.say(); //여전히 jidolstar
  • 자식 인스턴스를 생성할 때마다 상속을 실행하는데 여기서는 부모 객체를 계속해서 재생성하게 되어 비효율적이다.
 
클래스 방식의 상속 패턴 #2 - 생성자 빌려쓰기 

이 패턴은 자식에서 부모로 인자를 전달하지 못했던 #1의 문제를 해결한다. 이 패턴은 부모 생성자 함수의 this에 자식 객체를 바인딩한 다음, 자식 생성자가 받은 인자를 모두 넘겨준다. 

1.function Child(a, b, c, d) {
2.Parent.apply(this, argument);
3.}

이렇게 하면 부모 생성자 함수 내부의 this에 추가된 프로퍼티만 물려받게 된다. 중요한 것은 부모의 프로퍼티를 부모것을 참조하는게 아니라 부모것에 대한 복사본을 가진다는 점이다. 단, 프로토타입에 추가된 맴버는 상속받지 않는다. 

생성자 빌려쓰기는 프로토타입이 전혀 상속되지 않기 때문에 분명히 한계가 있다. 반면에 부모 생성자 자신의 맴버에 대한 복사본을 가져올 수 있다는 장점이 있어 자식이 실수로 부모의 프로퍼티를 덮어쓰는 위험은 방지된다. 

생성자 빌려쓰기 패턴은 다중상속도 가능하다. 
 
클래스 방식의 상속 패턴 #3- 생성자 빌려쓰고 프로토타입 지정해주기 

이 패턴은 앞선 두 개의 패턴을 혼용한다. 
01.function Child(a, b, c, d) {
02.Parent.apply(this, argument);
03.}
04.Child.prototype = new Parent();
05.//테스트
06.var kid = new Child('joykim');
07.kid.name; //joykim
08.kid.say(); //joykim
09.delete kid.name;
10.kid.say(); //jidolstar

자식 객체는 부모가 가진 자신만의 프로퍼티 복사본을 가지게 되는 동시에, 부모의 프로토타입 멤버로 구현된 재사용 가능한 기능들에 대한 참조 또한 물려받게 된다.

하지만 부모생성자를 비효율적으로 두 번 호출한다는 것은 단점이다. 

클래스 방식의 상속 패턴 #4 - 프로토타입 공유 

지금까지의 패턴과는 전혀 다르게 부모생성자를 전혀 호출하지 않는다.   
1.function inherit(C, P) {
2.C.prototype = P.prototype;
3.}
이 패턴은 프로토타입 체인 검색이 짧고 간편해지며 부모가 가진 say() 메서드를 자식에서도 똑같은 접근 권한을 가진다. 

그러나 이 패턴은 상속 체인이 공유되기 때문에  수정될 때 부모, 자식, 프로토타입에 전부 영향이 간다. 그리고 자식 객체는 부모의 name 프로퍼티를 물려받지 않는다. 
'

클래스 방식의 상속 패턴 #5 - 임시 생성자 
이 패턴은 프로토타입 체인의 이점을 유지하면서, 동일한 프로토타입을 공유할 때의 문제점을 해결하도록 부모와 자식의 프로토타입 사이에 직접적인 링크를 끊어버린다. 
1.function inherit(C, P) {
2.var F = function () {};
3.F.prototype = P.prototype;
4.C.prototype = new F();
5.}
여기서 함수 F가 임시 생성자를 프로토콜 타입으로 설정하므로 장점으로 자식 프로토타입이 수정되더라도 부모의 프로토타입에 영향을 주지 않는다. 생성자에서 this에 추가한 멤버는 상속되지 않는다. 
1.var kid = new Child();

위 코드에서 kid.name에 접근하면 undefined라는 값을 얻는다. name은 부모 자신의 프로퍼티인데 상속과정에서 new Parent()를 호출한 적이 없기 때문에 이 프로퍼티는 생성조차 않는다. 하지만 kid.say()의 경우 프로토타입 체인에 의해 Parent의 prototype에 정의된 say() 메서드에 접근이 된다. 

이 패턴에 부모 원본에 대한 참조를 추가할 수 있다. 상속체계에서 상위 클래스에 대한 참조를 자식 클래스가 접근하기 위해 super를 이용하듯이 말이다. 

1.function inherit(C, P) {
2.var F = function () {};
3.F.prototype = P.prototype;
4.C.prototype = new F(); //임시생성자 상속패턴 적용
5.C.uber = P.prototype; //부모 원본에 대한 참조
6.}

또한 상속 함수를 더욱 완벽하게 만들기 위해 한가지 더 설정해야 한다. 현재 상태에서는 모든 자식 객체들의 생성자는 Parent()로 지정되어 있다. 그러므로 생성자 함수를 자식 자신을 가리킬 수 있도록 아래처럼 코드를 추가한다. 

1.function inherit(C, P) {
2.var F = function () {};
3.F.prototype = P.prototype;
4.C.prototype = new F(); //임시생성자 상속패턴 적용
5.C.uber = P.prototype; //부모 원본에 대한 참조
6.C.prototype.constructor = C; //자식이 자식의 생성자 함수를 가리키도록 한다.
7.}

최종 버전은 최적화이다. 최적화되는 상속 함수는 상속이 필요할 때마다 임시(프록시) 생성자가 생성되지 않도록 한다. 즉시실행 함수와 클로저를 이용해 최적화 한다. 
01.var inherit = (function () {
02.var F = function () {};
03.return function (C, P) {
04.F.prototype = P.prototype;
05.C.prototype = new F();
06.C.uber = P.prototype;
07.C.prototype.constructor = C;
08.};
09.}());

클래스 방식 상속 패턴을 활용한 klass 제작 

아래 klass() 함수는 클래스를 상속을 구현하기 위한 문법 설탕을 제공한다.  함수 대신 Klass()라는 생성자 함수를 사용하거나 Object.prototype을 확장하기도 하는데 여기서는 간단한 함수를 보여주고 있다.
  
01.var klass = (function () {
02.var F = function () {};
03.return function (Parent, props) {
04.var Child, i;
05. 
06.//1. 새로운 생성자
07.Child = function() {
08.if (Child.uber && Child.uber.hasOwnProperty('__construct')) {
09.console.log('Child.uber.__construct 적용');
10.Child.uber.__construct.apply(this, arguments);
11.}
12.if(Child.prototype.hasOwnProperty('__construct')) {
13.console.log('Child.prototype.__construct 적용');
14.Child.prototype.__construct.apply(this, arguments);
15.}
16.};
17. 
18.//2. 상속
19.Parent = Parent || Object;
20. 
21.F.prototype = Parent.prototype;
22.Child.prototype = new F();
23.Child.uber = Parent.prototype;
24.Child.prototype.constructor = Child;
25. 
26.//3. 구현 메서드를 추가한다.
27.for (i in props) {
28.if (props.hasOwnProperty(i)) {
29.Child.prototype[i] = props[i];
30.}
31.}
32. 
33.//'클래스'를 반환한다.
34.return Child;
35.};
36.}());
책에서 제공된 소스와는 약간 다르다. 위 klass() 함수는 상속 패턴 #5에서 소개한 마지막 inherit() 메서드 최적화를 적용했다. 즉, 즉시실행함수를 사용해 임시(프록시) 생성자 F를 클로저 안에 저장해 한 번만 만들도록 바꾸었다.  

아래 코드는 이 klass() 함수를 사용해 Man 생성자 함수를 만드는 예제이다. 
01.//사람 생성자 함수 생성
02.var Man = klass(null, {
03.__construct: function (what) {
04.console.log('Man\'s constructor');
05.this.name = what;
06.},
07.getName: function() {
08.return this.name;
09.}
10.});
11. 
12.//사람 객체
13.var first = new Man('Adam');
14.console.log(first.getName());
사람 로그는 다음과 같이 찍힌다.  Man 생성자 함수의 경우 상속받는 대상이 없기 때문에 Object가 부모가 되며 __construct와 getName 메서드가 Man의 프로토타입에 추가된다. new Man()을 통해 생성자 내부에 정의된 두개의 if문을 거치게 되는데 일단 첫번째 if문에서 Man.uber는 Object의 프로토타입이므로 null이 된다. 그러므로 무시된다. 두 번째 if문에서는 Man.prototype에 __construct 메서드가 추가되었으므로 이 if문 내부는 실행된다. 여기서 상속 패턴 #3의생성자 빌려쓰기 패턴이 적용되어 __construct() 메서드 내부에 this는 Man 객체(first)를 가리키게 된다. 그러므로 Man 객체에서 getName()을 호출하면 this.name은 바로 Adam이 되는 것이다. 

Child.prototype.__construct 적용
Man's constructor
Adam



이제 사람 생성자 함수(Man)를 상속받는 슈퍼맨 생성자 함수를 만들자. Man과 차이점은 자식 생성자 함수인 Man을 인자로 넘긴것과 getName()에서  return this.name이 아닌 SuperMan.uber.getName()을 호출했다는 점이다. 하지만 여기서 this.name에서 this는 이미 SuperMan 객체의 자신을 가리키도록 klass() 함수 내부에 Child 생성자 함수 안 첫번째 if문에 this를 넘겼음을 상기하자. 결국 this.name은 SuperMan 객체의 프로퍼티인 셈이다. 
01.//슈퍼맨 생성자 함수 생성
02.var SuperMan = klass(Man, {
03.__construct: function (what) {
04.console.log('SuperMan\'s constructor');
05.},
06.getName: function () {
07.var name = SuperMan.uber.getName.call(this);
08.return 'I am ' + name;
09.}
10.});
11. 
12.//슈퍼맨 객체
13.var clark = new SuperMan('Clark Kent');
14.console.log(clark.getName());

다음은 위 코드를 실행했을 때 로그이다. 이번에는 Man을 상속했기 때문에 klass()함수의 Child 생성자 함수 내부에 첫 번째 if문 내부가 실행된다. 그래서 SuperMan에는 name 프로퍼티를 구현한 적이 없지만 Man에 구현되어 있기 때문에 SuperMan 객체에 name 프로퍼티가 자동으로 등록되게 된다.  

Child.uber.__construct 적용
Man's constructor
Child.prototype.__construct 적용
SuperMan's constructor
I am Clark Kent



이제 마지막으로 슈퍼맨 객체는 누구의 인스턴스인가 확인해보자.
1.//슈퍼맨 객체는 누구의 인스턴스인가?
2.console.log(clark instanceof Man); //true
3.console.log(clark instanceof SuperMan); //true
간단하지만 이 klass() 함수는 기본적인 클래스 상속을 구현해 냈고 제대로 동작하는 것을 확인할 수 있다. 지금까지 다룬 상속 패턴은 피하는 것이 좋다. 왜냐하면 기술적으로 언어에 존재하지 않는 혼란스러운 개념을 온통 끌고 오기 때문이다. 하지만 이 개념은 확실히 알 필요는 있다. 프로토타입과 클로저등을 사용해 어떻게 상속을 구현하는지 아는 것은 결국 이것들의 유용한 점을 알기에 좋기 때문이다. 이 패턴의 장점은 자바스크립트를 경험해보지 못해 프로토타입이 낮설게 생각하는 개발자들에게 유용할 수 있다.

출처 : http://blog.jidolstar.com/795