[js] 클래스 기반 언어 vs 자바스크립트 3/3

이전 포스트에서 프로토타입 시스템을 간략히(?) 짚어봤습니다. 한 마디로 브랜든 할베의 혈기왕성한 젊은 시절은…너무 멋져 보이는 것들을 우겨넣으려 했다는거죠. 그 덕분에 이 고생을.. 이 번에는 프로토타입을 사용하지 않고 인스턴스에게 필드나 메서드를 넣는 방법을 살펴보겠습니다.

  • 계속 말씀드리지만 자바스크립트 외에도 언어론적인 내용이 엄청 등장합니다.
    스트레스가 되실 수 있다는…

 
 
 

프로토타입을 왜 쓰지 말라는 거야?

그거야 느리기 때문입니다. 생각해보면 당연합니다. 하나의 해시 맵에서 키를 통해 값을 찾는 건 해시 함수 한 번 실행하면 되는 일입니다만, 프로토타입 체인이 관여하면 몇 번이나 반복해서 키를 검색해야 합니다. 따라서 프로토타입의 단계가 깊어질수록 해당 키를 찾는 시간은 늘어납니다.

이 느린 구조를 극복할 방법은 전혀 없는 걸까요? 현재 알려진 방법은 한 가지 뿐입니다.

프로토타입 체인에 존재하는 모든 키를 인스턴스의 키로 복사한다.

바로 이 방법이 크롬의 V8엔진이 채용하고 있는 최적화 방식이기도 합니다. 이 시리즈에서 줄기차게 등장하는 Alert 클래스로 돌아가보겠습니다.
이번 포스트에서는 프로토타입을 사용할 계획이 없으므로 new 를 배제하고 오브젝트를 반환하도록 개조하겠습니다.

function Alert( $msg ){
	return {_msg:$msg, action:action};
}

function action(){
	alert( this._msg );
}

var result = Alert( 'hello' );
result.action(); //hello

바로 이 스타일이 직접 키에 복사해주는 방식입니다. resultaction키에 실제 함수의 참조를 넣어줬습니다. 이 전의 프로토타입은 action을 찾기 위해 result.proto.action 에서 찾았던 것에 비해 지금은 result.action 에서 찾을 수 있게 되었습니다.
하지만 이 스타일에서 살짝 문제가 있는데 Alert 는 클래스 역할을 수행하는 함수이므로 존재해도 괜찮지만 actionAlert 에 딸린 부속품이므로 이렇게 방탕하게 전역 공간에 둘 수는 없는 노릇입니다. 그럼 어디에 간수해야 할까요?
좋은 자리 후보는 Alert 자신입니다. 다음과 같이 변경하죠.

function Alert( $msg ){
	return {_msg:$msg, action:Alert._action};
}

Alert._action = function(){
	alert( this._msg );
};

함수도 기본적으로 오브젝트이기 때문에 마음대로 키를 확장할 수 있습니다.
이제 actionAlert 의 소속이 되어 전역 공간에는 존재하지 않게 되었습니다.
이 개념은 c 에 흔하게 존재합니다. 헤더에 기술하지 않은 함수가 본문에 등장하는 식으로 함수 내부에서만 인지되는 경우입니다. c++, 자바라면 함수가 반드시 Alert 클래스 내부에 기술 되어야 하니 이미 형식 상 해결됩니다. 그런데…
 
 

private이슈

위의 스타일이 나쁘진 않지만 치명적인 단점이 있습니다. 제가 언더스코어를 붙였다고 해도 맘만 먹으면 얼마든지 속성을 악용할 수 있다는 것이죠. 예를 들어 다음과 같은 것이 가능합니다.

var instance = Alert( 'test1' );

// 속성 값을 직접 참조할 수 있다.
alert( instance._msg );

// 인스턴스용 메서드를 직접 참조할 수 있다.
Alert._action();

그럼 이름규칙 방어 외에 자바스크립트에서 private 를 만들 수 있는 방법은 없는 걸까요?[1. 실제 이름규칙은 가장 위대한 방어책일지도 모릅니다만 ^^]

사실 있습니다 ^^

자바스크립트에서의 private 는 언어적인 기능이 아니기 때문에 함수객체의 특징인 스코프(Scope)를 이용해 구축합니다.
문제는 스코프를 통해 private 를 구현할 경우 각 인스턴스마다 함수를 생성해야 하기 때문에 비용이 막대하다는 점입니다. 따라서 개발자는 정말 private 으로 보호해야 하는 상황인가를 적절하게 판단하여 스코프를 사용한 private 를 취사 선택해야 합니다. 예를 들어 스코프를 이용한 Alert 는 다음과 같이 작성할 수 있습니다.

function Alert( $msg ){
	return {
		action:function(){
			alert( $msg );
		}
	};
}

이렇게 작성하면 아래와 같이 result._msg 속성과 부가적인 Alert._action 함수는 존재하지 않게 됩니다.

var result = Alert( 'hello' );

// 속성 값 참조불가
result._msg == undefined

// 인스턴스용 메서드 참조불가
Alert._action == undefined

그럼 대신 지불한 비용은?
Alert_action 의 경우는 _action 을 한 번만 정의하고, 모든 인스턴스가 그것을 공유해서 사용했습니다. 하지만 위의 예는 각 인스턴스 별로 action 함수를 따로 생성합니다.
메모리 상으로 단 하나의 함수를 공유했던 상황에서 인스턴스 별 함수를 생성한 꼴이므로 비용은 비교할 수 없이 크게 됩니다.
요점은 그렇게까지 해서 _msgaction 함수의 참조를 보호할 것인가 입니다.

private 전략 중 _msg 를 보호하려면 반드시 인스턴스 별로 함수를 생성해야 합니다.하지만 필드가 아니라 메서드인 action 함수는 스코프구조를 약간 개선해 처리할 수 있습니다. 다음과 같은 형태를 생각해보죠.

var Alert = ( function(){
	function action(){
		alert( this._msg );
	}
	return function Alert( $msg ){
		return {_msg:$msg, action:action};
	};
} )();

전체를 감싸고 있는 즉시 실행함수는 스코프를 격리하는데, 이 이름공간 내에서만 action 함수가 유효하기 때문에 외부에서는 action 함수를 참조할 수 없습니다.
하지만 그 내부에서 생성된 Alert 함수는 action 을 인식할 수 있으므로 Alert._action 같은 외부 참조를 공개하지 않고 메서드를 은닉할 수 있습니다.
 
 

자바스크립트 모듈

  • 외부에 공개될 객체를 따로 두고,
  • 스코프 공간으로 격리하여,
  • 공개될 객체만이 아는 내부 객체를 정의하는 포괄적인 스타일을

자바스크립트 모듈이라 부릅니다.

모듈의 정체는 함수 하나일수도 있고 격리된 공간 내에 정의된 다수의 객체를 포함한 형태일 수도 있습니다.
중요한 점은 클래스가 없기 때문에 무리하게 프로토타입 체인을 이용한 유사 클래스 구조물에 집착하기보단 모듈로 격리시켜 설계하는 것이 자바스크립트에서 매우 일반적이란 것입니다.
따라서 클래스 기반의 개발자가 자바스크립트로 방대한 구조를 짤 때 자바식으로 클래스 구조물로 짠다면 속은 편하겠지만 결국 다음의 한계에 부닥쳐 자바같지 않다는 점을 알게 됩니다.

프로토타입 체인을 통해 메서드는 상속하여 사용할 수 있지만, 필드는 상속되지 않고 절대로 종단점 클래스의 인스턴스마다 복제 해 줘야 한다.

이 언틋 별거 아닌거 같은 차이점 때문에 필드, 메서드 양쪽을 이용한 클래스 관계도와 설계를 옮겨오는 게 불가능해집니다.

(이걸 어떻게든 방대한 setter, getter 를 만들어 보며 안타깝게 노력을 하다가 결국 포기하게 됩니다 ^^;)

자바스크립트를 깊이 사용하다보면 결국 프로토타입을 해결할 수 있는 것은 메서드를 수직분할하여 공유할 수 있는 즉 포함관계의 객체 설계 뿐이라는 걸 알게 됩니다.
더군다나 부모층과 자식층 간의 데이터 분리가 안되기 때문에 역할 상 완전히 분리된 포함 관계라는 겁니다.
이 문제는 매우 심각합니다. 간단한 템플릿 메서드를 구현해보겠습니다.

class CalcCost{

	private int baseCost;

	public void base( int v ){
		baseCost = v;
	}

	public int cost(){
		return baseCost+ subCost();
	}

	abstract protected int subCost();
}

class Cost extends CalcCost{

	private int cost;

	Cost( int v ){
		cost = v;
	}

	protected int subCost(){
		return cost;
	}
}

CalcCost c = new Cost( 50 );
c.base( 30 );
print( c.cost( 50 ) ); //80

이런 구조를 프로토타입으로 번역하면 부모객체의 변수(baseCost)조차 자식 클래스에 들어오게 됩니다.

function CalcCost(){}
CalcCost.prototype.base = function( v ){
	this.baseCost = v;
};
CalcCost.prototype.cost = function(){
	return this.baseCost + this.subCost()
};

function Cost( v ){
	this.cost = v;
}
Cost.prototype = new CalcCost;
Cost.prototype.subCost = function(){
	return this.cost;
};

var c = new Cost( 50 );
c.base( 30 );
alert( c.cost( 50 ) ); //80

( 이거 정말 자바하시던 분들이 와서 많이 짜는 코드 스타일입니다 )

언틋보면 호스트코드가 똑같이 나왔으니까 제대로 번역된 것처럼 보이지만 baseCost 도 보호되지 않고 subCost 메서드도, 심지어 cost 도 보호되지 않습니다.
결국 호스트코드가 진행되면서 전부 오염됩니다. 격리를 기반으로 하는 책임모델 설계가 전부 엉망이 되는거죠. 아무것도 격리되지 않고 호스트코드에 그대로 노출된 상태이기 때문입니다.

자바스크립트에서 변수를 보호할 수 있는 유일한 방법은 모듈화 시키는 것 뿐입니다.

애매하게 격리를 시키지 않고 안정성없이 모든 팀원을 믿고 갈 것이냐 아니면 모듈화 시킬 것이냐를 선택해야 합니다.
인간은 기본적으로 낮은 곳으로 흐르는 존재이기 때문에… 믿고 가면 일주일 후에 생길 불쌍사는 처참하겠죠….따라서 적당한 부분을 모듈화를 통해 격리시키는 것은 필수입니다.

….만….

결국 자바스크립트에서 모듈이라 불리는 것의 정체는 함수의 스코프를 이용한 참조 객체들의 격리입니다.

그렇다면 함수의 스코프 시스템을 이해하지 않는 상태에서 모듈을 효과적으로 설계할 수 있을까?
…..라고 고민해봤습니다.

…..만 불가능했습니다.

그래서 어쩔 수 없이 스코프 시스템을 살짝 설명할 수 밖에 없습니다(실은 알아서 찾아보세요라고 하고 싶습니다만, 이렇게 긴 시리즈를 읽어주시는 분들을 위해..=.=;)

이 주제는 방대한데 이유는

함수형 언어가 어떻게 함수를 객체화 하는가

에 대한 전반적인 내용이 되어버리기 때문입니다.

자바스크립트는 함수형 언어의 특징을 일부만 갖고 있습니다. 그 변태적인 특성 때문에 이상한 시스템이 덕지덕지 붙어있습니다.
바로 여기가 진짜 자바스크립트의 입구입니다. 그리고 자바, c++ 개발자가 프로토타입까지 즐거워하다가 자바스크립트를 포기해버리는 무덤이기도 합니다.
한 번쯤 포기할지. 가야할지 정하셔야 합니다…자바스크립트를 깊이 쓸거면 ^^;;

 
 
 

함수형 언어와 자바스크립트

함수형 언어라는 단어는 어느 정도 환상과 미신의 단어입니다. 아직까지도 함수형 언어가 무엇인지, 그리고 진정한 의미에서 함수형 언어 라는 게 존재하는가? 에 대한 분분한 의견이 있습니다. 함수형 언어의 조건으로 대두되는 두 가지 관점이 있습니다.

  • 수학적일 것
  • 함수가 일급객체일 것

물론 이 두 가지를 다 만족시킬 수 있습니다. 그럼 이 두 가지를 다 만족시키는 언어는 현재 얼마나 있을까요?

놀랍게도 현대화 된 자바, c++, c#, 파이썬, 자바스크립트, 루비…거의 모든 언어가 저 조건을 충족할 수 있습니다.

게다가 저 두 가지는 서로 크게 연관도 없습니다. 함수가 일급객체일지라도 수학적이지 않을 수 있고, 수학적일지라도 함수는 일급객체가 아닐 수도 있습니다.
 
 

수학적인 관점

우선 여기서 수학적이라 함은

부수효과가 없는 로직으로 되어있다

는 뜻입니다.

부수효과가 없다는 것은

  • 상태가 없다는 뜻도 되고
  • 모든 데이터형이 Immutability 일 것

이라는 의미도 됩니다(수학적 알고리즘마저 여기서 설명해야하는 건가…=.=;;;)

전혀 상상이 안가시는 분들을 위해 간략히 말하자면 인자와 지역변수만 사용하고 등장하는 모든 데이터는 숫자같은 참조형식이 아닌 함수를 생각해 보시면 됩니다.
그 함수는 외부의 어떤 상태에 상관없이 언제나 어떤 인자를 넘기면 동일한 값이 나옵니다. 쓰레드 동기화도 필요없고, 어떤 변수에 어떤 값이 들어있든 영향받지 않죠.

언제나 f(x) = y 인 겁니다.

그러한 의미로 수학적 함수라 합니다.

수학적으로 알고리즘을 작성하면 상태가 없기 때문에, 상태가 일으키는 모든 문제를 해결할 수 있습니다(쓰레드동기화, 시계열디버깅 등)
대신 상태가 없는(==변수 없는?!!) 프로그래밍을 해야합니다!!

이에 대한 다양한 교과서들이 출간되어있습니다.

고전적으로는 컴퓨터 프로그램의 구조와 해석(SICP) 같은 책도 있지만, 최근에 나온 프로그래밍 클로저 도 같은 주제를 클로저로 다루고 있습니다(거의 내용으로보면 원작자가 저작권 소송해도 될 지경)

수학적 프로그래밍은 더 어렵다고 보긴 힘들지만, 그렇다고 여태까지 상태 변수를 쩔게 써온 java, c++ 개발자가 새삼 공부 할만한 것도 아니란 겁니다(개인적으로는 브랜드할베처럼 SICP 의 열렬한 팬입니다만..)

이러한 수학적 프로그래밍을 기본으로 함수객체를 사용하는 경우 특정 인자를 넘긴 상태의 함수 자체를 인자로 사용할 수 있는데, 이런 함수는 일종의 인자가 지정된 상태의 함수이므로 상태를 갖게 됩니다.
상태가 없는 함수 만의 세계에서 출현하는 돌연변이인거죠. lisp 같은 계열에서는 이것을 클로저(closure)라 부릅니다. 말하자면 lisp 에서 클로저라는 것은 프로그래밍 시 나타는 일종의 반복되는 현상일 뿐이지 언어에 내장된 어떤 기능이 아닌 것입니다.(클로저를 이용한 간단한 예를 들자면 커링이 되겠습니다)

하지만 자바스크립트는 아시다시피 변수와 객체가 난무하는 상태 언어의 세계와 알고리즘을 주로 사용합니다.
게다가 작성된 코드들은 결코 수학적이지 않습니다.

브랜든 할베는 야심차게 이 모든걸 합체한 함수객체를 생각해 내는데, 함수 객체를 생성할 때 별도의 객체를 만들어 함수 주변의 모든 변수를 보관하여 참조할 수 있게 하는 시스템입니다.
결과적으로 구현된 자바스크립트의 함수객체에서는

  • 클로저라는 프로그래밍적 현상은 일어나지 않지만(일으킬 수 있을지라도 ^^)
  • 함수 객체에 내장된 스코프와 함수 호출 시 생성되는 실행컨텍스트 사이의 키체인 검색 시스템 일체를

(왠지 모르겠지만) 클로저라 부릅니다.

그래서 자바스크립트에서 클로저란 정말 설명하기 어려운, lisp 의 클로저와 상관없는 단어입니다.
 
 

Execution Context

각설하고 함수를 만들면 어찌되는지를 봅시다. 자바스크립트에서 함수는 언제나 런타임에 생성됩니다. 그리고 어떤 함수가 생성되는 시점은 항상 그 생성될 함수를 감싸고 있는 함수가 실행 중인 것입니다. 예를 들어 봅시다.

function a(){}

마치 전역에 아무도 없는데 만든 것 같죠? 하지만 암묵적으로 이 때도 전역 함수가 실행 중입니다. 이를 코드로 표현하면 아래와 같습니다.

function GLOBAL(){

	function a(){}
}
GLOBAL();

그렇습니다. 개발자가 자바스크립트를 제아무리 최상단에서 함수를 작성해봐야 이미 전역함수가 구동 중인 것입니다.

함수는 자신을 둘러싼 부모 함수가 실행되는 도중에 생성

됩니다.

자바스크립트는 어떤 함수가 호출되면, 스택메모리를 생성하는 방식이 직렬적인 메모리공간을 사용하는 것이 아니라 해시맵을 생성해서 처리합니다.
즉 위의 코드에서 GLOBAL(); 이라고 함수를 호출하면 그 순간 { } 를 하나 만들어냅니다.
이렇게 함수 호출 시마다 만들어지는 독립적인 메모리 공간으로서 해시맵을 실행컨텍스트(Execution Context)라 부릅니다. 이하 EC 로 줄입니다.
EC 는 함수가 호출되어 함수 내부의 코드가 전부 실행될 때까지 생존하다가 실행이 완료되고 나면 소멸됩니다. 이를 개념적인 코드로 표현하면 아래와 같습니다.

function GLOBAL(){
	//내부코드들..
}

GLOBAL();
//1. EC생성
EC_GLOBAL = {};

//2. GLOBAL의 내부코드 실행

//3. EC소멸
delete EC_GLOBAL;

EC 가 기존 언어의 함수 스택메모리를 대체하기 위해 만든 것이라 함수 내부의 변수를 정리한 키가 자동으로 셋팅됩니다. EC 에 자동으로 셋팅되는 내용은 정리하면 아래와 같습니다.

  1. ‘this’ 키에 컨텍스트 객체가 참조됨
  2. ‘arguments’ 키에 인자를 정리한 리스트가 생성됨
  3. 기명된 인자에 arguments 를 정리하여 할당
  4. 지역변수가 키로 정리되고 undefined 가 할당됨
  5. ‘[[Parent]]’ 키에 함수 생성 당시의 환경함수의 EC 를 할당

이를 코드로 보는 편이 더 알기 쉽습니다.

function GLOBAL( a, b ){ //기명인자 a, b
	var c, d; //지역변수 c, d
}

GLOBAL( 3, 4 );
EC_GlOBAL = {
	'this':window, //1 기본 컨텍스트는 전역
	'arguments':{0:3, 1:4, length:2}, //2 인자를 정리한 것
	'a':3, 'b':4, //4 기명인자 정리
	'c':undefined, 'd':undefined, //3 지역변수 정리
	'[[Parent]]', ??? //5 함수생성당시 EC할당
};

이런 느낌입니다. 자바스크립트의 초창기 파서는 전부 해시맵 시스템과 키체인을 통한 검색함수 하나로 만들 수 있는 정도입니다. 당연히 함수와 함수를 호출하는 시스템도 그것을 이용해 제작될 수 있습니다.
단지 마지막에 나오는 [[Parent]] 의 문제는 지금은 설명할 수 없으니 다른 부분을 살펴보고 나중에 알아보겠습니다.
 
 

this

우선 this 의 정체가 명확해집니다. this 란 고작 EC 에 정의된 키 이상도 이하도 아닙니다.
this 에 객체를 할당하는 방법은 이미 1번째 포스팅에서 설명했는데, apply, call 을 사용하거나 대괄호, 컴마 구문을 사용하면 각 문법에 적절한 객체가 this 에 할당되고 생략하면 전역 객체가 할당됩니다.
 
 

arguments와 기명인자

arguments 의 실체는 함수 호출 시 괄호 안에 넣은 리스트를 근거로 하고 있습니다. GLOBAL( 3, 4 ) 에서 3, 4 부분을 이용해 만든 것입니다.
사실 함수의 정의와 무관하게 함수 호출 시 괄호 안에 있는 내용만 갖고 생성되기 때문에 GLOBAL( 3, 4, 5, 6 ) 이라고 하면 당연히 4개의 요소를 갖는 agruments 가 만들어집니다.

일단 arguments 를 만들어낸 뒤에는 함수에 정의한 기명인자에도 그 값을 넣어줘야합니다. a, b 에 해당됩니다. 그렇다면 a = arguments[0], b = arguments[1] 로 볼 수 있습니다.
여기까지는 쉽습니다만, 언어론적인 관점에서 보면 저 연결은 관련없는 두 변수가 바인딩 되어있는 상황이기 때문에 최초 바인딩 이후 연결이 끊어진건지 단방향이나 쌍방향연결이 유지되고 있는 건지 확인해야 하는 복잡한 상황으로 번집니다.

브렌든 할베는 여길 깊이 생각하지 않아서 다른 자바스크립트의 모든 부분이 그렇듯이 매크로 수준으로 처음에만 바인딩 해주고 방치하는 전략을 사용했습니다.
하지만 ES5 에는 명시적인 쌍방향 연결을 해야하고 중복 지역변수도 지정불가로 정의했습니다. 따라서 브라우저에 따라 반응이 다릅니다 ^^;

function GLOBAL( a, b ){
	arguments[0] = 3;
	alert( a );
}

GLOBAL( 10, 20 );

즉 이 상황인데 크롬 등 ES5 준수 브라우저는 구지 스트릭트모드로 안들어가도 이미 a3 이 되어있습니다. 하지만 IE 하위는 여전히 10을 유지합니다. 또한 아래와 같은 상황도 서로 다르게 반응합니다.

function GLOBAL( a, b ){
	var a;
}

인자에 등장한 이름을 지역변수로 선언할 수 있는가의 문제인거죠. 그 외에도 심플렉스, 멀티플렉스의 수 많은 문제가 서로 다르게 반응합니다만 이 포스팅의 목적이 크로스브라우징이 아니니 이 정도로 줄이겠습니다(완벽가이드나 노란책 보면 상당히 나옵니다 ^^;)
 
 

var

이제 var 의 의미를 정확히 알 수 있습니다.
var 는 지역변수를 만드는 키워드가 아닙니다. 정확하게 말하자면 컴파일러 입장에서 EC 에 등록될 키를 정의하는 문법입니다.
보통은 특정 객체의 키가 되지만 var 를 통해 키를 선언하면 그 키는 객체의 키가 되지 않고 EC 에 들어갈 키가 되어 버립니다.

개발자 입장에서는 EC 자체를 직접 컨트롤할 방법이 없으니 당연히 delete 가 안되는 거죠.
 
 

어휘공간(Lexical Environment)

그러니까 EC 란 언어론적인 관점으로 보자면 어휘공간이라 불리는 것입니다(음음 너무 언어론을 다루나…먼산)

  1. 어휘란 쉽게 말해 변수입니다.
  2. 변수란 정확히 말해 메모리주소의 별명입니다.
  3. 개발시 변수를 사용할 때는 특정 메모리주소를 사용하는 건데, 그 메모리를 구역으로 정리해서 그 안에서의 이름을 부여합니다.
  4. 왜냐면 인간이 인지할 수 있는 이름이란건 제한되어있기 때문에 이름이 중복되기 때문입니다.
    같은 a 라는 변수라고 해도 그 a 가 어디에서 a 냐의 문제입니다.
  5. 처음부터 ab 로 이름을 지으면 되지 않느냐?
  6. …됩니다. 근데 그렇게 해서 이름을 몇 개나 지을 수 있나요? 그리고 그 수 백개에 이르는 이름을 정말 기억하고 관리할 수 있나요?
  7. 설령 그렇게 할 수 있다고 해도 이름기반의 개발을 하는 장점을 모두 잃어버립니다.
  8. 예를 들어 this 라는 변수는 왜 만들었을까요?
  9. 알고리즘을 전부 this 를 기반으로 작성하고, 상황에 따라 this 가 참조할 메모리를 변경함으로서 알고리즘은 일종의 일반화된 알고리즘이 됩니다.
  10. 이 원리는 모든 클래스의 메서드에도 사용되고, 제네릭에서도 사용되고, 실은 인터페이스나, 함수 포인터 등도 전부 동일한 원리입니다.
  11. 모든 것에 이름을 따로 지으면 이런 개발방법론을 전부 포기하게 됩니다.
  12. 따라서 해법은 그 이름에 해당되는 메모리 공간을 격리시키는 것입니다.
  13. 이러한 특정 변수를 찾기 위한 격리된 메모리 공간을 어휘공간이라 합니다.
  14. 긴 설명 들으시느라 수고하셨습니다 ^^

한 마디로 EC 를 함수 호출 시마다 생성하는 이유는 함수 내부의 코드가 실행되면서 등장하는 변수 이름을 찾는 메모리 공간으로 사용하기 위해서 입니다.
따라서 함수는 호출될 때마다 함수 안에서 a 를 찾으면 매번 별도의 메모리 공간에서 찾게 됩니다….

…..만! 이 평화로운 세상에 변태같은 넘이 바로 [[Parent]] 입니다. 흥분을 가라앉히고 좀 있다가 설명하겠습니다.
 
 

진짜 문제

우선 알아야만하는 중요한 문제는 개념적으로는 이런게 생겨나면 좋겠다는 건 알겠지만, 컴파일러는 무엇을 근거로 저 EC 를 각 함수별로 만들었냐는 겁니다.
 
 

Scope

함수가 실행될 때마다 만들어지는 EC 는 호출된 함수객체에 내장된 스코프(Scope) 객체를 이용해 만들어집니다.
위의 EC 를 만들기 위한 청사진으로서의 스코프객체를 역으로 설계해봅시다.

  1. ‘arguments’키에 기명인자의 이름을 배열로 저장한다
  2. 지역변수를 배열로 저장한다.
  3. ‘[[Parent]]’ 키에 환경함수의 EC 를 저장한다.

이것도 코드로 보는 편이 빠르겠죠.

function GLOBAL( a, b ){
	var c, d;
}

GLOBAL.[[SCOPE]] = {
	'arguments':['a','b'], //1
	'locals':['c','d'], //2
	'[[Parent]]': ??? //3
};

1, 2번은 쉽게 이해가 됩니다. 함수의 형태를 해석하면 쉽게 채울 수 있는 내용입니다. 새삼 new Function 으로 함수를 만들 때 왜 그렇게 인자를 받는지 알 수 있습니다.

var f = new Function( 'a,b', 'var c,d' );

첫 번째 인자를 파싱해서 스코프에 넣을 속셈인 것입니다 ^^;

그럼 실제로는 어떻게 위의 스코프로 EC 를 만들어낼지 시뮬레이션 해 봅니다.
 
 

스코프를 이용한 EC 생성의 일반화

먼저 arguments 와 기명인자를 처리해보죠.

GLOBAL( 3, 5 );
EC_GLOBAL = { 'arguments':{0:3, 1:5, length:2} };

우선 위의 과정은 앞 서 언급한 것처럼 컴파일러가 괄호안의 구문을 해석해서 기본으로 처리할 수 있는 부분입니다.
하지만 기명인자 a, b 와 연결하려면 스코프의 도움을 받아야겠죠.

var arg = GLOBAL.[[Scope]].arguments; //['a','b']

for( var i = 0, j = arg.length ; i < j ; i++ ){
	EC_GLOBAL[ arg[i] ] = EC_GLOBAL.arguments[i];
}

쉬우니 생략…하지 않고 설명하자면, 스코프에 있던 기명인자의 배열을 이용해 EC 에 키를 만들어 arguments 의 값을 넣어주면 됩니다.
이걸 통해 기명인자를 EC 에 잡아주는 과정을 컴파일러 입장에서 일반화 할 수 있습니다.

두번째로는 지역변수의 초기화인데 GLOBAL.[[SCOPE]].locals 를 한바퀴 돌아주면 되니 생략하겠습니다. 이런고로 EC 의 지역변수 초기화도 일반화 되었습니다.

this 는 함수가 호출되는 형태로 컴파일러가 결정해서 넣어주면 되니 역시 일반화 오케

그리고 [[Parent]] 조차도 GLOBAL.[[SCOPE]].[[Parent]] 를 복사해주면 되니 실제 EC 입장에서는 전부 일반화되었습니다.
 
 

[[Parent]]

스코프의 [[Parent]] 에는 그 함수객체가 생성될 당시 환경함수의 EC 가 들어갑니다.

이 말로 설명이 이해되면 앞의 내용을 완벽하게 소화 하신거라 할 수 있습니다.

하지만 보다 구체적으로 들어가보죠.

일단 환경함수란 용어는 공식적인게 아닙니다. 제가 딱히 부를 단어가 없어서 쓰는 말입니다. 환경함수란 함수가 생성될 코드를 감싸고 있는 함수 입니다. 다음과 같이 이해할 수 있습니다.

function EnvironmentalFunction(){
	var f = function ConcreateFunction(){};
}

EnvironmentalFunction();

위의 코드에서 구상 함수를 만드는 유일한 방법은 환경함수를 실행했을 때 뿐입니다.
처음에 설명드린대로 개발자가 아무리 최상위에서 함수를 정의해도 전역함수 실행 중이기 때문이 위의 상황은 어떠한 자바스크립트 함수에도 해당되는 것입니다.
즉 함수가 생성되려면 반드시 자기를 감싸고 있는 함수가 실행 중이란 거죠. 그럼 환경함수가 실행 중이므로 환경함수의 EC 가 존재합니다.

function EnvironmentalFunction(){
	var f = function ConcreateFunction(){};
}

EnvironmentalFunction();
EC_EnvironmentalFunction = {...};

f 가 만들어지는 시점에 EC_Env 가 반드시 있다는 겁니다. f 함수의 스코프에 있는 [[Parent]] 에는 바로 이 넘이 들어갑니다.

function EnvironmentalFunction(){
	var f = function ConcreateFunction(){};
	//환경함수의 EC가 들어간다!
	f.[[SCOPE]].[[Parent]] = EC_EnvironmentalFunction;
}

EnvironmentalFunction();
EC_EnvironmentalFunction = {...};

이렇게 [[Parent]] 에 들어갈 객체가 무엇인지 이해하는 것은 쉽습니다(문제는 그 의미입니다^^)
 
 

클로저 Closure

  1. 일단 환경함수 내부에서 구상함수를 생성해서
  2. 그 구상함수가 만약 환경함수 밖으로 안나간다면
  3. 환경함수의 EC 는 소멸되겠지만
  4. 구상함수가 환경함수 밖으로 유출되면 환경함수의 EC 는 가비지컬렉팅이 되지 않습니다.

우선 위의 코드가 환경함수 밖으로 구상함수가 유출되지 않는 상황이었습니다.

function Env(){
	var f = function Con(){};
	//내부에서만 처리되고 끝
}

하지만 일급객체 특성 상 외부에 값으로 유출할 수 있습니다.

function Env(){
	return function Con(){};
}

var f = Env();
  1. 이렇게 되면 fCon 함수의 참조를 쥐고 있게 됩니다.
  2. 근데 Con 함수 객체에는 [[SCOPE]].[[Parent]]EC 를 쥐고 있습니다.
  3. 활성화된 참조가 있는 이상 그 EC ( Env() 할 때 생겨난) 는 가비지컬렉팅 될 수 없게 됩니다.

원래 EC는 함수가 호출되는 시점에 탄생해, 함수내부의 코드가 실행완료되면 소멸된다고 말씀드렸는데, 위와 같은 구조로 인해 소멸될 수 없게 된 것입니다.

즉 갖혀버린거죠(Closure!)

그래서 클로져인 것입니다. 자바스크립트에서 정확하게 클로져란 소멸되어야 할 EC 가 위와 같은 이유로 갖혀버린 상황 그 자체를 의미하는 말입니다.
유명한 IE 의 클로져 메모리릭은 정확히 이해하지 않으면 무슨 미신처럼 퍼지는데, 저렇게 한번 가비지컬렉팅되지 않는 대상을 완전히 대상에서 배제해서 브라우저를 종료할 때까지 페이지를 새로고침해도 메모리에서 해지되지 않는 현상입니다.

과거에는 심각한 버그였는데, 지금은 원페이지형 앱을 지향하는 사이트가 많아서 딱히 문제가 없는 것 같기도 합니다 ^^;
 
 

스코프 체인

긴 여정을 오시느라 수고하셨습니다. 함수형 언어로서의 자바스크립트 따위 알까보냐. 진짜로 이해 해야 할 것은 스코프체인입니다.

여태까지 장황하게 설명한 자바스크립트의 함수 시스템은 전부 스코프체인을 설명하기 위한 것입니다.
사실 이미 체인 시스템 한 가지를 소개했습니다.

프로토타입 체인이죠.

스코프 체인도 완벽하게 동일한 알고리즘으로 동작합니다.

function getValue( $key ){
	var result = this;

	do{
		if( result[$key] ) return result[$key];
	}while( result = result.[[Parent]] )

	throw new Error( "정의되지 않은 변수인걸?" );
}

EC.getValue( 변수명 );

이전 코드에서 바뀐거라곤 __proto__[[Parent]] 가 되었을 뿐입니다(사실 __proto__ 도 브랜든 할베가 파서짜면서 왠지 그렇게 이름을 지어버려서 그렇지 ECMA 표준에 등록된건 [[…]] 로 된 이름입니다)
즉 함수가 함수 내부의 코드를 실행하면서 만나는 변수 이름을 EC 에서 찾다가 없으면 EC.[[Parent]] 에서 찾고 거기도 없으면 EC.[[Parent]].[[Parent]] 에서 찾는 식입니다.
그러다가 마지막에는 찾을 수 없는 이름도 있습니다.

이 경우는 의미상 단지 해시맵의 키를 찾는데 실패한게 아니라, 변수의 이름을 찾는데 실패한 것이 되므로 더 이상 컴파일러가 코드를 실행할 수 없게 됩니다. 따라서 즉시 정의되지 않은 변수라는 에러를 피토하고 모든 실행이 정지됩니다.
 
 

공유되는 함수 메모리

저 까마득히 위를 보시면 어휘환경에 대한 내용 중에

한 마디로 EC를 함수 호출 시마다 생성하는 이유는 함수 내부의 코드가 실행되면서 등장하는 변수 이름을 찾는 메모리 공간으로 사용하기 위해서 입니다. 따라서 함수는 호출될 때마다 함수 안에서 a 를 찾으면 매번 별도의 메모리 공간에서 찾게 됩니다.

라는 내용이 있습니다.

함수 내부에서 각 지역변수와 인자는 독립적인 메모리 공간에서 움직이고 그렇게 됨으로서 함수실행의 원자성이 보장됩니다. 실제로 EC 야 실행 시마다 생성되고 소멸되니 그 조건에 부합합니다.
하지만 EC안의 [[Parent]] 는 스코프에 있는 하나의 객체를 참조하고 있습니다. 따라서 함수 내부에서 각 함수 호출 시마다 공유되는 메모리가 됩니다.
이를 완전히 이해하기 위해 환경함수와 구상함수를 제작해보죠.

function ENV(){
	var data = "data";
	return function CON( $data ){
		alert( data +"::"+ $data );
		data = $data;
	};
}

var con = ENV();
con( "hika" ); // data::hika
con( "nike" ); // hika::nike

이 현상을 정확하게 규명해보죠.
우선 ENV 가 호출되는 con = ENV() 시점의 EC_ENV 를 생각해봅니다.(환경함수의 환경함수는 전역으로 가정했습니다)

var con = ENV();

EC_ENV = {
	this:global,
	arguments:{length:0},
	data:undefined
	[[Parent]]:global
};

이러한 EC 객체가 ENV 에서 반환될 함수의 스코프에 잡혀있을 겁니다.

이 상태에서 ENV 함수가 실행되면,
첫번째 줄에서 var data = “data”를 하게 되므로
EC_ENV.data = “data” 인 상태가 됩니다.

이 상황에서 첫번째 con의 호출을 살펴봅시다.

con( 'hika' );
EC_con1 = {
	this:global,
	arguments:{0:'hika', length:1},
	$data:'hika',
	[[Parent]]:EC_ENV,{
		this:global,
		arguments:{length:0},
		data:'data',
		[[Parent]]:global
	}
};

이런 상황입니다. 따라서 내부 코드인 alert( data +”::”+ $data ); 를 실행하려고 하면

  1. data, $data 변수를 EC_con1 에서 찾게 됩니다.
  2. $dataEC_con1 에 포함되어 있으니 곧장 ‘hika’ 를 찾았습니다.
  3. data 의 경우 스코프체인을 타고 EC_con1.[[Parent]].data 에서 찾게 됩니다. 결과적으로 ‘data’ 입니다.
  4. 따라서 alert( data +”::”+ $data ); 의 결과는 data::hika 입니다.
     

다음 줄에서 data = $data 를 하게 됩니다. 헉 그런데…!

  1. 프로토타입체인 시스템에서는 get은 체이닝을 하지만 set은 자신에게만 쓴다고 했습니다.
  2. 마찬가지로 data = $data를 했다면 저 원리대로 EC_con1.data = $data 가 되어야 합니다.
  3. 하지만 스코프체인 시스템은 체이닝에서 찾은 해시맵에 쓰게 되어 있습니다. data[[Parent]]에서 찾았으므로
  4. EC_con1.[[Parent]].data = $data 가 됩니다.
     

이러한 원리로 함수 내부의 코드는 자신의 EC만 더럽히는게 아니라, 체이닝 상에 있는 [[Parent]] EC 를 더럽힐 수 있고 실제 그 범위도 예측하기 어렵습니다.
이제 두 번째 con( “nike” ); 을 하게 되면 이미 EC_con1.[[Parent]].data = ‘hika’ 인 상태이므로 결과는 hika::nike (data::$data) 가 되어버립니다.
이는 동작 자체만 놓고 본다면 마치 c의 함수 static변수 같습니다. 하지만 [[Parent]]에 잡힌 ECENV 에서 만들어낸 모든 함수에 해당됩니다. 이 의미와 피해를 알기 위해 함수를 한 개 이상 만들어서 외부로 반환해보죠.

function ENV(){
	var data = "data";
	return {
		f1:function f1( $data ){
			alert( data +"::"+ $data );
			data = $data;
		},
		f2:function f2( $data ){
			data += $data;
		}
	};
}

var fs = ENV();
fs.f1( 'hika' ); //data::hika
fs.f2( '***' );
fs.f1( 'nike' ); // hika***::nike

아까와 큰 차이점은 ENV 에서 f1, f2 두 개의 함수를 만들었다는 것입니다. 이 두개의 함수가 [[SCOPE]] 에는 동일한 EC_ENV 객체를 참조하게 됩니다. 그 덕분에 f1내부에서 data를 건드려도 f2 내부에서 건드려도, 같은 EC_ENV 객체의 값을 수정하게 되죠.

이 시점에서 이미 명백하게 cstatic 변수와 달라지게 됩니다. static 변수는 함수 내부의 변수인데 비해, 스코프체이닝은 같은 환경함수 내부에서 탄생한 모든 함수가 공유하는 어휘공간이 되어버립니다.
일종의 그들(f1, f2)만의 전역공간입니다. 이걸 적극적으로 활용하게 되면 재밌는 프로그래밍이 가능합니다.

..만 격리된 메모리 공간으로서의 함수실행을 예상한 알고리즘만 짜오던 자바, c++ 개발자에겐 완전히 패닉이 됩니다.

이 패닉에서 구출해 줄 수 있는 방법은 아예 처음부터 함수 내부에 등장하는 변수에는 세 가지가 있다고 생각하는 것 밖에 없습니다.

  1. 함수내부의 지역변수 : 기명인자와 var 또는 함수선언식으로 통해 만들어진 변수명
  2. 전역변수 : 전역객체의 키나 전역객체의 var 또는 함수선언식으로 통해 만들어진 변수명
  3. 스코프체인변수 : 실행 중인 함수가 생성될 당시의 환경함수에 포함된 기명인자와 var 또는 함수선언식을 통해 만들어진 변수명
     

이는 꼭 세심하게 개념적인 차이를 이해하고 외워야합니다.

예를 들어 자바에서 변수이름을 찾을 때는 딱 두 가지 원리만 사용됩니다.

  1. 중괄호 중첩시 내부 괄호에서 선언된 이름을 우선 시 하여 찾는다
  2. 메서드인 경우 선언된 변수가 없다면 this 의 필드에서 찾는다.
     

하지만 자바스크립트는 완전히 다른 것입니다. 호출한 함수가 내부에 어떤 코드를 갖고 있냐가 아니라 그 함수의 생성 당시 환경함수의 EC 에 포함된 변수를 찾는다 입니다.
타 언어에서는 이런 경우 직접적인 메모리참조 변수를 얻을 방법이 필요합니다(주로 인자로 넘어오죠)
또한 이 시스템은 개발시엔 언어적으로 심각한 문제점을 내포하고 있습니다.

사실 브렌든 할베가 이 언어를 만들 당시에 격리나 협업에 대한 고려를 많이 하지 않고 전부 가볍게 짧은 코드를 개인이 다 만들거라고 가정한데서 비롯된건지도 모릅니다. 예를 들어 보죠. 제가 ENV 함수를 감춥니다.

<script src="env.js"></script>

var fs = ENV();
fs.f1( 'hika' ); //data::hika
fs.f2( '***' );
fs.f1( 'nike' ); // hika***::nike

같은 호스트코드지만 완전히 패닉입니다. 설령 제가 f1, f2의 코드를 봐도 소용없습니다. 왜냐면 f1, f2가 만들어질 당시의 환경을 봐야하기 때문입니다.
ENV 함수 전체 코드가 필요할 뿐만 아니라 ENV 의 환경함수가 있는지도 알아야하고 그 환경함수의 환경함수도 있는지 알아야합니다(전체 코드를 알아야합니다 ^^) 그래야 data 의 영향범위를 정확히 알 수 있습니다.
이 문제는 다중 스코프체인으로 들어가면 더욱 심각해집니다.

환경함수를 두개로 확장하고 그 결과를 취합하여 얻어봅니다.

function ENV(){
	var data = "data";

	function E1(){
		return function f1( $data ){
			data += $data;
		};
	}

	function E2(){
		return function f2( $data ){
			data += $data;
		};
	}

	return {f1:E1(), f2:E2()};
}

var fs = ENV();

위의 경우 ENV 안에서 다시 E1, E2 가 탄생하고 그들을 각각 호출하여 f1, f2 를 얻게 됩니다.
사실 f1, f2 는 완전히 다른 환경함수에서 탄생했지만 그들 안에 있는 data 변수는 [[Parent]].[[Parent]].data 를 동일하게 참조하고 변경하게 됩니다.
스코프체이닝이 환경 함수 내부에서 생성된 모든 병렬 계층의 함수에 적용되는데 비해, 함수 자체는 함수 내부의 내부의 내부 라는 식으로 수직적으로 확장되기 때문에 계층이 다른데도 변수가 참조되는 문제가 발생됩니다.

자바스크립트로 스코프체인 시스템을 설계할 때 바로 이 점을 주의깊게 설계하지 않으면 안됩니다. 이러한 설계론을 집중적으로 다루는 책은…마땅히 없습니다 불행하게도 ^^;

이 구조는 복잡하고 혼란을 야기한다고 자바나 c++ 개발자에게 치부되는게 보통이라 나쁘다고들 하지만, 그럴리가 있겠습니까 ^^;
(이 구조를 잘 이용한 설계론을 일반화시켜 전개하지 않았을 뿐입니다. 불모지니 누군가 스코프체인 설계 패턴을 만들어 널리 퍼트리면 창시자가 될 기회가 있습니다 !)
..나름대로 저의 패턴들이 있지만 그건 또 나중에 기회가 되면 소개해보겠습니다.
 
 

스코프체인의 성능문제

프로토타입이 체이닝으로 인한 성능문제를 야기하는데 스코프 체인은 성능에 문제가 없을까요?
당연히 있습니다. 프로토타입체인과 완전히 동일한 비용이 발생합니다. 다중으로 참조해야하는 스코프체인변수를 이용하는 알고리즘을 매프레임마다 십수번씩 호출하면 성능저하는 확연합니다.
최초 이 포스팅이 프로토타입 체인이 느린 것을 해결하기 위해 직접 키에 복사하는 주제였습니다(까마득하죠? 젤 위로 가시면 그런 내용이 있습니다 ㅎㅎ)
마찬가지로 스코프체인이 깊숙한 경우 카피를 통해 체이닝 단계를 줄일 수 있습니다.

프로토타입은 객체에 직접 키를 넣어줌으로서 단축시켰지만 스코프체인은 환경함수의 인자로 넘겨주는 것을 통해 체이닝 단계를 줄입니다. 보는게 빠르죠.

function ENV(){
	var data = "data";

	function E1(){
		return function f1(){
			alert( data );
		};
	}

	return E1();
}

var f = ENV();

위 코드에서 ENV 안의 E1f1 을 만들어서 반환합니다. f1 은 내부에서 data 변수를 찾을 때 본인 EC 에도 없고 환경함수인 E1EC 에도 없으므로 ENVEC 까지 가서 찾습니다.
EC_f1.[[Parent]].[[Parent]].data 에서 찾아오죠. 이걸 1단계 줄여서 EC_f1.[[Parent]].data 에서 찾게 하려면 E1입장에서 지역변수나 기명인자로 정리해버리면 됩니다.

function ENV(){
	var data = "data";

	function E1( data ){
		return function f1(){
			alert( data );
		};
	}

	return E1( data );
}

var f = ENV();

이렇게 되면

  • E1data는 기명인자로 존재하게 되므로
     
  • f1 은 더 이상 ENVEC까지 가지 않고,
     
  • E1EC 에서 찾아와 EC_f1.[[Parent]].data 수준에서 얻게 되어
     

한 단계를 줄이게 됩니다.

코드의 예제에서는 한 단계를 점프시킨 정도지만, 설계구조에 따라 몇 단계를 취합할 수도 있습니다.
스코프 체인이 깊어지는 경우 세심하게 중간 클론층을 설계해주는 것도 중요한 스킬입니다.

….만 프로토타입체인의 키를 죄다 캐쉬 잡듯이 크롬같은 최신 브라우저는 짜피 스코프 체인도 캐쉬 객체에 전부 때려박기 때문에 별로 상관없습니다.(하지만 모바일브라우저나 TV 용 브라우저 등 IE6 를 능가하는 저성능 브라우저는 더욱 쏟아져 나오고 있습니다 ^^)

 
 
 

자바스크립트 모듈

헉. 그렇습니다. 원래 자바스크립트 모듈을 설명하려다가 함수 시스템 전체를 설명하고 말았습니다.
정신을 챙기고 원래 소스로 돌아옵시다.

var Alert = ( function(){
	function action(){
		alert( this._msg );
	}
	return function Alert( $msg ){
		return {_msg:$msg, action:action};
	};
} )();

(기억이 아득하시겠지만 젤 위에 이런 소스가 있었습니다 ^^;)

익명함수를 사용하면 일종의 스코프공간을 생성하는 것이나 마찬가지 입니다. 따라서 함수 안에서 선언된 기명인자와 지역변수 또는 함수선언식으로 생성된 변수는 죄다 EC 에 잡히겠죠.
그 익명함수 내부에서 함수를 만들게 되면 EC 를 통해 환경함수인 익명함수의 요소를 사용할 수 있게 됩니다. 하지만 외부에서는 action 함수가 노출되지 않게 되죠.
(물론 완전히 노출이 안되는건 아니고 Alert 함수를 실행해서 얻은 오브젝트의 키를 뒤지면 참조를 얻을 수 있지만 명시적인 참조가 불가능하다는 것입니다)
사실 아무리 스코프 공간을 생성해 격리해도 객체는 보호할 수 없습니다.

객체는 참조되기 때문에 참조를 파고들면 원본에 접근할 수 있게 됩니다. 하지만 객체가 아닌 기본형(숫자, 문자, 참거짓 등)은 복제로 들어오기 때문에 보호받을 수 있는 거죠.
그걸 어기면 사실 스코프로 막아도 보호할 수 없습니다. 예를 들어보죠.

var f = ( function( ){

	var data = [1,2,3,4];

	return function f(){
		return {a:data};
	};
})();

var arr = f().a;

이 경우 arr 을 조작하면 결국 data 가 조작됩니다. 즉

var arr1 = f().a;
alert( arr1[0] ); // 1
arr1[0] = 3;

var arr2 = f().a;
alert( arr2[0] ); // 3

이런 식으로 객체가 전달되면 참조의 연쇄를 타고 실 객체를 가리키게 되기 때문에 원본이 오염되는데, 이 경우 원본은 내부의 f 함수의 스코프에 있는 EC가 오염되는 것입니다.
따라서 오염을 방지하려면 객체는 반드시 복제해서 보낼 수 밖에 없습니다.

var f = ( function( ){

	var data = [1,2,3,4];

	return function f(){
		return {a:data.concat()}; //복제로!
	};
})();

var arr1 = f().a;
alert( arr1[0] ); // 1
arr1[0] = 3;

var arr2 = f().a;
alert( arr2[0] ); // 1 !

사실 이 내용은 스코프 관리라기 보다 참조의 오염을 막는 일반적인 방법론이죠.

대다수의 모델시스템을 지원하는 프레임웍에서는 복제본 반환을 기본으로 제공합니다(백본이나 앵글러나..)

함수객체의 오염을 방지하기 위한 복제본은 존재하지 않습니다. 함수객체의 오염을 막으려면 매번 새 함수를 만들어야겠죠 ^^;
 
 

함수선언문

모듈의 다양한 패턴을 이해하기 위해 함수선언문이라는 문법이 내부적으로 어떻게 매크로로 전환되는지 알아야 합니다. 이 구문이 너무 자주 나옵니다.

//함수선언문
function Alert(){
}

//진짜 내부에서 일어나는 일
var Alert = function Alert(){
};

함수선언문은 실제로 문(statement)가 아닙니다. 그냥 호이스팅이란 기능이 붙은 매크로입니다. 이 매크로가 해주는 것은 세 가지입니다.
선언문에 있는 함수이름을 이용해

  1. 같은 이름의 지역변수를 생성하고 : var Alert
  2. 함수객체를 만들며 : function(){};
  3. 그 함수의 name속성에 같은 이름을 넣어준다 : function Alert(){}
     

이 세 가지 기능을 한 방에 처리해주는 매크로가 바로 함수선언문입니다. 단지 이렇게 하면 컴파일러는 무엇보다 더 빨리 함수선언문부터 처리하기 때문에 다른 지역변수 선언이나 할당보다 빨리 처리하는 우선권을 갖게 됩니다. 이를 호이스팅이라고 하는데 간단히 코드로 표현하면 아래와 같습니다.

var Test;
alert( Test ); // 순서대로라면 undefined 하지만 함수가 나옴

function Test(){
}

분명히 아래서 Test 를 생성했지만

  1. 컴파일러가 우선적으로 Test 지역변수를 생성하고,
  2. 거기에 함수 객체를 할당한 뒤에나 나머지가 실행되므로

이미 var Test; 구문조차 중복 지역변수 선언일 뿐더러, Test 에는 함수객체가 들어가 있는 거죠.
하지만 이는 조건지연과 만나면 골때립니다.

function A( a ){
	var Test;
	alert( Test );

	if( a > 3 ){
		function Test(){
		}
	}
}

위의 조건문을 살펴보면 a 가 3 보다 작은 경우는 실행되지 않습니다.
원래는 쭉 실행 되어야 하는 문이 조건에 따라 즉시 실행되지 않고 스택에 머물고 조건을 판단한 뒤에 실행되는 경우 조건지연이라 합니다.
이 조건지연에 따르면 함수선언문은 실행될수도 있고 실행되지 않을 수도 있습니다.

하지만 처음 말했던 컴파일러가 어떠한 코드보다 빨리 처리한다는 것과 상충되지 않나요?
이 모순이 존재하는데 실제 여러 브라우저에서 어떻게 될까요?

..

..

..실험해보세요! ( 갑자기 답을 알려드리기 싫은 이 맘을 어쩔 수가 ^^/ 비사이드의
잔잔한 기술력은 이런 되도않는 실험을 무지하게 한 덕분인지도 ㅋㅋ )

대신 다른걸 말씀드리죠. 이건 이론 상 명백히 모순됩니다. 따라서 ES5 에서는 조건지연이 일어나는 반복문, 조건문 등의 내부에서 함수선언문을 사용하는 것을 금지시켰습니다.

자바스크립트 모듈은 스코프를 포함한 함수 시스템 전반을 아울러 매우 독특한 사용법이 존재합니다. 이제 그러한 형태를 소개하는걸로 마무리할까 합니다.

수고했다는..제가!

근데 아직 밑에도 내용이 많이 남아있습니다 ^^;
 
 

지연정의모듈

지연정의되는 함수의 코드를 보겠습니다.

function Alert( $msg ){

	//스스로를 재정의한다.
	Alert = function( $msg ){
		var result;
		result = {_msg:$msg, action:action};
		return result;
	};

	function action(){
		alert( this._msg );
	}

	return Alert( $msg );
}

var temp = Alert( "hello" );
temp.action(); //hello

위의 코드는 호스트 코드 입장에서는 Alert 이 걍 멀쩡히 존재하는 것처럼 보이지만 실은 완전히 다른 상황입니다.
최초의 Alert 함수는 존재는 하지만 껍데기역할만 하고 실체는 없습니다. 일단 호출되면 진정한 Alert의 본체를 재정의합니다.
그리고 최종적으로 재정의된 Alert 을 내부에서 호출하여 결과를 반환하기 때문에 호스트코드에서는 차이를 인식할 수 없을 뿐입니다.

왜?

왜 이런 짓을 할까요. 이 패턴을 사용하는 이유는 보통 프록시패턴을 쓰는 것과 같은 이유입니다. 보통 두 가지죠.
 
 

프록시만 넘겨주고 진짜는 나중에 처리

로그를 처리하는 함수를 하나 생각해보죠. html 상에 로그를 보여주려고 하는데 실제 로그를 보여줄 div 가 로딩되었는지 확인해야 합니다.
dom 의 완전한 로딩을 기다려야만 로그를 사용할 수 있죠.

하지만 프록시를 넘겨서 로그를 미리 큐를 받아두고 로딩이 완료되면 큐에 있는걸 출력한 뒤 이후부터는 큐가 아니라 진짜 div 에 써가면 됩니다.
즉 로그 함수의 즉시성을 확보할 수 있습니다. 코드로 구현해보죠

….근데 이제부터…

읽고 계신 분들은 위의 내용을 다 이해하셨다고 가정하고 인정사정없이 js 코드를 사용하겠습니다.
무정하다 원망말고 꾸준한 복습만이 살 길입니다!
(..실은 짧게 짧게 풀어쓰다가 글이 길어져서 지쳤습니다. =.=; 걍 제 스타일대로 막 짜버리지 않으면 글을 포기할거 같아서…^^;;;)

…구현하려고 했더니 한 가지를 더 설명드려야하는데 =.=;

더블디스패치(double dispatch)라는 기법에 대해서 입니다.

간단하게만 설명하겠습니다. 자세한 내용은 켄트백할베의 구현패턴을 보시라고 하고도 싶지만 그 책에서도 매우 살짝만 다루고, 상태패턴이나 데코레이터 패턴도 전형적인 더블디스패치 응용이지만 이 관점으로 설명한 책이 별로 없으니…걍 나중에 코드로 이해하셔도 됩니다(머래니..)

  1. 참조 또는 포인터를 사용하는 언어에서는 객체 스스로가 외부의 참조된 포인터를 바꿀 수 있는 능력이 없습니다.
  2. 따라서 외부에는 일부러 실객체의 포인터를 감싼 객체의 포인터를 줍니다. 그럼 호스트코드에서는 감싸진 객체에 키나 메서드로 실객체를 참조하게 되는데,
  3. 이 원리를 이용하면 실객체가 호스트코드에게 넘겨준 래핑객체의 내부를 바꿈으로서 실객체를 교체할 수 있는거죠.

설명은 어렵지만 많이 봐왔을지도 모릅니다. 코드로~

var logger = ( function(){

	//dom 로딩체크 플래그
	var isLoaded = false;

	//귀찮으니 jq에게 시키자
	$( function(){
		isLoaded = true;
	} );

	//로딩되기 전까지 수고할 로그큐
	var que = [];

	//반환될 래퍼객체와 그안의 log함수
	var wrapper = {log:function log( $log ){
		//아직 dom로딩이 안되어있으니 열띰히 큐에 넣어두자
		if( ! isLoaded ){
			que[que.length] = $log;
			return;
		}

		//여기에 도달했다면 dom이 로딩된 것이다.

		//일단 que에 뭐가 있으면 우선 출력하자.
		if( que.length ) $( '#log' )[0].innerHTML = que.join('<br>');

		// log함수를 재정의하자
		wrapper.log = function log( $log ){
			//이젠 걍 쓰면 된다.
			$( '#log' )[0].innerHTML += $log + '<br>';
		};
	} };

	return wrapper;
} )();

logger.log( "aaa" );
logger.log( "aaa1" );

이 때 더블디스패치의 원리는

  1. 익명함수가 wrapper 를 호스트코드에게 포인터로 알려주고,
  2. 호스트코드에서는 wrapper 를 직접 사용하지 않고 .log 를 통해 사용함으로서,
  3. 런타임에 내부 포인터 참조 연산을 매번 새로 해준다

는 점입니다.

이렇게 전달된 포인터와 호스트코드가 사용하는 포인터가 다르기 때문에 wrapper 내부에서 log 에 할당될 객체를 교체해도 호스트코드에서는 알아차릴 수 없게 되는 거죠.
(플래이스홀더 – PlaceHolder 가 아니라 진짜 프록시를 구현하는건 원래 좀 빡셉니다. 쥐도새도 모르게 호스트코드가 그대로인 상태로 교체되어야하기 때문이죠^^)

이제 dom 로딩에 맞춰 wrapper.log 가 자동으로 변경되기 때문에 dom 이 로딩되기 이전이든 이후든 자유롭게 로그를 사용할 수 있게 되었습니다.
 
 

컴파일부하 회피

자바스크립트 컴파일이 순식간에 된다고 생각하시나요?

브라우저는 자바스크립트 태그를 만나면 그 안의 내용이 완전히 파싱되어 메모리에 적재되고 실행마저 되지 않으면 모든 동작을 정지시킵니다.
자바스크립트의 로딩이나 해석은 브라우저의 작동을 완전히 정지시키는 가장 강력한 시스템입니다.
40k 대의 js를 파싱하여 적재하는데 기계 차이야 있지만 0.2 초 이상이 걸립니다.

하물며 여러 유명 사이트 가서 모니터해보세요. 족히 300k 는 넘게 로딩합니다. 용량이 문제가 아니라 로딩이 완료된 상태에서 이걸 파싱하는데만 0.5초가 넘어갑니다.

따라서 정말 필요한게 아니면 파싱하고 싶지 않다는 욕구도 자연스레 드는 법입니다.

대표적으로 document.querySelectAll 을 생각해봅시다. 이게 지원되는 브라우저는 이걸 쓰면 그만입니다.
지원안하면 sizzle.js 를 쓰는 전략을 세웠다고 해보죠.

하지만 매번 셀렉터를 호출할 때마다 이런 판단을 하기도 싫고 sizzle.js 는 게다가 동적으로 파싱해야하는 상황입니다.
이 경우 지연정의모듈은 로딩을 포함하긴 어렵지만 파싱을 지연시키는건 간단합니다.

<html>
<head>

<script id="sizzle" type="text/sizzle">
	//여기에 시즐코드를 둔다!
</script>

<script>
function Select( $selector ){
	if( document.querySelectAll ){
		Select = function Select($selector ){
			return document.querySelectAll( $selector );
		};
	}else{
		var s = document.createElement( 'script' );
		document.getElementsByTagName('head')[0].appendChild( s );
		s.text = document.getElementById( "sizzle" ).text;
		Select = function Select( $selector ){
			return sizzle( $selector );
		};
	}
	return Select( $selector );
}

var aa = Select( '#id' );

이렇게 하면 최초 스크립트가 파싱될 때의 부하는 sizzle 을 포함하지 않기 때문에 매우 가볍습니다.
또한 sizzle 이 필요할 때만 파싱될 것이라 필요하지 않는 경우는 아예 파싱 자체를 하지 않겠죠. 이 경우에도 지연정의 패턴이 매우 유용합니다.
 
 

클래스 팩토리 모듈

모듈화 되어있는 함수를 곰곰히 생각해보면 공통요소를 EC 에 잡아두고 상황에 따라 다른 함수를 생성하여 반환하는 구조를 만들 수 있다는걸 알 수 있습니다.
이는 마치 공통요소를 부모클래스에 두고 개별 특징을 자식클래스로 정의한 것과 마찬가지인 상황이 됩니다. 따라서 특징에 맞는 자식클래스를 만드는 팩토리로 사용할 수 있습니다.
이는 사실 프로토타입체인도 마찬가지 입니다만 프로토타입체인으로 만들어진 구조는 오직 메서드만 유의미한데 비해 모듈로 분화하면 변수를 공유하고 격리시킬 수 있습니다.
그렇다면 가장 이상적인 것은??

모듈 안에서 프로토타입을 처리하는 것일 수 있습니다.

이번에는 계층구조의 대명사 그래픽클래스를 이용해 예제를 생각해보겠습니다.

우선 등장하는 클래스를 소개합니다.

  • display : x, y 를 갖고 있고 X(), Y() 메서드가 있다.
     
  • box : width, heigth 를 갖고 있고 W(), H() 메서드가 있다.
     

일단 순수하게 프로토타입으로 만들어보죠(아까 말씀드린대로 이미 스코프를 아신다고 가정하고 코드는 인정사정없이 가겠습..)

//각 속성별 메서드를 만들어주는 함수...귀찮..=.=;
function maker( $k ){
	return function(){
		return arguments.length?(this[$k]=arguments[0]):this[$k];
	};
}

function display(){ this.x = this.y = 0; }
display.prototype.X = maker('x');
display.prototype.Y = maker('y');

function box(){ this.width = this.height = 100; }
box.prototype = new display; //상속
box.prototype.W = maker('width');
box.prototype.H = maker('height');

일단 클래스별 정의와 메서드 할당을 끝냈습니다. maker 가 만들어내는 메서드는 간단한데 인자가 있으면 해당 키를 갱신후 반환하고 아니면 그냥 원래 키에 있는 값만 반환합니다.
jq 등에서 자주 쓰이는 형태입니다.

하지만 뭔가 허전합니다. 바로 부모의 생성자 호출을 깜빡한 것입니다(생성자 체인을 저번 포스팅에서 안다루고 갔더니 결국 여기서 나오는군요 =.=;)
생성자를 체이닝하도록 개선합니다.

function box(){
	this.constructor.call( this );
	this.width = this.height = 100;
}

요정도면 됩니다. 간단하게만 짚어보죠.

this.constructor
== this.__proto__.constructor
== box.prototype.constructor
== (new display).constructor
== display.prototype.constructor
== display

이러한 이유로 display 즉 부모의 생성자가 됩니다. 생성자라곤 해도 함수일 뿐이니 call 을 이용해 this 를 전달 해줘야 합니다.
이제 호스트코드를 작성해 자유롭게 사용해봅니다.

var a = new box;

a.X( 30 );
a.W( 50 );

alert( a.W() ); //50

문제는 무엇일까요?

a.x = 60;
a.width = 70;

이딴게 된다는 겁니다. 이래서야 X, Y, W, H 메서드를 만들 필요가 있긴 한겁니까?
이걸 스코프와 합체하여 막아봅시다.

여기서 사용할 아이디어는

  1. 데이터 저장소를 스코프로 옮기되
  2. 각 인스턴스별로 고유해야하므로 고유한 키를 부여한다

는 것입니다.

근데 고유한 키는 어떻게 부여할 것이며 그 키의 보안은 어떻게 할 것이냐의 문제가 남습니다.
키를 보호할 유일한 방법은 동적으로 키를 찾게 하는 것 뿐입니다만, 그렇게 되면 너무 연산 비용이 크게 발생합니다. 일단 이 방식의 코드를 보죠.

function makeDisplay(){
	//인스턴스별 x,y저장소
	var x = [], y =[];

	//고유한 객체키를 인덱스로 보관하는 배열
	var id = [];

	function display(){
		//생성하자마자 id에 넣어준다.
		var i = id.length;
		id[i] = this;

		//해당위치의 변수도 초기화한다.
		x[i] = y[i] = 0;
	}

	display.prototype.X = function(){
		//고유키를 얻는다.
		var i = id.indexOf(this);

		return arguments.length?(x[i]=arguments[0]):x[i];
	};

	return display;
}

이 방법을 쓰면 display 안에는 어떠한 키도 잡히지 않고 id 를 관리할 수 있습니다만, 메서드를 사용할 때마다 indexOf 를 해야한다는 압박이 있습니다.
인스턴스수가 적을 땐 괜찮지만 많아질수록 지옥입니다.

따라서 너무 집착하지 말고 적당히 숨겨진 키를 하나 쓰는 편이 낫습니다.

function makeDisplay(){

	//인스턴스별 x,y저장소
	var x = [], y =[];

	//고유한 객체키
	var id = 0;

	function display(){
		//__id키에 고유한 id를 넣어두자
		this.__id = id++;

		//해당위치의 변수도 초기화한다.
		x[this.__id] = y[this.__id] = 0;
	}

	display.prototype.X = function(){
		//__id를 이용한다.
		var i = this.__id;

		return arguments.length?(x[i]=arguments[0]):x[i];
	};

	return display;
}

하지만 __id 를 누군가 호스트코드에서 만질까봐 잠이 들 수 없습니다.
그럼 indexOf 보다는 싸고 __id 보다는 비싼 방법을 생각해보죠.
마찬가지로 별도의 스코프로 함수를 하나 생성하면 됩니다.

function makeDisplay(){
	//인스턴스별 x,y저장소
	var x = [], y =[];

	//고유한 객체키
	var id = 0;

	//id를 반환하는 함수를 만들어내는 함수
	function makeGetId( $id ){
		return function(){
			return $id;
		};
	}

	function display(){
		//id메서드에 makeGetId를 이용해 함수를 할당한다.
		this.id = makeGetId( id++ );

		//id()를 이용해 변수를 초기화한다.
		var i = this.id();
		x[i] = y[i] = 0;
	}

	display.prototype.X = function(){
		//id()를 이용한다.
		var i = this.id();

		return arguments.length?(x[i]=arguments[0]):x[i];
	};

	return display;
}

이렇게 대처하면 인스턴스가 만들어질 때마다 id 키에 함수객체가 하나씩 새로 생성되겠지만 어떠한 키도 변수로 잡지 않고 사용할 수 있게 됩니다.
컴터 프로그래밍 세계는 결국 메모리를 아낄 것이냐, 연산을 아낄 것이냐의 문제입니다.

  1. 처음 소개드린 indexOf 로 처리한다면 연산비용은 크게 들지만 display의 인스턴스 입장에서는 아무런 키도 잡히지 않게 됩니다.
     
  2. 두번째의 __id 를 사용하는 경우 인스턴스에 키가 생기지만 X() 호출 시 연산비용이 없어집니다.
     
  3. 마지막으로 id 에 함수객체를 할당하는 것은 가장 큰 메모리 비용을 내면서 동시에 함수 호출이라는 연산비용도 지불합니다. 하지만 연산비용이 indexOf 보다 작고, 두번째 안에 비해 변수를 보호할 수 있게 됩니다.
     

이는 어떤 전략이 절대적인 우위가 있는 것이 아니라 클래스별 상황에 맞춰 선택해야하는 전략입니다. 구지 id 의 문제가 아니라도 자바스크립트에서 변수를 보호하는 위의 세 가지 방식은 다양한 국면에서 개발자의 의사결정을 기다리고 있습니다.

..
..
요정도로 대처하고 호스트코드를 보겠습니다 ^^;

var display = makeDisplay();

var d = new display;

d.X( 50 );
alert( d.X() ); //50
alert( d.x ); //undefined

이제 원하는대로 굴러가는 느낌입니다. 변수는 감췄고 메서드는 공개했습니다. 이제 box 로 상속계층을 향해 확장합니다.

function makeBox( $parent ){
	var width = [], height = [];

	function box(){
		this.constructor.call( this );
		var i = this.id();
		width[i] = height[i] = 100;
	}

	box.prototype = new $parent;
	box.prototype.W = function(){
		var i = this.id();
		return arguments.length?(width[i]=arguments[0]):width[i];
	};

	return box;
}

짜피 생성자 체이닝을 하고 있으므로 display 가 새로운 id 를 받아옵니다. 구지 box 에서 새로 id 를 발급할 필요는 없습니다.
생성자 체인을 호출하여 displaythis.id 를 먼저 생성하게 해야합니다.
간략히 호스트코드를 봅니다.

var box = makeBox( makeDisplay() );

var b = new box;

b.X( 30 );
b.W( 50 );

각 상속계층에 맞춰 자신이 참조하는 스코프가 다르면서도 프로토타입체이닝으로 메서드는 공유할 수 있게 되었습니다!
 
 

그 외의 모듈 패턴

정말 재밌는 생각을 인간들이 엄청했습니다. 모듈과 관련된 패턴은 무궁무진하게 있습니다. 헌데 크게 보면 두 가지로 나눌 수 있습니다.

..
우선 모듈과 호스트코드 간의 통신 방법에 대한 패턴입니다.

모듈이란 스코프로 만들어진 이름공간인데 위에 제가 작성한 코드 태반은 제각각입니다. 함수를 반환하기도 하고 오브젝트를 반환하기도 하죠.
예를 들어 모듈이 언제나

래퍼객체와 그 래퍼객체의 키로 원하는 기능을 구현하여 반환해야한다

라고 약속하면 호스트코드는 보다 일관성 있게 모듈을 사용하도록 작성될 것입니다.

즉 이것은 개발적인 패턴이라기보다는 일종의 프로토콜에 대한 것입니다. 이 분야는 사실 누가 깃발꼽냐로 결정되는거죠.

현재 가장 유력한 후보는 commonJS 라 불리는 약속인데, node.js 가 이걸 택해서 매우 널리 사용되고 있습니다(commonJS 에도 단점이 많은 지라 벌써 현재 버전을 뒤엎을 분위기가…)

..
또 한 가지는 위에 제가 생각나는대로 적은 모듈의 사용방법에 대한 다양한 패턴들입니다. 사실 프로토타입 체인과 스코프체인을 합체하면 끝없는 변태짓이 가능하기 때문에 이 동네의 숱한 변종이 널려있습니다.
구지 소개할 필요는 없지만 유명 js 라이브러리들은 제각각 나름대로의 변태짓을 하고 있습니다(이제 볼 수 있는 눈이 생겼으니 구경해보시면 재밌습니다 ^^)

 
 
 

결론

총 3회에 걸쳐 자바스크립트에서 클래스 구문을 대체하는 방법을 알아봤습니다.

다룬 주제 외에도 기존 OOP 언어의 거의 모든 카테고리에 대해 자바스크립트만의 해법이 존재합니다. 하지만 그건 또 다른 기회에 다루게 되겠죠.
정말 본의 아니게 너무 많은 언어론과 자바스크립트의 기저 시스템을 기술하고 말았습니다.
처음 컨셉은 자바, c++ 개발자가 자바스크립트 세계로 와서 겪는 고통을 좀 줄여줄까 하고 썼는데, 생지옥으로 안내한 느낌..

P.S
태반이 글쓰면서 생각나는대로 작성한 코드라 분명히 에러가 많이 있을 걸로 사료됩니다.
절대로 날로 쓰지마시고, 개념을 이해하시는데 참고하시길 바라옵고 정신건강 챙기시길 기원합니다.
오류레포트 해주시면 즉각즉각 수정하겠습니다!