아래와 같은 에러를 만나 본적이 있으신가요?


-[CALayer release]: message sent to deallocated instance


개발자가 만든 코드가 아니라서 디버깅해서는 찾기 힘든 경우입니다. 이런 경우에는 일단 예상되는 부분을 주석처리 해서 저 메시지가 안나오게 한다음 문제되는 부분을 분석해서 찾는 것이 속이 편한듯 싶습니다. 아니면 꼼꼼히 코드를 보면서 누수부분을 찾아야겠지요. 아직 Objective-C가 익숙하지 않아서 인지 찾는데 좀 시간이 걸리더군요.


제가 저 에러를 만났을때 문제가 되었던 것은 바로 적절한 retain을 해주지 않아서 였습니다.


//1 에러발생 : -[CALayer release]: message sent to deallocated instance

//단, dealloc에서 [_btnGlobal release];_btnGlobal=nil;을 호출해준다.

_btnGlobal = [UIButton buttonWithType:UIButtonTypeCustom];

[self addSubview:_btnGlobal];


//2 문제없음

//단, dealloc에서 [_btnGlobal release];_btnGlobal=nil;을 호출해준다.

_btnGlobal = [UIButton buttonWithType:UIButtonTypeCustom];

[_btnGlobal retain];

[self addSubview:_btnGlobal];


//3 문제없음

UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];

[self addSubview:button];


//4 문제있음 : Stack Over가 생김 

UIButton *button = [[UIButton buttonWithType:UIButtonTypeCustom]autorelease];

[self addSubview:button];


//5 문제있음 : 4번과 현상 같음 

UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];

[self addSubview:button];

[button release];

//6 문제없음

UIButton *button = [[UIButton alloc]init];

[self addSubview:button];

[button release];

//7 문제없음

UIButton *button = [[[UIButton alloc]init]autorelease];

[self addSubview:button];

//8 메모리 leak발생

UIButton *button = [[UIButton alloc]init];

[self addSubview:button];


기본적으로 objective-c에서는 retain수와 release 수가 항상 같아야 메모리 문제가 발생하지 않지요. retain수>release수 가 되면 메모리 leak가 발생하고 반대로 retain수 < release 수가 되면 중간에 뻗어 버립니다. 그래서 오히려 전자가 더 찾기 힘들죠.


저한테 닥친 문제는 1번의 경우였습니다. 일단 _btnGlobal은 전역변수입니다. 여기에 @property, @synthesize와 같은 설정은 안했습니다. 그리고 dealloc함수에서 release시키죠. 1번에서 문제는 객체 생성시 new, init, alloc을 사용하면 기본적으로 autorelease되지 않아서 수동으로 release해야하며 반대로 new,init,alloc으로 생성하지 않는 경우에는 autorelease처리되어 release를 하지 않아도 된다는 점을 간과했습니다.  1번의 경우는 자동으로 release되었으니 처음에는 문제없이 동작하지만 dealloc함수가 호출시에 [_btnGlobal release]를 호출하기 때문에 release를 한번더 한 경우가 되어 에러를 발생시킵니다.  


그래서 1번 문제는 2번처럼 해주면 문제가 발생하지 않습니다. 


3번의 경우 1번의 경우와 같은 것 같지만 button 변수가 함수내부에 있으므로 문제가 없습니다. dealloc함수에서 release를 호출할 일도 없고 또한 new, alloc, init함수등으로 객체를 생성한 것이 아니기 때문에 자동 release되므로 문제 없습니다.


반면 4의 경우처럼 autorelease를 붙이거나 5번의 경우처럼 release를 했다면 스택오버플로우(?)와 같은 현상이 발생합니다.


6, 7번의 경우는 alloc, init, new함수를 썼으므로 release나 autorelease를 사용했습니다.. 이 경우에는 문제없습니다.


하지만 8번의 경우 retain되었고 addSubview가 되는 순간 두번째 retain이 됩니다.  내부적으로 addSubView에대한 것은 자동 release되겠으나 dealloc함수등에서 release시키지 않으므로 메모리 leak이 발생합니다. 이는 Instruments를 이용해서 찾을 수 있을겁니다.


retain과 release를 적절히 사용하는게 무엇보다 중요합니다. 개발하면서 항상 체크하세요.


이 개념을 잡는데 가장 도움이 되었던 글은 문씨님 글인것 같습니다.

http://cafe.naver.com/mcbugi/71504 

글쓴이 : 지돌스타(http://blog.jidolstar.com/730)

아래 언급한 내용이 정확한 건지는 나도 확신이 들지 않는 부분도 있다. 동적,정적 언어에 대한 교양을 쌓아본적도 없고 단순히 블로그와 여러 자료를 읽어보고 내린 나의 결론일 뿐이다. 이 생각을 이해하려면 먼저 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을 참고할 수 있게 되고 이런건 컴파일 타임에 에러를 발생하지 않고 왜 그런지 이유 조차 모를 수밖에 없기 때문이다.

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



Flash 애플리케이션을 제작할때 항상 고민되는 것은 메모리와 속도일 것이다. 이 문서는 이러한 고민을 했던 분들을 위해 본인의 경험을 반영하여 작성한 것이다. 주의할 것은 이 문서에서 다루는 Object Pool은 메모리, 속도 개선을 위한 하나의 방법일 뿐이지 전부는 아니다라는 것이다. Object Pool이 어떻게 당신의 애플리케이션의 속도와 메모리를 개선시켜줄 것인가 전달하는 것이 문서의 목표이다.

목차

  1. 객체 관리에 대한 질문!
  2. new 연산자는 비싸다!
  3. Object Pool이란?
  4. Object Pool을 도입할 때 고려사항
  5. Object Pool을 사용해보자.
  6. Object Pool과 new 연산자 사용 비교
  7. 메모리 사용의 폭이 급격한 애플리케이션은 좋지 못하다.
  8. 재사용할 수 있는 클래스 제작
  9. 정리하며
  10. 참고글



1. 객체 관리에 대한 질문!
제작된 애플리케이션에 필요한 최소한의 메모리에 대해서 생각해 본적이 있는가? 하나의 애플리케이션에 들어가는 최소 자원(resource)은 얼마나 될까? 그 자원은 얼마나 자주 사용하고 버리는 것일까?

슈팅 게임을 보면 동일한 적군전투기가 많이 나온다. 그 중에 적군전투기1이 있다고 하자. 적군전투기1이 한 화면에 5개가 나왔다. 아군전투기에 의해 죽든 화면에서 사라지든 어쨌든지 언젠간 화면에서 사라지고 쓸모없게 된다. 좀 있다가 또 적군전투기1이 3개가 나온다. 또 반복해서 사라진다. 당신이 이 슈팅 게임을 만든다면 적군전투기1 관리를 어떻게 할 것인가? 즉, 화면에 나타날때 new로 생성하고 사라지면 delete로 메모리에게 해지요청할 것인가? 아니면 어느 특정 영역에 화면에 나타날 전투기1을 미리 5개에서 10개 생성해놓고 필요할 때마다 재사용할 것인가?

2. new 연산자는 비싸다!
new는 클래스를 통해 객체를 생성하는 연산자이다. 이는 비교적 비용이 비싼편이다. 비용이 비싸다는 말을 구체적으로 설명하자면 new를 통해 객체를 생성하여 메모리에 올리고 다 사용했다고 판단될때 delete이나 null등을 이용해 객체참조를 제거하여 가비지 컬렉터에 맡기게 되며 결과적으로 가비지 컬렉터는 이 객체를 어느 특정시점에 제거하는 일련의 객체 생성과 삭제까지 동작이 꽤 복잡하여 메모리, CPU연산등의 자원소비가 크다라는 것을 의미한다. 특히나 클래스의 크기가 크고 복잡하다면 그 비용은 더욱 갑절로 들게 된다. new 연산 사용은 이처럼 비싼 비용을 지불해야 하기 때문에 할 수 있다면 최소한으로 사용하도록 하는 것이 속도와 메모리 관리에 도움을 준다.

앞서 설명한 슈팅 게임을 보자면 적군전투기1은 수시로 보였다가 안보였다가 한다. 이 경우 new, delete를 반복한다면 경우에 따라서 엄청난(?) 비싼 비용을 지불하게 된다.

다른 예를 들어보겠다. 본인은 얼마전에 스타플(http://starpl.com)이라는 사이트에서 제공하는 타임라인 애플리케이션을 Flash로 개발했다. 타임라인은 사용자의 기록, 앨범등을 시간순으로 보여주게 되며 좌우측으로 이동해서 내 인생의 기록을 보는듯한 느낌을 주도록 만들었다. 이때 화면에 보여지는 사용자의 기록들이 수시로 보여졌다가 사라진다. 이때 사용된 기록을 보여주는 시각객체(DisplayObject)를 new와 delete를 반복해서 생성 및 삭제한다면 너무 비싼 비용을 들이는 것과 같다.

스타플의 타임라인

참고글 : 스타플 타임라인 업그레이드 및 앨범 기능 추가 소식

앞의 2가지 예에서 언급했듯이 자주 보여지고 사라지는 객체를 필요할 때마다 new연산자를 사용하는 것보다는 어느 특정 영역에 미리 생성해놓고 재사용을 반복하는 구조를 도입한다면 메모리와 CPU연산에 있어서 큰 비용절감을 기대할 수 있게 된다.

3. Object Pool이란?
Object Pool(객체 풀)은 객체를 미리 만들어놓고 담아놓는 메모리 영역이다. 개발자는 이 Object Pool에 미리 만들어진 객체를 사용하고 다 사용한 뒤에는 다시 반환시킴으로서 재사용(또는 대여)을 하도록 한다. Object Pool을 사용하면 처음 객체를 생성할때와 더 필요한 객체가 있는 경우를 제외하고 new 연산을 최소화 할 수 있다.

4. Object Pool을 도입할 때 고려사항
Object Pool은 미리 객체를 생성하고 지속적으로 재사용하기 위해 사용한다. 이때 미리 객체를 생성한다는 것은 초반 애플리케이션 동작 퍼포먼스에 영향을 미칠 수 있다.

Object Pool을 사용했을때 다음과 같은 장점을 얻을 수 있다.

1. new 연산자를 자주 사용해야하는 경우에 도입하면 애플리케이션 속도 향상을 기대할 수 있다.
2. 가비지 컬렉터에 의존하지 않는 메모리 관리를 꾀할 수 있다.

하지만 일반적으로 10개가 필요한데 미리 100개를 만들어 항상 10% 미만 정도의 재사용률을 보인다면 그것은 메모리 낭비이다. 그러므로 다음과 같은 조건에서 Object Pool을 사용하고 관리하는 것이 좋겠다.

1. 비교적 큰 용량의 객체를 자주 사용되거나 반환해야하는 경우에 사용한다.
2. 미리 생성할 객체를 적당하게 정하도록 한다.

반대로 위 경우가 아니라면 쓸데없이 Object Pool을 사용하지 말자.


5. Object Pool을 사용해보자.

폴리고널 lab에서 간단한 Object Pool을 공개했다. 아래 링크에서 다운로드 받을 수 있다.

Download: ObjectPool_v1.1.zip

사용 방법은 매우 간단하다. ObjectPool 클래스는 아래 코드처럼 특정 클래스를 할당(allocate)하면 된다.

var isDynamic:Boolean = true;

var size:int = 100;

var pool:ObjectPool = new ObjectPool(isDynamic);

pool.allocate(MyClass, size);


isDynamic 플래그는 Object Pool내의 객체가 비어있는가 체크하기 위해 사용한다. 즉, true이면 Pool은 자동적으로 정해진 크기(size)를 넘어서더라도 Pool의 크기를 확장하여 새로운 객체를 줄 수 있으나 false이면 정해진 크기만큼 객체를 이미 준 경우에는 Pool이 비게 되므로 Error를 발생시킨다. 

크기(size)를 100으로 지정했기 때문에 미리 Object Pool에 100개의 MyClass의 객체를 생성해 두도록 한다.

이제부터 MyClass는 아래와 같은 방식으로 사용하면 되겠다.

myObjectArray[i] = new MyClass();

// 코드 대신 아래를 사용한다.

myObjectArray[i] = pool.instance;


다 사용한 MyClass객체는 Object Pool에 반환해줘야 한다. 이때 가비지 컬렉터의 기능을 이용하는게 아니라 재사용(recycle)을 위해서 다음과 같이 코딩하면 되겠다.

pool.instance = myObjectArray[i];

사용방법은 이게 전부다.

원한다면 자신의 애플리케이션에 맞게 이 공개된 ObjectPool을 개선시킬 수 있다. 가령, 100개의 Pool을 만들어놓지만 처음부터 100개를 생성해놓지는 않겠고 필요할 때마다 생성해주는 구조인 게으른(lazy) 생성을 기대할 수 있을 것이다. 또는 DisplayObject의 경우라면 사용한다는 것을 DisplayObject.parent 속성으로 판단할 수 있다. 위 처럼 pool.instance = '다 사용한 객체'와 같은 형태로 반환하는 것보다 자신의 부모가 존재하지 않는다면 재사용할 객체로 판단하도록 만들면 일종의 자동 Object Pool을 만들 수 있다. 자동 Object Pool은 다음 링크를 참고하면 좋겠다.

자동 풀링(auto pooling)을 구현해보자.



6. Object Pool과 new 연산자 사용 비교

Object Pool을 사용해야하는 타당성은 퍼포먼스 실험을 통해 쉽게 알 수 있다.  이러한 점에서 폴리고널 랩에서 제공하는 실험은 매우 유용한 자료이다.

실험 방법은 아래와 같다. (폴리고널 랩에서는 이 실험을 Window Vista, Flash Player 9.0.124에서 진행했다.) 그리고 Pool크기는 100으로 지정했다.

첫번째로 아래 코드처럼 k수 만큼 Object Pool로 부터 객체를 get만 한다.

for (var i:int = 0; i < k; i++) instances[i] = p.instance;


두번째로 아래 코드처럼 k수 만큼 Object Pool로부터 객체를 get/set 한다.

for (i = 0; i < k; i++) instances[i] = p.instance;

for (i = 0; i < k; i++) p.instance = instances[i];


세번째로 아래 코드처럼 k수 만큼 Object Pool대신 new 연산자를 사용한다.

for (i = 0; i < k; i++) instances[i] = new MyClass();



결과는 다음과 같다.


네이티브 Object 클래스의 경우 Object Pool을 사용하는 것이 new 연산자를 사용한 것보다 5배 정도 빨랐다.


Object 보다 더 크고 복잡한 flash.geom.Point의 경우는 약간더 빠른 결과를 보인다.


 flash.display.Sprite 객체를 가지고 하면 무려 80배나 속도 개선이 되었다. 이는 복잡하고 큰 클래스에 Object Pool을 도입하면 효과적일 수 있다고 판단할 수 있다.

이 실험을 통해 적절한 방법으로 Object Pool을 사용하는 것은 애플리케이션의 퍼포먼스 증가에 지대한 영향을 줄 수 있다는 것을 깨달을 수 있다.


7. 메모리 사용의 폭이 급격한 애플리케이션은 좋지 못하다.
본인은 어떤 애플리케이션(Flash가 아니더라도)이든지 그 애플리케이션에 필요한 최소한의 메모리는 항상 존재한다고 생각한다. 무조건 메모리 사용을 줄여보겠다고 new, delete를 반복하는 것은 잘못된 것이다. new, delete를 반복하면 오히려 CPU 및 메모리 사용의 반복을 부축이며 애플리케이션의 전체적인 퍼포먼스를 저하시키는 경우가 발생할 수 있다. 실제로 new,delete를 반복하는 구조로 개발한 애플리케이션을 Flash Builder의 프로파일링 기능을 통해 메모리 사용을 살펴보면 큰폭으로 메모리 사용/해지가 일어난다는 것을 확인할 수 있다. 그러므로 몇몇 사람들이 Object Pool과 같은 개념과 같이 퍼포먼스 향상에 도움이 되는 개념을 전혀 사용하지 않고 Flash는 느리고 가비지 컬렉터 동작은 비정상적이다라고 말하는 것은 섣부른 판단이다라고 생각한다. 얼마든지 메모리를 효율적으로 사용하고 속도 개선을 할 수 있음에도 불구하고 잘못된 개발 방식때문에 전체적인 퍼포먼스가 나빠지는 것은 결국 개발자의 잘못인 것이다. 이는 Flash든 어떤 언어든 동일한 생각으로 접근해야 한다.

Flash Builder의 프로파일링 기능을 이용해 Object Pool과 new 연산자간의 메모리 변화를 확인할 수 있는 간단한 실험을 해보겠다. (프로파일링 기능을 사용하는 방법은 주제에서 벗어나므로 제외하겠다.)

먼저 Shape객체를 확장해서 크기와 색이 렌덤하게 그려지는 클래스를 만든다.

//화면에 출력할 Shape

class RandomShape extends Shape {

        static private var colorPool:Array = [0xff0000,0x00ffff,0x0000ff,0x00ff00,0x0f0f0f,0xf0f0f0,0xffff00,0xf00cf0,0x00fcaa,0xff0c9a];

        public function RandomShape():void {

        }

        public function draw():void {

               var radius:Number = Math.random() * 40;

               var color:uint = colorPool[ Math.floor(Math.random()*9) ];

               graphics.clear();

               graphics.beginFill(color,1.0);

               graphics.drawCircle( 0, 0, radius );

               graphics.endFill();

        }

}


위 클래스를 가지고 자동 Pool을 구현한 클래스를 만든다.(참고 : http://www.diebuster.com/?p=1000)

//Shape 자동 객체 Pool

class RandomShapeAutoPool {

        private var _pool:Vector.<RandomShape>;

        public function RandomShapeAutoPool() {

               //적당히 수로 초기화한다.

               _pool=new Vector.<RandomShape>();

        }

 

        public function getShape():RandomShape {

               var key:*, result:RandomShape;

               //먼저 기존의 pool에서 찾아본다.

               for (key in _pool) {

                       //만약 해당 객체가 null이라면 new 통해 생성한다.

                       if (_pool[key] === null) {

                              _pool[key]=new RandomShape;

                              result=_pool[key];

                              break;

                       //null 아니라면 parent 조사하여 사용가능한지 판단한다.

                       } else if (_pool[key].parent === null) {

                              result=_pool[key];

                              break;

                       }

               }

               //기존의 pool안에서 쓸만한 찾지 못했다면

               if (result === null) {

                       result=new RandomShape;

                       _pool[_pool.length]=result;

               }

               //인스턴스를 반환한다.

               return result;

        }

}


이제 테스트할 수 있는 호스트코드를 제작해보겠다.

package {

        import flash.display.*;

        import flash.events.*;

        import flash.text.*;

        import flash.utils.*;

 

        [SWF(backgroundColor="#ffffff", frameRate="60", width="400", height="400")]

        public class ObjectPoolTest extends Sprite {

               private var pool:RandomShapeAutoPool=new RandomShapeAutoPool;

               private var poolFlag:Boolean = false;

               private var textPoolFlag:TextField;

               private var shapeCanvas:Sprite;

 

               public function ObjectPoolTest() {

                       addEventListener(Event.ADDED_TO_STAGE, ADDED_TO_STAGE);

               }

 

               private function ADDED_TO_STAGE($e:Event):void {

                       removeEventListener(Event.ADDED_TO_STAGE, ADDED_TO_STAGE);

                       stage.scaleMode=StageScaleMode.NO_SCALE;

                       stage.align=StageAlign.TOP_LEFT;

                       //rf)http://blog.jidolstar.com/656

                       if (stage.stageWidth === 0 && stage.stageHeight === 0) {

                              stage.addEventListener(Event.ENTER_FRAME, function($e:Event):void {

                                             if (stage.stageWidth > 0 || stage.stageHeight > 0) {

                                                     stage.removeEventListener($e.type, arguments.callee);

                                                     init();

                                             }

                                      });

                       } else {

                              init();

                       }

               }

 

               private function init():void {

                       //shape 부모객체

                       shapeCanvas = new Sprite;

                       addChild( shapeCanvas );

                       //PoolFlag 상태표시

                       textPoolFlag = new TextField();

                       textPoolFlag.text = "poolFlag=" + poolFlag;

                       addChild(textPoolFlag);

                       //마우스 클릭 영역

                       graphics.beginFill(0x000000,0);

                       graphics.drawRect(0,0,stage.stageWidth,stage.stageHeight);

                       graphics.endFill();

                       addEventListener(MouseEvent.CLICK, MOUSE_CLICK);

                       //반복동작     

                       addEventListener(Event.ENTER_FRAME, ENTER_FRAME);

               }

 

               private function MOUSE_CLICK($e:MouseEvent):void {

                       //마우스 클릭때마다 poolFlag 상태를 전환

                       poolFlag = !poolFlag;

                       textPoolFlag.text = "poolFlag=" + poolFlag;

               }

              

               private function ENTER_FRAME($e:Event):void {

                       var shape:RandomShape, i:int, numChildren:int;

                       //poolFlag 따라서 new연산자 또는 Object Pool에서 객체 참조

                       shape = poolFlag ? pool.getShape() : new RandomShape;

                       shape.draw();

                       shape.x = Math.random() * stage.stageWidth;

                       shape.y = 0;

                       shapeCanvas.addChild( shape );

                       //계속 아래로 떨어뜨림. 화면에서 벗어나면 removeChild시킴

                       numChildren = shapeCanvas.numChildren;

                       for( i=0; i<numChildren;i++) {

                              shape = shapeCanvas.getChildAt(i) as RandomShape;

                              shape.y += 5;

                              if( shape.y > stage.stageHeight ) {

                                      shapeCanvas.removeChild( shape );

                                      i--;

                                      numChildren--;

                              }

                       }

               }

        }

}


동작은 매우 단순하다. poolFlag가 true이면 제작한 자동 Object Pool을 사용하겠다는 것이고 false이면 new 연산자를 사용해 RandomShape객체를 생성하겠다는 것이다. poolFlag 전환은 마우스 클릭으로 할 수 있다. 그리고 비가 내리는 것처럼 Shape 객체는 위에서 부터 아래로 떨어지며 맨 아래에 닿으면 removeChild 시킨다.

테스트 화면


실행 코드와 결과물은 http://wonderfl.net/code/805dc65d1f7800b7bee5dc3cd72ca21c0a87c2d6 에서도 볼 수 있다.

이제 Flash Builder의 프로파일링 기능을 이용해 메모리 변화 및 객체 생성빈도를 살펴보자.

먼저 new 연산자를 사용했을때 메모리 변화를 보자.

new 연산자로 객체생성시 Memory Usage


new 연산자를 사용해 객체를 생성하고 필요없을때 removeChild를 하게 되면 가비지 컬렉션 처리가 되기 때문에 일정한 시점에 메모리가 해지되는 것을 반복하는 것을 확인할 수 있다.

new 연산자로 객체생성시 Live Object


위 표는 축적된 객체수(Cumulative Instances), 현재 객체수(Instances), 축적된 메모리(Cumulative Memory), 현재 메모리(Memory)를 나타낸다. 변화 무쌍하며 실제로 보여지지 않는 객체도 가비지 컬렉션 처리가 일어날때까지 메모리에 남아 있다. 분명 비효율적으로 보인다.

반대로 Object Pool을 이용했을때 메모리 변화이다.

Object Pool로 객체관리시 Memory Usage

new 연산자를 사용했을때와 비교가 안될 정도로 고요한 느낌이 든다. 거의 일직선의 메모리 사용율을 보인다.

Object Pool로 객체관리시 Live Object


축적되는 객체도 81개를 넘지 않는다. 그만큼 객체 생성과 삭제에 민감하지 않게 되고 가비지 컬렉터 도움을 받지 않고 객체관리가 되는 것을 확인할 수 있다.


8. 재사용할 수 있는 클래스 제작

Object Pool을 사용시 한가지 고려할 사항은 재사용할 수 있는 클래스를 만들어야 한다는 점이다. 왜냐하면 Object Pool은 단순히 재사용을 위한 공간만 제공하는 것이지 재사용을 하기 위한 어떤 기능을 클래스에게 줄 수 없기 때문이다. 그래서 재사용이 가능한 클래스를 설계하는 것은 하나의 기술적 이슈가 될 수 있다. 

재사용할 수 있는 클래스를 어떻게 만드는지 간단한 예를 들어보겠다.

final public class MyData {

        public var type:String;

        public var id:int;

        public var name:String;

}


위 클래스는 데이터를 담는 클래스이다. id, name은 실제 사용하는 데이타 값들이고 이 데이타의 종류는 type으로 구분한다. 이 데이타를 사용하는 인터페이스를 제작해보겠다.

public interface IMyClass {

        function init($data:MyData):void;

        function clear():void;

        function get data():MyData;

}


이 인터페이스는 앞으로 만들 MyClass01, MyClass02... 등을 구현하기 위한 것이다. 초기화 하기 위해 init()함수가 있고 인자로 위에서 제작한 MyData 객체를 받는다. 또한 사용할 필요가 없을때 내부 청소를 위해 clear()함수를 추가했다. 게다가 가지고 있는 data를 참고하기 위해 get data()도 만들었다. 이제 이 인터페이스를 구현한 클래스는 아래처럼 만든다.

final internal class MyClass01 extends Sprite implements IMyClass {

        private var _data:MyData;

        public function MyClass01() {

        }

        public function init($data:Mydata):void {

               _data = $data;

               //구현

        }

        public function clear():void {

               _data = null;

        }

        public function get data():MyData {

               return _data;

        }

}

 

final internal class MyClass02 extends Sprite implements IMyClass {

        private var _data:MyData;

        public function MyClass02() {

        }

        public function init($data:Mydata):void {

               _data = $data;

               //구현

        }

        public function clear():void {

               _data = null;

        }

        public function get data():MyData {

               return _data;

        }

}

 

final internal class MyClass03 extends Sprite implements IMyClass {

        private var _data:MyData;

        public function MyClass03() {

        }

        public function init($data:Mydata):void {

               _data = $data;

               //구현

        }

        public function clear():void {

               _data = null;

        }

        public function get data():MyData {

               return _data;

        }

}



코드가 길어보이지만 3개 클래스 모두 IMyClass를 구현했고 Sprite를 확장했다. 단지 클래스 이름만 다르며 실제 구현부는 알아서 구현하면 된다.

개발자는 MyClass01, MyClass02, MyClass03은 매우 자주 new 연산자로부터 생성되는 클래스로 판단했고 그래서 Object Pool을 도입하겠다고 결정했다고 하자. 그럼 다음과 같이 시도해 볼 수 있다.

import de.polygonal.core.ObjectPool;

import flash.display.DisplayObject;

 

public class MyPool {

        private static var poolList:Object;

       

        static public function init():void {

               poolList = {

                       'type01':new ObjectPool(true),

                       'type02':new ObjectPool(true),

                       'type03':new ObjectPool(true),

               };

               poolList.type01.allocate(20, MyClass01);

               poolList.type02.allocate(10, MyClass02);

               poolList.type03.allocate(100, MyClass03);

        }

       

        static public function getObject($data:MyData):IMyClass {

               if( $data === null ) {

                       throw new Error('인자값은 null이면 안됩니다.');

               }

               try {

                       var object:IMyClass = poolList[$data.type].object;

                       object.init($data);

               } catch {

                       throw new Error('데이타의 type값이 잘못된 값입니다.');

               }

               return object;

        }

       

        static public function returnObject($object:IMyClass):void {

               if( $object === null ) {

                       throw new Error('인자값은 null이면 안됩니다.');

               }

               $object.clear();

               if( DisplayObject($object).parent ) {

                       DisplayObject($object).parent.removeChild( DisplayObject($object) );

               }

               poolList[$object.data.type].object = $object;

        }

}



위에서 제작된 MyPool은 static클래스이며 ObjectPool을 이용해 IMyClass 인터페이스를 구현한 클래스를 init()함수에서 적당하게 할당하는 것을 확인할 수 있다. 또한 getObject()를 통해 인자값 data를 참고하여 참조할 Object Pool을 찾아 IMyClass를 구현한 각각의 클래스의 객체를 받아올 수 있으며 returnObject를 통해 다 사용한 객체를 반환한다.

실제로 실무에서 이런 형태로 개발했고 이는 매우 유용했다. MyPool 클래스를 더 개선해서 더 많은 수 Object Pool을 감당할 수 있도록 확장할 수 있다면 더욱 유용해질 것이라 생각한다.


9. 정리하며

Object Pool 개념은 Flash에만 국한되는 유용한 개념이 아니다. 이런 개념들에 대한 노하우를 계속 쌓아간다면 당신의 애플리케이션은 더욱 좋은 작품으로 탈바꿈할 수 있을 것이다.

Flash Player가 느리고 메모리 관리가 안된다고 생각하지는 말자. Flash Player 태생자체가 그런것을 어찌하겠는가? 제작한 애플리케이션이 느리다면 그것은 결국 개발자가 잘못한 것임을 항상 인지하자. 중요한 것은 여전히 Flash Player는 유용하고 많이 사용되고 있으며 지금도 나날이 발전하여 Cross OS, Cross Browser를 넘어 Cross Device 세계로 뻗어가고 있다는 점이다.


10. 참고글
글쓴이 : 지돌스타(http://blog.jidolstar.com/666)

 

위 그림은 Flex Builder 3(Flash Builder 4)의 Profiling(프로파일링) 기능에서 시간별 메모리의 상태를 보여주고 있는 것이다. 프로젝트를 하다가 저놈의 메모리가 도데체 왜 올라가는지 이해할 수 없었다. 분명 참조를 제대로 지워주고 있지 않으렸다!

 

등록했던 이벤트도 지워주고 자식들도 빼주고 붙여주면서 원인을 분석해갔다. 전체에서 부분으로 부분에서 전체를 훑어보면서 메모리의 변화를 계속 살펴봤다.

 

 

위 그림은 메모리가 정상적으로 반환되고 있는 모습이다. Flash Player에서는 참조관계가 root에 묶여 있거나 환형참조일때 가비지 컬렉션(Garbage Collection)으로 인식하여 어느 서점에 메모리에서 해지를 시킨다. 그래서 개발자는 참조를 잘 지워줘야 한다.

 

메모리가 비정상적으로 올라간 첫번째 원인은 이벤트 핸들러를 등록만 하고 지우지 않은데서 비롯되었다. 그래서 이벤트 핸들러를 약참조(weak reference)로 하도록 해서 해결했다. 약참조의 경우 소멸시점이 명확하지 않을때 유용하게 사용할 수 있다. 그리고 객체 중복 참조로 인해 메모리가 상승했는데 한쪽에서만 참조를 없애고 다른 한 곳에서는 삭제하지 않은 오류를 범했다. ㅡㅡ;

 

아~ Flex Builder에 이 기능이 없었다면 저걸 어떻게 찾을 수 있었을까? Flash Builder 4부터는 이 프로파일링 기능에 퍼포먼스 해석(performance analysis)기능 까지 추가 되고 개선되었다는데~~ 어찌하는지 해봐야 겠다.

 

Flex Builder에서 디버깅&프로파일링 하기 - 1부 -

Flex Builder에서 디버깅&프로파일링 하기 - 2부 -

 
Flash Builder는 아래 링크에서 다운로드 받을 수 있습니다.
http://www.adoberia.co.kr/pds/down.html?src=text&kw=000026 

DisplayObject 속성중에 cacheAsBitmap을 사용해 본 적이 있나요?

 

cacheAsBitmap은 시각화된 객체를 Bitmap으로 캐싱해주는 역할을 하는 속성입니다. 이것이 true가 되면 DisplayObject는 Bitmap처럼 되는 것이지요. Flash에서 Bitmap과 상반되는 것이 Vector입니다. 일반적으로 Flash에서 그래픽적인 요소는 거의 Vector로 처리되지요. Bitmap과 Vector에 대한 차이점과 장단점을 굳이 설명안하겠습니다.

 

단지 제가 궁금하고 해결하고 싶은 문제는 cacheAsBitmap속성이 적용된 것을 확대/축소할때 입니다.

이 속성을 true로 하고 이동처리(마우스 Drag등)을 하는 것과 안한것은 엄청난(?)속도 차이를 보입니다. 복잡한 Graphic이 들어가고 이동이 필요한 경우 cacheAsBitmap을 true로 설정하여 애플리케이션의 퍼포먼스를 높힐 수 있지요. 하지만 cachAsBitmap은 메모리와는 적입니다. 가령 확대/축소할때 가장 큰 문제가 되는데 Bitmap이 확대/축소할때 이미지는 그 크기만큼 메모리를 잡아먹습니다. 아래 제가 쓴 글을 보시면 이해될 겁니다.

 

[Flex/AIR] 메모리의 무서운 적! DisplayObject의 cacheAsBitmap 속성

 

저는 이동시에는 cacheAsBitmap을 true로 설정하고 확대/축소할때는 false로 설정해서 퍼포먼스 개선을 하려고 합니다. 하지만 cacheASBitmap을 설정하더라도 적용되는 시점이 문제입니다. 확대/축소하기 전에 false로 지정하고 바로 확대/축소를 하면 메모리를 잡아먹는다는 것이지요. 다음 Render시점에서 확대/축소를 해야하는 것인가 생각이 드는데 아직 뾰족한 묘안이 없습니다.

 

혹시 저와 같은 경험을 하셨다던가 아니면 해결책을 알고 계신분 있다면 댓글 부탁드리겠습니다.

 

 

+ Recent posts