'Flash 애플리케이션의 속도와 메모리 개선 : Object Pool의 활용'에 해당되는 글 2건

  1. 2014.08.20 [펌] Flash 애플리케이션의 속도와 메모리 개선 : Object Pool의 활용
  2. 2014.06.17 [펌] Flash 애플리케이션의 속도와 메모리 개선 : Object Pool의 활용
카테고리 없음2014. 8. 20. 14:37
반응형

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을 공개했다. 아래 링크에서 다운로드 받을 수 있다.


사용 방법은 매우 간단하다. 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

Posted by 1010
반응형

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


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을 공개했다. 아래 링크에서 다운로드 받을 수 있다.


사용 방법은 매우 간단하다. 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

Posted by 1010