아래 언급한 내용이 정확한 건지는 나도 확신이 들지 않는 부분도 있다. 동적,정적 언어에 대한 교양을 쌓아본적도 없고 단순히 블로그와 여러 자료를 읽어보고 내린 나의 결론일 뿐이다. 이 생각을 이해하려면 먼저 ActionScript 3.0에 대한 언어적 구조를 이해할 필요가 있으므로 아래글을 꼭 읽어보길 바라며 잘못된 내용이 있으면 댓글로 언제든지 지적해주었으면 한다.


동적언어와 정적언어의 차이점은 실행시점에 메모리 관리와 관계있다. 정적 언어에서는 실행시 메모리상에 정의된 클래스,함수등을 수정될 수 없지만 동적언어는 언제든지 바꿀 수 있다.  

ActionScript 3.0은 JavaScript처럼 prototype 체인을 기반으로 하는 동적언어이다. 하지만 개발자가 느끼기에 정적언어처럼 사용을 강제하는 구조로 만들어져 있다. 즉, 겉으로 보기에는 정적언어인 Java나 C++처럼 Class, 메소드 기반의 개발을 유도한다. 

동적언어는 정적언어에 비해 속도,메모리 관점에서는 불리한 면이 많으나 유연성이 좋기 때문에 그에 따른 장단이 있다. 

ActionScript 3.0은 동적언어인데 왜 굳이 정적언어 흉내를 내려고 한 것일까? 여러가지 이유가 있겠지만 언어의 패러다임을 맞춰 Java, C++개발자들과 같이 객체지향개발에 익숙한 사람들도 쉽게 접근하기 위함이 있겠고 또한 속도개선을 위한 메커니즘을 만들어내기 위한 것이 아닌가 생각된다. 

Class기반의 객체지향프로그래밍은 Java,C++개발자라면 익숙하기 때문에 접근하기 좋다. ActionScript 2.0때 부터 Class나 extends, private, public 과 같은 키워드를 도입해서 객체지향적 프로그래밍을 강제했다. 내부적으로는 prototype 체인구조로 되어 있지만 문법적으로 이들 키워드를 이용해 개발하면 컴파일시 자동으로 prototype 구조를 변형되는 것으로 판단한다. 하지만 이런식의 단순 변화는 개발의 접근성이 좋아진 반면 속도적인 이득은 없다. 

ActionScript 2.0까지 AVM1(ActionScript Virtual Machine 1) 기반하에 동작할 때는 함수 및 속성에 접근하기 위해 prototype 체인을 검색하면서 실행했기 때문에 속도적으로 너무 느리다. 하지만 ActionScript 3.0부터는 새로운 AVM2 기반에서 동작하게 되어 느린 prototype 체인 검색을 많은 부분 개선하게 되었다. 즉, prototype대신 traits라는 일종의 캐쉬기능이 도입되었다. 이 기능의 도입으로 속도와 메모리가 상당부분 개선이 되었다. 

AVM2에서 A클래스의 객체구조(출처:어도비)




분명 AVM2에서 traits 도입은 ActionScript 3.0의 매력도를 굉장히 증가를 시키게 되었다.  이렇게 하여 접근 방법이 빨라지고 클래스에 정의된 메소드들의 this는 항상 클래스 객체를 가리킬 수 있게 된것이다. 이런 개념때문에 우리는 sprite1.addEventListener(myEvent, display1.hnFunc)와 같이 아무 문제없이 이벤트 핸들링을 할 수 있게 된것이다. display1.hnFunc 안에서 this를 사용하면 이것은 sprite1이 아닌 display1을 가리키게 된다. hnFunc는 display1 객체를 정의한 클래스 내에 정의된 메소드이기 때문이다. 만약 traits 개념이 없다면 ActionScript는 여전히 prototype 기반이기 때문에 의도하는 this의 행방이 애매모호해져서 ActionScript 2.0때 처럼 delegation(대리자) 개념을 이용해야하는 불편함을 가지게 된다. 

메소드 클로저는 Class내의 메소드 일때만 유효하다. 만약 다음과 같은 경우 this는 의도하는 this가 아닌 경우가 생긴다. 예제로 아래 ActionScript 3.0코드를 보자.

class MyClass {
	function MyClass() {
	}
	public  function method():void {
		trace(this);  //[object MyClass]
		hnRun(this);  //[object global]:[object MyClass]
		function hnRun($object:*):void {
			trace(this +":"+$object); 
		}		
	}
}

위에서 언급한 대로 위 클래스의 method()는 클래스의 메소드이므로 메소드 클로저 개념이 도입되어 method() 내에서 this는 그 클래스의 객체 자신을 가리킨다. 만약 prototype구조로만 정의된다면 당연히 this는 global을 가리켜야 한다. 이러한 이유로 ActionScript 3.0이 Java나 C++처럼 this의 의미가 같아지게 되어 헷갈리지 않고 사용할 수 있게 된다. 

ActionScript 3.0은 분명 동적언어이므로 method()내에 hnRun()과 같은 형태로 메소드 안에 또 함수를 정의할 수 있다. 이 함수는 내부함수라 지칭된다. 이 경우에도 this는 MyClass를 가리켰으면 좋겠지만 실제로 출력결과를 보면 global이 나온다. 즉, 내부함수는 prototype 체인기반의 함수 특성을 여전히 가지고 있다는 것을 의미한다. 

ActionScript 3.0는 이런 내부함수를 다룰 수 있는 여지는 남겨두었지만 잘 알지 못하고 사용하면 오히려 역효과를 줄 수 있기 때문에 신경을 써야한다. 왜냐하면 this는 앞선 이유 때문이고, 메모리 부분의 경우 내부함수는 그것을 감싼 메소드가 호출될때마다 새롭게 생성되기 때문에 참조를 누적해서 사용하면 메모리누수를 일으킬 수 있다.(실제로 method()를 두번이상 호출해보고 hnRun의 메모리값을 디버깅을 통해 보면 번지수가 다르다는 것을 볼 수 있다.) 이러한 특징과 문제가 되는 사항을 잘 알면 오히려 실보다 득이 많다. 왜냐하면 내부함수를 사용하면 전체코드의 가독성을 향상시켜주는 좋은 도구이기 때문이다. 

위 ActionScript 3.0 코드를 아래처럼 조금 변경해보자. 

class MyClass {
	function MyClass() {
	}
	public  function method():void {
		trace(this);  //[object MyClass]
		hnRun(this);  //[object global]:[object MyClass]
		var a:Object;
		a = {method1:hnRun};
		a.method1(this);  //[object Object]:[object MyClass]		
		function hnRun($object:*):void {
			trace(this +":"+$object); 
		}		
	}
}

a.method1(this)를 실행하면 이번에는 this가 아닌 Object가 된다. 즉 a를 가리킨다. Object내에 정의된 속성, 메소드는 traits 캐쉬에 등록되고 그 안에서 사용된 this는 traits를 가리키도록 강제되기 때문이다. 순수 prototype 기반이라면 당연히 global이 나와야 한다. 

this에 대해서 헷갈릴 수 있는 부분 한가지는 a.method( function():void{trace(this);}); 처럼 사용할 때이다. 이때 this는 a객체에 정의된 method()에서 실행되므로 this는 a가 된다. 

이처럼 동적언어인 ActionScript 3.0에서 this에 대해서 잘 이해하고 써야한다. 안그러면 내부함수를 사용하게 되는우 원하는 클래스의 인스턴스를 참조하는게 아닌 엉뚱한 global을 참고할 수 있게 되고 이런건 컴파일 타임에 에러를 발생하지 않고 왜 그런지 이유 조차 모를 수밖에 없기 때문이다.

이런 개념을 이해하고 개발하는 것은 생산성을 극대화 시켜준다는데는 경험상 확인되었다.


Prototype 디자인 패턴GoF의 23가지 디자인 패턴중 생성관점에서 바라보는 패턴중에 하나이다. 생성관점에서 바라보는 이유는 복사를 해서 새로운 객체(인스턴스,instance)를 만든다는 개념을 가지고 있기 때문이다.

Prototype의 용어가 “원형“이라는 뜻을 알아둘 필요가 있다.

Prototype 디자인 패턴은 “원형”이 되는 클래스의 객체와 똑같은 것을 복제해서 사용할 수 있게 하는 것이다.

 

왜 Prototype 디자인 패턴을 사용하는가?

일반적으로 Something 클래스를 이용하여 객체를 만들때 new Something()과 같이 new 연산자를 이용해서 생성하게 된다. 이 경우 10개 만들면 new연산자를 10개 써야한다. 하지만 Prototype 디자인 패턴을 이용하면 new 연산자를 사용하지 않는다. 대신 복사(복제)하는 방법을 이용한다. 이 패턴은 어떤 클래스의 객체를 생성하는 것이 자원과 시간을 많이 잡아먹거나 복잡한 경우에 유용하게 쓰일 수 있다. 또한 객체를 생성하는 부분이 필요한 상황일때, 생성하려는 객체의 종류정보가 컴파일 시간이 아닌 실행시간에 정해지는 경우에도 필요하다.

Yuki Hiroshi가 지은 “Java 언어로 배우는 디자인 패턴 입문”에서 다음 경우에는 기존의 객체를 가지고 복사(복제,clone생성)해서 사용하는 경우가 유용할 수 있다고 한다.

 

1. 취급하는 객체의 종류가 너무 많아 다수의 소스파일을 작성하는 일이 빗어질때

마음만 먹으면 얼마든지 많은 객체를 다루게 될 수 있다. 이러한 경우 복사해서 사용하는 경우가 유용할 수 있다.

 

2. 클래스로부터 인스턴스를 생성하기가 복잡할 때

가령, 마우스를 사용해서 조작하는 도형 에디터와 같은 애플리케이션에서 도형을 나타내는 객체와 똑같은 것을 만드는 경우 클래스를 이용해 new 연산자로 객체를 생성하는 것보다 인스턴스를 복사해서 만드는 방법이 더 간단할 수 있다.

 

3. 프레임워크와 생성할 인스턴스를 분리하고 싶을 때

프레임워크의 기본은 클래스간에 결합도를 줄여주고 확장성을 높여주는 것이 중요하다. 이것은 프레임워크가 특정 클래스에 의존적이지 않게 한다는 의미이다.  가령, 진열장이 프레임워크라고 하자. 진열장안에는 각종 물건이 있다. 진열장에는 어떤 물건이든 들어올 수 있다.  진열장에 있는 물건은 구입자가 그 물건을 사기위한 원본이 된다. 이 원본에는 이름이 있고 구입자가 이 물건을 구입할때 진열장에 있는 물건을 바로 구입하는 것이 아니라 물건과 똑같은 물건이 구입자 손으로 넘어온다. 여기서 중요하게 짚고 넘어갈 것은 진열장(프레임워크)는 어떤 물건(객체)이 들어있는지 알 수 없으나 그 물건이 선택되면 그와 똑같은 물건을 반환해준다.

 

Prototype 디자인 패턴에서 객체를 복사해서 사용하는 이유에 대해 3가지를 언급했지만 감이 잘 안올것이다. 사실 제대로 이해하기 위해서는 직접 코딩해보고 유용함을 몸소 깨달아야한다. 본인도 new연산자를 사용하는 것과 복사해서 사용하는 것에 대한 장단점을 가려내는데 많이 힘들었지만 하다보니 객체를 복사해서 사용하는 것이 더욱 깔끔한 설계를 이끌어 내는데 도움이 되는 것을 느낄 수 있었다.

 

깊은 복사와 얕은 복사

여기서 한가지 짚고 넘어갈 것은 “복사”라는 용어이다. 복사에는 “얕은 복사(Shallow Copy)”와 “깊은 복사(Deep Copy)”가 있다. 얕은 복사는 일종의 참조이다.  ActionScript 3.0의 복사는 대부분 얕은 복사이다.

 

 

상단 코드의 결과는 어떻게 나올까?

 

1,2,4

1,2,3

 

이라고 생각했다면 아직도 당신은 ActionScript 3.0이 깊은 복사로 되는줄로 착각하고 있는 것이다.

 

답은

 

1,2,4

1,2,4

 

이다.

 

앞서 설명했지만 일반적인 복사의 형태는 참조복사의 형태이다. 즉, 얕은 복사이다. 참조한다는 것은 메모리를 공유하고 있다는 것을 뜻한다. 위 코드에서 단순히 var b:Array = a를 통해 b에 a를 복사했기 때문에 a와 b가 전혀 다른 객체를 참고하고 있다는 것을 뜻하지 않는다. 실제로는 a에서 참조하고 있는 데이타를 b도 참고한다는 것을 의미한다. 그러므로 a의 요소가 수정되면 b의 요소도 바뀌는 것이다.

 

 

이 글에서 다뤄지는 Prototype 디자인 패턴의 복사는 깊은 복사이다.

 

즉, 앞선 코드에서 a와 b가 있다고 할 때 a와 b의 데이터의 값은 완전히 같지만 서로 다른 메모리의 주소를 참고하도록 한다는 것이다. 일반적으로 일란성 쌍둥이를 생각하면 된다. 모든 유전자정보가 같은 일란성 쌍둥이지만 엄연히 완벽히 다른 2개의 객체인 것이다. Prototype 디자인 패턴에서 객체를 복사한다는 의미는 이러한 의미에서 깊은 복사를 뜻한다.

 

위 코드에서 깊은 복사의 형태로 바꿔지기 위해서는 다음과 같은 작업이 필요하다.

 

createClone() 메소드를 통해서 위 코드를 실행하면

결과는 아래와 같다.

 

1,2,4

1,2,3

 

깊은 복사가 제대로 실행된 것을 볼 수 있다.

 

createClone() 메소드에 사용된 ByteArray를 통해 객체를 생성하는 것을 유심히 살펴보자.

 

Java의 경우 Cloneable인터페이스와 java.lang.Object에 clone()이 정의되어 있어서 객체복사(깊은복사)를 매우 쉽게 할 수 있다. 하지만 ActionScript 3.0의 경우 그런 메소드가 존재하지 않는다. 그래서 이와 같이 조금 어려운 객체 복사를 해야한다. 이 또한 클래스의 객체를 완벽하게 복사하기 위해서 쓰기에는 완벽하지 않다. 아래처럼 조금 더 수정될 필요가 있다.

 

 

위 코드는 createClone() 메소드를 통해 클래스 객체도 완벽히 복사할 수 있도록 만들었다.

 

메소드내 추가된 코드는 객체을 복사할때 원본 객체의 클래스 유형을 유지하기 위한 부분이다. 기존에 클래스가 등록된 경우에는 getClassByAlias()로 Class가 반환되지만 등록되어 있지 않은 경우 ReferenceError를 발생시킨다. 에러 발생시 registerClassAlias()을 이용해 원본객체의 클래스 유형을 유지시킨다. 이 코드를 넣지 않으면 이 함수를 통해 만들어진 복사된 객체가 Object가 되어 원하는 Class의 원형을 갖추지 못하게 된다. 이 말은 registerClassAlias()를 사용하지 않으면 var a:MyClass = createClone( b ) as MyClass로 사용할 수 없고 var a:Object = createClone( b ) 로 밖에 사용할 수 밖에 없다는 것을 뜻한다. 아래 코드는 이에 대해서 이해하기 위한 예제이다.

 

 

위 코드에서 registerClassAlias()를 빼면 trace() 결과가 false로 나온다. 이와 관련된 자세한 내용은 “getClassByAlias()와 registerClassAlias() 설명서“를 참고하길 바란다.

 

Prototype 디자인 패턴을 적용한 UML과 ActionScript 3.0 코드

 

위 그림은 Prototype의 기본 디자인 패턴에 대한 클래스 다이어그램이다. 사용자인 Client 클래스는 인터페이스 Prototype을 구체적으로 구현한 ConcretePrototype1, ConcretePrototype2…. 를 사용한다. Client 클래스는 ConcretePrototype1, ConcretePrototype2의 존재를 알필요 없이 clone()만 알고 있으면 된다. 이 clone() 메소드가 바로 깊은 복사를 통해서 자신을 복사해주는 역할을 하는 것이다.

 

 

위 다이어그램은 이제부터 선보일 Prototype 디자인패턴을 적용시킨 클래스들의 관계를 설명하고 있다. 아래 이미지는 다이어그램에 따라서 만들어진 ActionScript 3.0 프로젝트의 Package구조를 보여주고 있다.

 

 

간단히 설명하자면

framework 패키지와 car 패키지로 구분되어 있는데 이렇게 구분한 것은 Prototype 디자인패턴이 적용된 framework 패키지 안에 클래스를 이용하여 계속 확장할 수 있음을 뜻한다. 가령, car 뿐아니라 boat, television, radio 어떤 것이든 상관 없다는 것이다.

Prototype의 핵심 인터페이스인 IPrototype을 확장한 Product를 또 Engine과 Wheel 클래스로 확장했다. 또한 IPrototypeClient 인터페이스를 확장해 ProductManager를 framework에 만들고 이것을 확장한 CarPartManager 클래스를 볼 수 있다.  CarPartManager 클래스에 Engine, Wheel의 객체를 등록하면 CarPartManager의 create()메소드를 통해 Engine과 Wheel의 객체의 새로운 객체를 복제할 수 있다.

이해를 돕기 위해 사용예를 보자.

 

CarPartManager의 객체에 2개의 Wheel 객체와 2개의 Engine 객체를 등록하고 create() 메소드를 통해 새로운 객체를 생성하고 있다. 결과는 다음과 같이 나온다.

 

This is a wheel A
This is a wheel B
This is a engine 1
This is a engine 2

 

중요하게 체크해야할 사실은 등록할때 외에는 Wheel과 Engine의 객체 복사를 new를 통해하지 않는다는 것이다. 단지 처음에 Wheel과 Engine를 CarPartManager에 register()메소드의 첫번째 인자값만 가지고 등록된 객체를 복사해낸다. Prototype 디자인 패턴은 이렇게 new를 사용하지 않고 등록시 사용한 별칭(wheel1, wheel2, engine1, engine2)만 가지고 똑같은 객체를 복사생성하기 때문에 new를 통한 생성의 복잡함(설정이 매우 많은 경우)이 있는 경우 크게 도움이 될 수  있다.

각각의 클래스 코드를 살펴보자.

 

먼저 인터페이스이다.

 

IPrototype은 매우 단순하다. clone() 메소드를 통해서 복사만 가능하도록 구현할 수 있으면 된다.

 

 

IPrototypeClinet는 복사가 가능한 인터페이스(IPrototype)를 가지는 객체를 특별히 지정된 이름으로 등록할 수 있고 등록된 객체로 부터 지정된 이름으로 생성할 수 있도록 하는 것이 목적이다.

 

위 2개의 핵심 인터페이스를 구현한 프레임워크내 2개의 핵심 클래스를 살펴보자.

 

위 클래스는 IPrototype을 구현한 Product클래스이다. 앞서 소개했던 CloneMachine클래스를 확장해서 만들어졌다. clone()메소드를 구현한 부분을 살펴보면 CloneMachine 클래스에 정의된 createClone()메소드를 이용하여 자기 자신을 복제하여 넘겨주는 것을 확인할 수 있다. Product클래스는 일종의 생산품의 부모라고 생각하면 된다. 이 생산품을 설명해주는 description get/set 메소드도 추가되어 있는 것을 확인하자. 이 클래스는 단독으로 쓰이지 않으며 반드시 확장해서 사용한다. 일종의 추상클래스라 생각하면 된다. ActionScript 3.0에서는 추상클래스로 지정하기 위해 Java의 abstact와 같은 키워드가 없으므로 별도의 설정은 하지 않았다.

 

 

위 클래스는 IPrototypeClient 인터페이스를 확장한 ProductManager 클래스이다. 이 클래스는 등록(register)와 복제(create)를 구현하고 있다. 코드는 그리 어렵지 않다. create()메소드에 등록된 객체를 찾아 clone()메소드로 복사하는 것을 꼭 확인하기 바란다.

 

프레임워크 부분은 모두 설명했다. 이제 활용 예제로 만든 car 패키지 부분을 살펴보자.

 

IPrototype을 구현한 Product를 확장해 Wheel과 Engine 클래스를 만들었다. 자동차 부속품인 바퀴휠과 엔진을 의미한다. 단순히 생성자만 새로 만들었을뿐 별 다른 구현은 하지 않았지만 필요하다면 다른 구현을 해도 무방하다.

 

 

IPrototypeClinet를 구현한 ProductManager를 확장해 CarPartManager를 만들었다. 더이상 다른 정의를 하지 않았고 ProductManager가 구현한 것을 그대로 쓰고 있지만 원하는데로 확장해서 사용해도 문제없다. 의미적으로는 자동차 부속품을 관리하는 관리자 클래스라고 생각하면 되겠다.

 

다시 한번 이 클래스를 사용하는 예제를 보자.

 

 

이제 어렵지 않게 해석할 수 있을것이다.

 

이것만은 꼭 기억하자. Prototype 디자인 패턴은 객체를 원하는데로 복사(깊은복사)해서 사용할 수 있게 만든 패턴이다. Prototype 패턴을 이용해 Flex/AIR/Flash/ActionScript 3.0 프로그램 설계에 도움이 되길 바란다.

 

참고자료

ActionScript 3.0 Prototype Design Pattern: A Minimalist Example

ActionScript 3.0 Clone: A Prelude to the Prototype Design Pattern

getClassByAlias()와 registerClassAlias() 설명서

소스

-> 무료 UML 툴인 StarUML을 다운로드 받아 보실 수 있습니다.

-> Flex Builder 3 에서 Import 해 사용하세요.

 

추가내용

Flex에 관련된 컴포넌트는 여기서 소개된  Prototype 방식으로 clone()을 생성하면 에러가 생성될 가능성이 많다. 특히 UIComponent 계열은 Flex SDK 프레임워크 종속적인 클래스이다. 이 것으로 만들어진 객체를 clone()으로 깊은 복사에 성공하더라도 프레임워크 자체에서 UIComponent 객체들을 관리하기 때문에 단순한 clone() 메소드로는 Flex 자체에서 동작하도록 만들 수 없다. 그러므로 프레임워크와는 상관없는 Sprite, Shape등을 이용해서 만들도록 하자.

추가내용 2 (2009.12.9)

히카님과 대화하면서 새롭게 안 사실입니다. 위에서 객체 복사를 위한 부분에 대해서 실제로 메서드, 변수정의는 모두 되지만 ByteArray, Vector, Array, Dictionary, Object 값외에 다른 값의 복사는 되지 않는다고 합니다. 그래서 결국 이 방법으로 복사하는 것은 잘못된 것이네요. 참고하세요.

+ Recent posts