자바스크립트 개발자들이 클로저에 대해서 매우 깊이 이해하는데 비해 as3를 쓰시는 개발자 분들은 놀랍게도 클로저를 거의 이해하지 못하고 있다는 게 어쩔 때는 참 당황스럽습니다. 그래서 그 이유부터 제 나름대로 짚어봤습니다.
- AS3가 매우 추상적인 수준으로 ECMA스크립트를 래핑하고 있어 하부의 prototype기반 구조를 거의 은닉한다.
- 동적 함수 생성을 자제하는 정적 구조를 권장하기 때문에 런타임 함수 생성을 기본적으로 사용하는 자바스크립트에 비해 거의 사용되지 않고 있다.
- 기본적으로 동적 언어에 대한 이해가 얕고 언어의 정적 형식 지원부분을 공부하여 흡수하기도 벅차한다.
스크립트 언어에 대한 이해가 얕은 정적 언어 개발자들은 as3도 자바나 c++의 연장선으로 사용하기 때문에 아예 동적인 측면을 사용하려 하지 않습니다. 반대로 자바스크립트나 as2를 사용하시던 분들은 as3는 정적 특성대로만 써야 as3를 제대로 쓰는 거다라고 얘기를 들어 동적인 스타일을 as3에서 사용하려 하지 않습니다.
동적 언어로서 as3
해서 먼저 as3가 근본적으로 동적언어로서의 특성이 무엇인지부터 명확하게 하고 넘어가겠습니다.
as3는 애시당초 vm에서 작동하는 동적 언어로 컴파일타임에 뭘 검사해줬던 문법이 어떻게 생겨먹었던 빌어먹을 프로토타입으로 구현된 vm으로부터 런타임에 모든 클래스 정의와 메모리 사용을 할당받아 로딩하는 완전한 동적언어입니다.
단지 as2에서 as3로의 변화는 공식문서에 나와있는 것 처럼 몇 가지 기능을 ECMA표준 프로토타입 스크립트에 추가한 것 뿐입니다.
일단 새로 추가된 기본 언어의 기능에는 봉인된 클래스라는 기능이 눈에 띕니다. 이건 다시 더 상세하게 설명하는 공식 문서가 있습니다. 바로 아래 링크의 글입니다.
ActionScript 3.0 클래스 객체
근데 이 글을 매우 자세하게 읽어보시면 아래와 같은 결론을 얻으실 수 있을 겁니다.
- as3는 프로토타입 엔진으로 구현된 언어를 근간으로 한다.
- 정적형식을 지원하는 자바와 c++처럼, as3도 정적 클래스마냥 생긴 걸 쓸 수 있게 가상적인 형식으로 제공한다(즉 진짜 정적 타입이 존재하는 것이 아니라 정적타입이라고 표시해두면 내부적으로 이에 대해 추가적인 처리를 통해 동적 타입이지만 캐쉬에 잡아주거나 요소를 공유하는 등의 처리를 한다)
- 2번의 정적타입을 흉내내주는 근본적인 방법은 vtable을 흉내내주는 traits객체다.

위의 클래스를 new했을 때 일어나는 일을 그려준 다이어그램이 뜻하는 바는 매우 간단합니다. 봉인된 클래스 문법으로 객체를 생성하는 경우 프로토타입체인 선 상의 객체에 대해 일반적인 프로토타입구조의 인스턴스인 클로저변수를 사용하지 않고 trait 객체를 생성하여 캐쉬 및 참조구조를 잡아준다는 뜻입니다.
만약 봉인된 클래스가 아니라 동적으로 생성하면?
그림에 있는 Pa를 가리키는 클로저 공간을 즉시 생성한 후 클로저 공간을 가리키는 참조변수를 반환해주는 식으로 인스턴스를 만듭니다. 대체 클로저 공간, 클로저 – 이게 뭔지에 대해? 설명을 쭉 해갈 것이지만 이러한 상황은 매우 흔하게 있다는 겁니다. 아래의 as3소스는 클로저 공간에 대한 인식이 필요합니다.? 아래 보이는 this들은 과연 무엇을 가리키는 걸까요?
var a:Object;
a = { instance:this, method1:hnRun };
a.method1( this );
function hnRun( $object:* ):void{
trace( this +":"+ $object );
}
동적 언어의 이해
동적 언어는 동적으로 메모리를 확보하여 사용하는 언어를 말합니다. 동적이다 하면 실행하는 시점에서 메모리를 확보한다는 뜻인데, 세상에 실행하기 전에 메모리를 확보하는 프로그램은 없습니다. 그러면 대체 동적 언어의 진정한 의미는 무엇일까요?
이와 반대되는 정적 언어도 물론 실행시점에 메모리를 확보합니다만, 동적 언어와의 큰 차이점은 실행시점을 크게 둘로 나눠 실행하자마자 초기화 단계에서 메모리를 확보하고 그 메모리에 먼저 기록할 걸 다 기록한 상태에서 진행된다는 점입니다. 더불어 대부분의 정적 언어는 메모리를 사용할 때 용도에 맞게 구분지어 사용하는 능력을 갖고 있습니다.
이에 비해 동적 언어는 모든 메모리를 하나의 거대한 공간으로 보고 있고 이 메모리를 실행하는 시점에 자유롭게 사용하는 언어입니다.
위의 추상적인 차이점으로 말미암아 다음과 같은 차이가 분명하게 생깁니다.
- 정적 언어가 최초 할당하여 기록한 초기화 값들은 이 후 실행하면서 건드릴 수 없게 할 수 있다(메모리를 분리할 타이밍이 있으므로 아예 격리하면 됨)
- 동적 언어의 입장에서 실행 도중 바꿀 수 없는 데이터란 없다. 모든 클래스의 정의, 함수의 정의 등도 전부 실행 도중 바꿀 수 있다.
2번의 특징이 바로 동적 언어의 대표적인 부분인데 동적 언어가 이렇게 생겨먹을 수 밖에 없는 이유가 있습니다.
- 대부분의 동적 언어는 직접 OS와 대화하지 않는다.
- 즉 플래시플레이어는 실제 OS위에 구동되는 프로그램이고 swf는 플레이어 위의 스크립트일 뿐 직접 OS와 대화하지 않는다.
- 따라서, swf입장에서 이용하는 모든 메모리는 OS로부터 받은 메모리가 아니라, 플레이어에게 받은 가상 메모리일 뿐이다.
- 이러한 동적 언어는 이미 그 동적 언어가 구동되는 시점에서는 벌써 플레이어가 구동한 상태이기 때문에 OS입장에선 실행 상태 중에 어떤 상태변화일 뿐이다.
- 따라서 정적 초기화하여 메모리를 분리할 기회가 없다.
- 따라서 동적 언어가 선언한 모든 클래스, 함수도 재정의가 가능하고 이로부터 생성된 인스턴스, 참조 등도 선언을 런타임에 변경함으로서 즉시 변화가 반영된다.
어찌보면 프로토타입이란 ECMA스크립트의 폐단이 아닙니다. 동적 언어의 숙명 같은 거죠. 그 숙명을 받아들이고 그걸 정리하여 인터페이스화 한 어떤 표준안 같은 게 바로 ECMAscript라고 할 수 있습니다.
as3도 기반적으로 이러한 동적 언어의 베이스 위에 구축된 형태입니다. 단 동적 언어의 위와 같은 특성은 매우 심각한 문제를 야기합니다.
- 모든 인스턴스는 자신의 틀이 되는 클래스가 변경될 수 있고, 또한 인스턴스마다 다른 속성이나 메쏘드를 부여할 수 있으므로 사실 상 하나의 클래스로부터 생성된 인스턴스라 할지라도 전부 다른 존재이다.
- 따라서 정적 언어처럼 인스턴스가 공유하는 메쏘드는 메모리에 하나만 생성하고 공유하지 않는 속성만 생성하는 식의 효율성을 추구할 수 없고 개별 인스턴스마다 모든 필요한 정보를 전부 메모리에 따로따로 생성해야 한다.
- 만약 상속구조를 쓰게 되면 더욱 심각하게 되어 개별 인스턴스별로 그 당시 상속한 모든 클래스를 인스턴스화 하여 전부 개별로 저장해야만 한다(상속받은 클래스조차 변경이 가능하므로)
- 또한 프로토타입을 통한 선언 부분의 변화가 광범위하게 영향을 끼치기 때문에 알고리즘적인 격리가 불가능하다.
- 이 모든 상황으로부터 하나의 인스턴스를 컨트롤할 때 그 인스턴스의 속성 및 메쏘드를 찾아내기 위한 전역적인 검색이 프로토타입 체인 및 개별 인스턴스의 연결관계 전반에 걸쳐 발생한다.
- 이를 지옥의 프로토타입체인 검색이라고 한다.
- 또한 컴파일타임이 존재하지 않으므로 컴파일 검사가 없고 런타임에러만 발생한다.
뭔가 생각해보면 좀 아찔하죠. 동적 언어는 원래부터 모든 선언이나 정의가 런타임에 이뤄지니 당연한 일입니다.
음 갑자기 궁금하죠? 원래부터 as를 짜고 swf로 만들어왔으니 분명히 컴파일 타임이 존재한건데 무슨 소린가 하고. 자바스크립트가 컴파일 없이 소스 상태에서 곧장 실행되다가 런타임에러가 나는 건 어찌보면 런타임언어의 매우 일반적인 형태인겁니다. 단지 플래시는 텍스트 대신 바이너리를 사용하니 플래시의 컴파일러는 바이트번역기 정도지 실제 그 생성된 바이트코드는 런타임언어와 완전히 동일한 수준으로 런타임에 에러 내면서 작동합니다.
그럼 정적 언어 컴파일러와 무슨 차이가 있냐구요? 진짜 컴파일러는 문법 검사같은 걸 하는게 중요한게 아니라 그러한 코드를 분석하여 아까 말씀드렸던 최초 실행 하자마자 초기화할 때 확보해야하는 메모리영역과 거기에 써야할 변경되지 않을 정의 같은걸 준비해두는 일을 합니다. 그리고 컴파일러가 검사할 때도 그러한 초기화 메모리영역이 생성될걸 가정한 상태에서 코드에서 선언한 객체나 여러 메모리 사용이 올바른가 판단하죠.
하지만 런타임언어는 이것 자체가 사실은 불가능한겁니다. 코드가 뭐라고 작성되었던 그게 전부 가능하니까요 ^^ Bitmap의 인스턴스가 explose() 라고 해도 이게 맞는건지 틀린건지 컴파일러가 판단할 방법이 없습니다. 왜냐면 어떤 시점에 어떤 방식으로 그러한 정의가 동적으로 Bitmap클래스에 추가되었을지, 혹은 그 인스턴스 한정으로 추가되었을지 판단할 수 없거든요. 메모리 스냅샷을 실제 그 시점에 당해봐야만 알 수 있다는거죠.
하지만 그 이후 여러가지 생각을 해냈습니다.
- 동적 언어라도 특정 객체를 생성할 때 미리 약속한 표시를 해두면 프로토타입체인을 검색하지 않고 미리 그 체인검색결과를 저장한 캐쉬를 메모리가 같이 잡아줘 사용시 속도를 높일 수 있다.
- 강제로 컴파일타임이란 걸 끼워넣고 컴파일러가 미리 약속한 표시를 검사하게 한다.
1번 항목은 현재 모든 브라우저들이 자바스크립트 처리 속도를 높이는 주요한 수단입니다. 그에 좀 선구자적으로 먼저 적용한게 AVM2입니다. AVM2에서는 1번의 아이디어를 traits라는 캐쉬 인스턴스를 생성해주는 걸로 진행했습니다. 그 결과 정적 언어의 클래스와 유사하게 클래스의 static 항목 수준에서 캐쉬를 잡아주는 Tca(그림참조)와 인스턴스별로 달라야하는 속성만 캐쉬잡은 Ta를 중심으로 재설계했습니다.
즉 Ta캐쉬는 정적 언어의 클래스 모델과 비교해보면 힙메모리에 인스턴스별로 잡아주는 속성구조체에 해당됩니다. 그럼 정적메모리에 잡혀있는 인스턴스가 공유해야할 메쏘드들은? Ca에 잡았죠. 이로서 vtable모델을 사용하는 정적 언어의 클래스가 메모리를 사용하는 것을 완전히 흉내낼 수 있게 되었습니다. 그 결과 아래와 같은 일이 생겼습니다.
- Ca가 인스턴스별로 공유되려면 Ca가 변하지 않는다는 확신이 필요하다. 따라서 코드에서 봉인된 클래스라는 형태로 작성하면 가상머신이 해당 객체의 prototype체인에 접근하는걸 거부하도록 처리한다.
- Tca가 static관련 내용을 하나만, Ca가 인스턴스가 공유할 내용을 하나만 메모리에 잡고 속성값만 Ta를 생성해가는식이므로 기존 정적 언어보다는 효율이 어떨지 몰라도 기존 동적 언어보다는 월등이 메모리 사용량도 줄고 속도도 빨라진다.
해서 결국 AVM2에서 이용한 방법은 기존의 정적 언어 모양을 흉내내어 퍼포먼스를 높이고 동시에 봉인된 클래스 선언 여부에 따라 Pa를 은닉할지 노출할지를 AVM2가 결정 한다는 거죠.
참고로 크롬의 V8엔진이나 웹킷, 파이어폭스 등의 자바스크립트 엔진들도 나름대로의 방법으로 이미 프로토타입 동적 언어의 단점을 극복해버렸고, 엔진마다 어떻게 다른 방식으로 극복했는지는 설명하기 귀찮으니 대략 넘어가겠지만 꼭 말씀드려야할 내용은 이미 AVM2의 위에서 사용한 방법은 구닥다리가 되어서 최근 브라우저의 자바스크립트 엔진 속도가 더 빨라지고 있습니다.
머 1번으로부터 파생되는 아이디어는 결국 캐쉬를 효과적으로 잡겠다는건데 그 부분만 역량을 집중해보면(괜히 정적 언어의 메모리를 흉내내는데 치중하지 않고 ^^) 얼마든지 속도개선할 수 있는 아이디어는 무궁무진하다는거죠. AVM2가 같은 스크립트에 대해 더 빨리 처리한다는 착각은 잠시 잊어주세용 ^^;
클로저
와 클로저를 설명하기 위해 위의 모든 상황을 설명해야 한다는 것과, 더군다나 이걸 대략 남의 링크를 걸어서 보여주면 좋을텐데 그런 글도 매우 희귀해서 제가 쓸 수 밖에 없었다는 사실도 놀랍습니다.
암튼 이제서야 클로저라는 걸 설명할 수 있는데, 먼저 정적 언어 입장에서 스코프(변수의 범위)를 생각해보죠.
문법적으로 어떤 변수가 특정 스코프를 갖게 하면 정적 언어에서는 이 처리를 컴파일 타임에 가상으로 초기화시킨 정적 메모리를 고려하여 처리하게 됩니다. 즉 지역변수인지 보다 오래 존속하는 변수인지 컴파일러가 구문분석을 통해 판단한 후 실제 실행시 확보될 메모리와 실행도중 사용할 메모리에서 어디에 끼워넣을지 정교하게 vtable이나 스택 후보에 올려두는 거죠.
하지만 동적 언어는 완전히 다른 양상입니다.
- 사실 상 모든 변수는 실행도중 생성되므로 언제 파괴할지 판단하기가 어렵습니다.
- 유일한 방법은 그 변수가 속해있는 어떤 범위를 인식하여 만약 범위를 인식할 수 있으면 그 범위를 규정짓는 녀석 내에서 사용하도록 할당하거나 범위를 인식할 수 없거나 애매하면 무조건 전역변수로 잡을 수 밖에 없습니다.
- 게다가 어떤 변수를 사용하려고 하면 변수의 이름을 통해 코드에서 통제하는데 문맥 상 2번의 내용 때문에 전역변수 인지 지역변수인지 판단하기가 쉽지 않습니다.
어쩌면 좋을까요? 동적 언어 설계자들은 이 문제를 동적 언어의 특성을 그대로 사용하여 해결했습니다.
- 실행 도중 어떤 범위라고 인식할 수 있는 대상은 객체의 선언이다.
- 객체의 정의가 이루어질 때, 즉 객체의 정의를 최초 메모리에 담을 때 그 정의를 한 문맥을 판단하여 해당 변수를 객체 지역적으로 처리할 것인지 전역변수에 넣을 것인지를 결정한다.
요컨데 동적언어는 모든 선언도 동적으로 하니까 동적으로 선언하고 있는 구문을 파싱하여 메모리에 정의로 올리는 순간에 판단하겠다 이겁니다.
이러한 객체의 정의가 이루어지는 순간의 주변 환경. 이것이 바로 클로저입니다(드..드디어 클로저가 뭔지 설명할 수 있게 되었습니다 ㅜㅜ)
그럼 간단한 예를 보면서 클로저를 인식해봅시다. 아래에서 정의하는 함수는 함수를 동적으로 만들어내는 함수입니다.
function functionMaker( $trace:String):Function{
return function():void{ trace($trace); };
}
코드를 보시면 이 함수를 호출하면 새로운 함수를 즉시 생성하여 반환하는 걸 보실 수 있습니다. 이제 이 코드를 보면서 클로저를 인식해볼까요.
이 함수생성기가 새로운 함수를 생성하는 시점에서의 환경은 어딜까요?
그 때의 환경은 전역이 아니라 함수생성기 내부입니다. 즉 함수생성기 내부의 상황이 바로 새롭게 생성될 함수가 생성되는 시점에서의 환경인 클로저가 됩니다.
따라서 함수생성기가 인자인 $trace로 받아온 값은 함수생성기 입장에서는 지역변수일 뿐이지만, 새로 생성되는 함수 입장에서는 마치 자기 환경 내에서는 미리 주어져 있는 전역변수 같은 효과를 갖게 됩니다.
즉 아래와 같은 일이 일어나는 거죠.
var test1:Function; //test1을 인자로 보내서 생성한 함수 test1 = functionMaker( 'test1' ); var test2:Function; //test2을 인자로 보내서 생성한 함수 test2 = functionMaker( 'test2' ); test1(); // 'test1' 을 출력한다. test2(); // 'test2'를 출력한다.
분명히 test1이나 test2나 생성기를 통해 받은 함수 자체는 function():void{ trace($trace); } 이것입니다. 하지만 그 함수가 동적으로 생성될 당시의 환경이 다른 겁니다. test1이 생성될 당시는 $trace가 ‘test1′인 상황이었고 test2가 생성될 당시에는 $trace가 ‘test2′인 상황이었던 거죠. 이러한 클로저 덕분에 변수 이름이 중복되어도 런타임 인터프리터를 해깔리지 않을 수 있습니다. 다음의 코드를 볼까요.
function maker():Function{
var str:String;
str = '지역변수';
return function():void{ trace( str ); };
}
var str:String;
str = '전역변수';
var test:Function;
test = maker();
test(); //'지역변수' 출력
왜 지역변수가 출력되었을까요? maker가 만들어지는 시점에서는 str은 전역변수지만 반환할 새 함수가 생성될 때는 maker안의 환경에서 str은 지역변수로 선언했기 때문에 같은 이름인 경우 지역변수가 우선하는 원칙에 따라 환경(클로저)가 str을 지역변수로 인식하게 만들기 때문입니다.
함수 객체
매우 기본적인걸 하나 건너 뛰었군요. 언어에 따라 함수를 객체로 인정해주는 언어가 있고 객체가 아닌 내장된 어떤 기능의 일부로 처리하는 언어가 있습니다. 자바, c++등 많은 전통적인 언어들은 함수를 객체로 처리하지 않습니다. 하지만 대부분의 동적 언어들은 당연하다면 당연하게도 함수라는 것도 동적으로 만들어지는 특정한 유형이므로 객체로 인정합니다. 이러한 언어를 함수객체언어라고도 하고 함수가 일급객체인 언어라고도 하고 람다언어라고도 합니다. 뭔가 셋 중에 하나 정도는 들어보신 적 없나요 ^^; 여러분들이 당연하게 이벤트 리스너에 함수명을 넘기시거나 위의 샘플에서 변수에 함수를 할당하는 짓이 가능한 이유이기도 합니다. 저런 짓은 자바나 c++에선 애시당초 불가능하거든요.
이는 정적 언어에 익숙한 사람들의 눈에는 악의 축으로 보입니다. 왜냐면 나쁘기 때문이 아니라 모르기 때문입니다. Lisp나 algol 등에 익숙하다면 이 람다언어들이 절대 비주류이거나 배제될 기능이 아니라 유연하고 효과적인 표현으로 알고리즘을 혁신시킬 수 있음을 인정할 것입니다만, 대부분 자기가 이해 못하면 화나고 욕하는게 심리라..
암튼 람다 언어적인 측면을 강력하게 부각시킨 루비를 비롯하여 말도 안되는 문법과 생산성의 파이썬은 물론이고 여전히 주류 언어인 php, 자바스크립트, 액션스크립트 등 수 많은 언어가 함수를 객체로 지원하고 있습니다.
클로저와 this
이제 거의 종착역에 가까워지고 있군요. 요거 다음에 메서드 클로저까지만 설명하면 끝입니다.
자바스크립트는 엄밀히 말해 함수클로저만 갖고 있는 셈입니다. 근데 this라는 미묘한 키워드가 존재합니다. this란 환경을 가리키는 전역 참조 변수인데, 환경이란 현재 인터프리터가 해당 코드를 실행시키는 시점에서의 컨텍스트입니다. 제길 컨텍스트도 설명해야 하는군요.
컨텍스트란 의미적으로 라는 뜻..이 아니라 ^^; 현재 실행되고 있는 부분의 변수들이 소속된 메모리 영역을 뜻합니다. 이거 뭔가 아까 설명한 클로저랑 왕창 비슷하지 않나요?
맞습니다. 클로저도 환경이고 컨텍스트도 환경입니다. 단지 클로저는 가리키는 대상이 아니라 함수나 객체를 생성할 때 인터프리터가 인식할 메모리 영역을 뜻한다면, 컨텍스트는 생성할 때가 아니라 실행되는 시점에서의 해당 변수들이 소속된 메모리 영역입니다.
this란 바로 그 컨텍스트를 가리키도록 언제나 인터프리터가 관리해주는 변수의 이름입니다.
여기서부터 this와 클로저의 관계가 생겨납니다.
- 함수를 생성하는 것도 실행시점이다.
- 따라서 현재 실행 중인 영역 즉 컨텍스트가 존재하는 상태에서 함수를 생성한다.
- 따라서 클로저를 인식하는 시점에 this가 가리키는 영역도 달라질 수 있다.
단 this에는 매우 중요한 조건이 달려있습니다. this는 절대로 함수는 될 수 없고 class에 해당되는 객체만 가능하다는 점입니다.
으햐~ 어려운걸 설명하려니 더 어렵군요 ^^; 코드로 봅시다.
function maker():Function{
return function():void{ trace( this.str ); }
}
var str:String;
str = '전역변수';
var testA:Function;
testA = maker();
testA(); //'전역변수' 출력
위의 소스가 전역변수를 출력한 이유는 함수생성시 참조한 this가 바로 전역객체(아마도 stage)이기 때문입니다.
만약 함수를 참조했더라면 속성없음 에러가 나야 정상이었겠죠.
이러한 this는 객체 단위로만 인터프리터가 바꾸기 때문에 객체단위가 변경되어 할당될 때 항상 조심해야합니다.
class testA{
public var str:String = '인스턴스';
public var run:Function;
public function testA(){
}
public function setFunction( $f:Function ):void{
run = $f;
}
public function runFunction():void{
run();
}
}
이러한 클래스가 있다고 해봅시다. 위의 클래스는 run속성에 대해 동적으로 함수를 할당받고 runFunction을 통해 이를 실행하는 기능을 갖고 있습니다(일종의 동적 언어버전의 전략패턴인 셈이죠)
var str:String;
str = '전역';
var test:testA;
test = new testA;
test.setFunction( function():void{ trace( this.str ); } );
test.runFunction(); //인스턴스 출력
왜 인스턴스를 출력했을까요? 위의 소스를 잘 보시면 setFunction의 인자로 넘기기 위해 함수를 동적으로 생성하는 시점에서 this가 누구인지를 이해할 필요가 있습니다. 그 시점에서 this는 test의 메쏘드 인자라는 범위 내에서 실행되었기 때문에 test를 this로 인식하게 됩니다. 따라서 this.str이 test.str을 가리키게 되어 전역변수의 str을 사용하지 않고 인스턴스의 것을 사용하게 된거죠. 만약 전역의 것을 사용하고 싶다면 별도의 참조를 만들어 보내주면 됩니다.
var str:String;
str = '전역';
var THIS:*;
THIS = this;
var test:testA;
test = new testA;
test.setFunction( function():void{ trace( THIS.str ); } );
test.runFunction(); //전역 출력
THIS라는 변수에 현재 컨텍스트인 전역자체를 참조로 잡아준 상태에서 인자를 생성하려고하면 THIS라는 참조변수를 인식할 수 있는 곳이 생성될 함수 입장에선 전역클로저밖에 없으므로 전역에서 선언된 str을 참조하게 되는 것입니다.
메서드 클로저
메서드 클로저는 앞에서 설명했던 AVM2가 동적 언어의 성능 개선을 위해 메모리의 구조를 정적 언어의 객체가 메모리를 쓰는 걸 사용하는걸 모방하는 모델로 가다가 나온 자연스런 부산물입니다. 공식문서에서는 다음과 같이 설명하고 있습니다.
메서드 클로저
ActionScript 3.0에서는 메서드 클로저를 원본 객체 인스턴스에 바인딩할 수 있습니다. 이 기능은 이벤트 처리에 유용합니다. ActionScript 2.0에서는 메서드 클로저가 어떠한 객체 인스턴스에서 추출되었는지 기억하지 못했기 때문에 메서드 클로저가 호출되면 예기치 못한 비헤이비어가 발생했습니다. 이 경우 일반적으로 mx.utils.Delegate 클래스를 사용하여 문제를 해결했지만 이제 더 이상 mx.utils.Delegate 클래스를 사용할 필요가 없습니다.
설명이 어렵지만 앞에 this와 클로저의 관계를 명확히 이해하셨다면 이것도 어렵지 않습니다.
기본적으로 this는 함수가 생성되는 당시의 컨텍스트에 영향을 받는게 정상입니다. 하지만 class{} 구문 내에서 선언된 함수는 인터프리터가 언제나 this라는 참조를 new통해 생성된 인스턴스로 고정시켜준다는 의미입니다. 이 설명은 정확하지만 어렵기 때문에 부가 설명을 더 해야겠군요.
class{ 변수, 메쏘드…} 등을 선언하신 뒤 new class 하시는 순간이 바로 그 안에 선언하셨던 함수와 변수와 객체들이 만들어지는 순간입니다. 원래대로라면 그 만들어지는 순간의 컨텍스트와 클로저를 기반으로 함수가 생성되어야하지만, class{}라는 구문 내에서 선언한 함수와 변수 등등은 전부 만들어지는 순간의 인스턴스를 this로 강제 참조하게 하겠다는 것입니다. 이게 가능한 이유는 도입부에 설명드렸던 Ca와 Ta 캐쉬를 잡아두었기 때문에 인터프리터가 영역으로 인식할 수 있는 구역이 생겨나기 때문입니다.
즉 여러분은 마지 자바의 class선언처럼 as3에서도 class내부에서 메쏘드를 선언했으니 그 메쏘드 내부에 있는 this는 당연히 인스턴스 그 자신이지라고 생각해오셨겠지만 사실은 전혀 아닙니다.
원래 class 내부에 선언한 메쏘드도 사실은 함수인거고 함수란 동적 언어에서는 클로저와 컨텍스트 기반으로 생성되는 게 당연합니다. 하지만 avm2는 class선언에 대해 추가적인 기능을 처리해주어 메쏘드로 선언된 함수의 경우 this가 언제나 Ta와 Ca를 가리키도록 처리해준다는 것입니다. 그래서
기본 언어 기능
기본 언어에는 명령문, 표현식, 조건문, 반복문 및 유형과 같은 프로그래밍 언어의 기본적인 구성 단위가 정의되어 있습니다. ActionScript 3.0에는 개발 작업의 속도를 향상시킬 수 있는 여러 가지 새로운 기능이 포함되어 있습니다.
라는 타이틀 밑에 메서드 클로저가 자랑스럽게 있는거죠. 사실 이러한 메서드 클로저 기능을 avm2가 지원해주지 않는다면 이벤트 리스너 모델은 근본적으로 성립하지 않습니다.
spriteA.addEventListener( MouseEvent.CLICK, text1.onClick );
이런식으로 할당하면 우리가 지정한 onClick리스너 내부의 변수나 this는 전부 text1인스턴스 기반으로 움직이길 원할텐데 원래 클로저와 this만 지원되는 환경이라면 얄 짤 없이 spriteA 기준으로 움직이게 되겠죠.
AS2의 Delegation
먼가 as2의 delegation 새록새록 하시죵? 그거 참 프로토타입 언어에서는 반드시 필요한 거랍니다. 왜냐면 as3처럼 인스턴스를 자동으로 class의 메쏘드에게 this로 매핑하는 기능이 없으니까 유저가 수동으로 그때 그때 해줘야하는 거죠. this의 후기 바인딩이랄까..
이러한 기법은 자바스크립트에서도 비슷비슷하게 사용합니다. 프로토타입이나 jquery등도 암묵적으로 delegation을 해서 지정한 리스너의 this가 자신을 가리키리 수 있게 해주죠. 머 소스 뜯어보면 as2의 클래스나 자바스크립트의 소스나 개 허접한 eval로 떡칠해서 만들어둔거긴 합니다만 쨌든 왜 delegation이 as2나 자바스크립트에서 필요하고 as3에서 필요없는지를 완전히 이해가 가셨을거라 생각합니다(정말?)
callee와 caller
하나의 함수를 기준으로 그 함수를 호출한 측은 caller 가 되고 호출을 당한 함수 자신은 callee가 됩니다. 특히 callee가 필요한 이유는 함수 내부에서 this를 사용하면 컨텍스트를 가리키지 함수 자신을 가리킬 수 없기 때문입니다. 이는 as2와 as3사이에 미묘한 문제를 만들어 냅니다.
as2나 자바스크립트에서는 this가 생성 당시의 컨텍스트를 가리키기 때문에 this에 의존한 격리 로직을 짤 수 없습니다. 이를 실현하려면 세 가지 방법이 있습니다.
- 인자로 호출하는 대상을 참조변수로 잡아 보내는 주는 방법입니다. 하지만 코드가 지저분해지기 때문에 ( test(this,진짜인자..) ) 잘 사용하지 않습니다.
- 두번째 방법은 delegation을 이용하여 this를 강제적으로 재설정하여 함수 생성시에 반영해주는 방법입니다. 이 방법은 결국 내부적으로 보면 this를 원하는 객체로 설정한 뒤 함수를 다시 생성하여 할당하는 방식입니다. 코드는 깔끔해지지만 지저분한건 마찬가지입니다.
- 마지막 방법은 함수 내부에서 this 대신 arguments.caller를 통해 생성당시의 컨텍스트가 아니라 실행 중 함수를 호출한 컨텍스트를 조사하여 그로부터 원하는 객체를 역추적하는 방식입니다. 구문이 생소하고 복잡한 면이 있지만 ECMA오리지널 구조에선 가장 합리적인 방법이기도 합니다.
그래서 함수가 누가 함수를 호출한건지를 인식할 수 있도록 arguments.caller가 제공됩니다. 하지만 이는 알고리즘의 의존관계에서 하향식의 단방향 참조가 아닌 양방향 참조를 만들어내게 되어 해석할 수 없는 스파게티코드를 만들어내는 이유가 되기도 합니다. 바람직한 알고리즘은 부모는 자식을 알고 자식은 부모를 모르게 하고, 컨테이너는 엘레멘트를 알지만 엘레멘트는 컨테이너를 모르게 함으로서 단계별로 통제가 가능하도록 짜는게 일반적입니다. 따라서 as3에서는 클래스 메서드에게 메서드 클로저 기능을 AVM2수준에서 부여하게 되어 구지 callee기준의 로직을 짤 필요가 없다고 판단했고(어도비가 맘대로) 곧장 이 기능을 막아버렸습니다(어도비가 맘대로) 따라서 as3에서는 함수 내부에서 인자로 받지 않으면 누가 함수를 호출한 건지를 알아낼 수 없습니다.
어쨌든 함수 내부에서의 this는 컨텍스트는 객체이므로 반대로 현재 실행 중인 함수 자신을 가리킬때 this를 사용할 수는 없습니다. 실행 중인 함수 자신을 가리키기 위해서는 arguments.callee 를 사용합니다.
이에 관해서는 공식문서에 더욱 자세하게 설명되어있습니다.
http://help.adobe.com/ko_KR/AS3LCR/Flash_10.0/arguments.html#callee
결론
as3는 동적 언어에 성능개선을 위해 요것 저것 편법을 섞어 넣은 형태로 정적 언어의 클래스를 가상적으로 흉내내 줍니다. 따라서 분명히 언어의 정적인 측면과 동적인 측면이 동시에 공존하고 있습니다.
동적 언어는 매우 유연합니다. 예를 들어 위에서 샘플로 나왔던 동적 함수 할당으로 구현하는 전략패턴은(test.runFunction();) 자바나 c++에서는 그 메쏘드 하나당 클래스 하나를 각각 만들어서 정적으로 선언 해둬야만 쓸 수 있을 정도로 딱딱하고 코드 양이 많아집니다(이는 곧 개발자라는 인간이 귀찮아서 변화에 소극적이거나 개선에 방어적인 자세를 취하게 합니다)
그러한 엄격한 특성은 대규모 개발 시 각 코드 수준을 격리할 수 있어 안전하게 해주는 효과가 있기 때문에 반대로 그러한 안심(?)을 할 수 없는 동적 언어의 특성들은 나쁘다라고 규정짓는 무리들이 많습니다.
하지만 거대한 프로그램도 부분적으로는 격리 되어 있는 층이 있고 그 격리된 구역 내에선 효과적이고 유연하며 가볍게 사용할 일도 있는 법입니다. 동적 언어의 특성은 그러한 생산성, 유연성을 만족 시킵니다.
따라서 나쁘다 좋다 이전에 양쪽의 특성을 확실히 이해하고 써야할 곳에는 쓰는 것이 좋다( 왜 안써 바보냐 ) 라는게 제 개인적인 결론입니다.



오늘도 좋은글 감사합니다.