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나 기타 무거운 프레임워크를 사용하지 않고 ActionScript 3.0 API 수준으로 개발을 한지 이제 겨우 1년이 넘은 것 같다. Flex로 개발할 것과 ActionScript로 개발해야할 것에 대한 구분은 항상 명확하다. 자주 새로 빠르게 로드되어야 하는 애플리케이션 제작은 무조건 ActionScript 기반으로 제작한다. 하지만 가끔 사용하며 Flex 컴포넌트를 잘 활용해야하는 애플리케이션이라면 Flex로 개발한다.

ActionScript 3.0기반으로 개발하고 완성하고 나서는 뭔가 꼬여있는듯한 느낌, 유지보수가 안될 것 같은 느낌을 항상 받아왔다. 또한 깨끗하지 못한 객체관리가 항상 걸렸고 너무 길어져서 지저분해지는 코드 또한 마음에 들지 않았다. 그리고 클래스간 역할 분담이 분명하지 않는다는 느낌을 받았다. 동작은 뭔가 석연치 않은 느낌을 계속 받았다. 어떤 개발의 스킬이나 전문적인 지식에 대해서 학습을 해보지 못해봤던 나로써는 설계때부터 항상 고민거리가 생긴다.

나는 근 한달동안 블로그 활동 및 외부 활동을 거의 안하면서 개발에만 매진하면서 지냈다. 바로 스타플의 타임라인 개발때문이였다. 타임라인이 궁금하시다면 아래 링크로 접속해보면 된다.

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


개발하면서 위에서 제시한 문제점을 어느정도 해소시킬 수 있는 몇가지 개념들이 있었다. 여기에 간단하게 적어두도록 하겠다.


1. 객체의 재사용 Object Pool
Flash 개발하면서 항상 걸리는 문제중에 하나가 메모리이다. Flash Player가 정확한 가비지 컬렉터(GC)를 가진다고 하지만 지금도 그다지 명확한 신뢰를 받지 못하고 있다. 하지만 분명 GC는 동작한다. 중요한 것은 Flash Player의 GC를 너무 맹신하지는 말자는 것이다.

new를 통한 객체 생성은 항상 지양하는 것이 가장 중요한 메모리 관리의 첫걸음이다. MyClass.Factory()와 같은 static 클래스를 통해 내부적으로 new를 사용하고 각종 처리는 factory가 담당하는 것이 각종 초기화 및 객체관리에 도움이 된다.

더불어 매우 자주 사용하다가 없어지는 객체를 계속 new를 통해 생성하고 GC객체화 시키는 것은 메모리 관리에 도움이 안될뿐 더러 퍼포먼스에도 문제가 있다. 실제로 비교적 큰 디스플레이 객체인 Sprite를 new를 자주하면 전체적인 퍼포먼스에 좋지 않은 영향을 줄 수 있다. 이런 경우에 퍼포먼스와 메모리를 동시에 해결할 수 있는 방법은 바로 Object Pool을 이용하는 것이다.

Using object pools : http://lab.polygonal.de/2008/06/18/using-object-pools/ 

위의 글은 Object Pool의 강력함을 시각적으로 말해주고 있다. 특히나 Object나 Point와 같은 작은 클래스가 아닌 Sprite와 같은 큰 클래스의 경우 new 연산자 사용보다 Object Pool의 사용은 큰 이점을 주고 있다는 것을 알 수 있다.

사용한 객체를 그냥 버리지 말자. 잘 보관했다가 다시 사용할 수 있게 만들고 사용하자... 특히나 시각객체(Sprite기반)이 자주 생성,소멸해야하는 경우라면 강력히 Object Pool을 사용하라고 주장한다.


2. Linked List 와 Dictionary
컴퓨터를 전공한 사람이라면(본인은 아님) 자료구조를 배우면서 Linked List는 반드시 이해해야할 필수 항목이다. 나는 이번 타임라인을 제작하면서 Linked List를 활용해 중간중간에 얻어진 시간순서 데이터를 엮어주는데 요긴하게 사용했다. 일반적인 Array형태의 데이터는 중간에 데이터 삽입하는 것은 효율적이지 못하지만 Linked List는 연결하고자 하는 지점의 연결부의를 끊고 그 중간에 삽입하여 앞뒤로 연결만 하면 되기 때문에 훨씬 효율적이다.

Data Structures example : linked lists http://lab.polygonal.de/2007/08/13/data-structures-example-linked-lists/

또한 특정 key의 자료검색에 Dictionary를 사용했다. Dictionary를 직접 사용하는 것보다는 한번 감싸서 클래스를 만들어 추가/삭제/편집/데이터접근등을 처리하기 쉽게 하면 정말 요긴해진다. 나는 Linked List와 더불어 Dictionary를 통해 key값을 통한 데이터 검색에 사용했다. 이것을 이용하면 아무래도 index 순으로 찾는 것보다 훨씬 빠른 검색을 할 수 있다.


3. MVC
개발자라면 Model-View-Controller에 대한 개념을 잘 알고 있을 것이다. 이러한 개념은 Flex 수준의 개발이라면 Flex가 어느정도 지원해주는 편이기 때문에 그나마 괜찮지만 ActionScript 수준에서 개발하려면 이 점을 고려하는 것이 좋다. 기본적으로 Model의 데이터 변화는 View에 영향을 미치고 View의 변화는 Controller에 영향을 준다. Controller는 View와 Model을 통제하며 서로의 상호작용에 조절한다. 그리고 View가 Model을 절대 변경하지 않는다. 이런 약속으로 MVC 구조를 설계하면 View와 View관계도 명확해지고 아직 로드되지 않는 View(모듈)에게도 도움이 된다.

타임라인의 경우 최초 구동될때 달력과 보기설정은 아에 모듈로 만들어 버린다. 그리고 보여달라고 요청할때만 모듈을 로드해 시각화시킨다. 이때 모듈은 부모 애플리케이션에 Model에 있는 데이터를 사용한다. 이 데이터는 View(모듈)의 존재를 모르기 때문에 어떤 상황이 되든 사용할 수 있다. 또 달력과 보기설정은 같이 보여질 수 없다. 각각 show()와 hide()함수가 있는데 달력이 show하면 보기설정은 hide가 호출되어야하는 식이다. 그런데 View에서 이런것을 컨트롤해버리면 View간에 상호 간섭이 생긴다. 이런 경우 Controller가 중재하도록 하면 훨씬 쉽게 이런 문제를 해결할 수 있다.


4. 인자객체를 이용하자.
TextField를 이용해서 객체를 생성해본적이 있을 것이다. 1개 Text를 출력하기 위해서 얼마나 많은 설정을 해야하고 또한 수십줄이 넘는 코딩을 해야하는가? 타임라인의 달력 기능에는 한개 한개가 다 TextField 객체들이다. 물론 반복적으로 생성해주면 되지만 이 마저도 마우스오버,클릭,선택등과 같은 동작에 대응해야하기 때문에 단 몇줄 코드로는 이런 기능을 구현하기가 어렵다.

나는 이러한 문제를 인자객체를 이용해 해결했다.
이에 대해서는 더이상 말이 필요없다. 다음 글을 보자.
인자객체 : http://diebuster.com/flash/?s=인자객체


5. Helper 수준의 프레임워크
이번 타임라인을 제작하면서 가장 도움이 되었다고 생각하는 사람은 hika님이다. 물론 같은 회사도 아니고 자주 만나본 것도 아니지만 hika님의 블로그에 정리해놓은 내용은 나에게 큰 자극제가 되었다. 그중에 hika님이 만들어놓은 ActionScript 3.0 프레임워크는 나에게 큰 도움이 되었다. 이는 흔히 말하는 무거운 프레임워크 아니다. 정확히 말하자면 Helper 수준이다. Helper 수준이라는 것은 Flex처럼 Button, Label, DataGrid와 같은 컴포넌트를 가지는 것이 아니라 Sprite, Shape, TextField, Loader등의 기본 ActionScript 3.0 기반의 클래스를 더욱 효율적으로 사용하게 하기 위한 것이라는 것이다.

나는 이 프레임워크를 이용해서 시각객체관리, 레이어관리, Embed자원관리, 원격자원관리, 이벤트관리, 데이터관리등 전반적으로 큰 도움이 되었다. 나는 hika님이 만들어놓은 프레임워크를 분석하면서 큰 구조의 변경없이 나만의 프레임워크로 변경하고 이번 타임라인 개발에 적극적으로 사용했다. 만약 프레임워크의 도움이 아니였다면 역시나 어려운 코딩을 감당해야할지도 모르겠다.


6. EnterFrame 수준의 동작 관리
MouseMove 이벤트를 시각객체를 이동하는데 사용하지 말자. 대신 EnterFrame 이벤트시에 stage.mouseX, stage.mouseY등을 사용해서 시각객체를 이동시키자. 실제로 MouseMove 이벤트는 그 동작이 매우 느리다. 그러나 EnterFrame 이벤트는 최소한 stage.frameRate에 지정된 수준에서 발생하기 때문에 느린 마우스 동작에 걱정하지 않아도 된다.

나는 타임라인을 마우스로 움직일 때 이 방법을 사용했다.

마우스 이벤트 최적화 : http://diebuster.com/flash/category/algorithm/io


7. Event 사용의 최소화
나는 Event 사용을 최소화 하려고 많이 노력했다. 정말 필요한 곳에만 사용해도 개발에 무리가 없었다. 시각객체를 통한 이벤트 전파는 생각보다 매우 느린 편이기 때문에 되도록이면 사용을 지양해야한다. 이것도 정말 필요하고 요긴하다고 판단할때만 사용하는 것이 옳다. 사용한 이벤트는 반드시 remove시키는 것이 현명하다. useWeakReference는 정말 필요할때만 사용하는 것이 좋다. 되도록이면 명시적으로 remove 시키는 것이 메모리 관리에 도움이 된다.


8. arguments.callee 활용
arguments.callee는 함수 자신을 말한다. 이는 꽤 유용한데 특히나 이벤트 리스너 함수에서 자신을 호출한 이벤트를 삭제할때 사용될 수 있다.

스스로 삭제되는 이벤트 리스너 : http://diebuster.com/flash/110


9. BitmapData.clone() 사용 지양
BitmapData 클래스는 비트맵 데이터를 저장하는 클래스이다. 이 데이터를 시각화 하기 위해 사용하는 클래스는 Bitmap이다. new Bitmap( bitmapData ) 시에 첫번째 인자가 BitmapData 객체이다.  같은 bitmapData가 자주 사용되어져야 하는 경우에 bitmapData.clone() 을 통해 계속 생성하지 말고 그냥 원본 bitmapData를 이용해 Bitmap객체를 생성해서 시각화 처리하자.

나는 타임라인에 들어가는 각종 비트맵 객체에서 bitmapData를 뽑아내어 static으로 참조해 계속 사용하도록 했고 Bitmap 또한 Object Pool 관리를 통해 최소한의 객체 생성을 유도시켜 메모리 관리를 하도록 했다.


10. mouseChildren, mouseEnabled, tabChildren과 같은 속성은 왠만하면 false로 지정하자.
이러한 속성들이 필요없는 곳에 사용되면 오히려 퍼포먼스에 악영향을 준다. 정말 필요한곳에만 사용하는 것이 좋다. 이들 속성의 의미를 파악해두는 것이 중요하다.


11. cacheAsBitmap은 이동에만 사용하자.
cacheAsBitmap은 확대,축소,회전,Alpha값 변경되는 곳에는 사용하지 않는 편이 좋다.
다음 글을 참고하자.

왜 cacheAsBitmap은 나쁜가? http://www.bytearray.org/?p=290


12. 동적언어의 장점을 충분히 살리자.
동적언어와 정적언어의 차이점은 메모리를 사용하는 차이점에서 비롯된다. 실행하는 시점에 초기에 메모리를 확보해서 사용하는 언어는 정적이다라고 하며 거대한 메모리를 먼저 잡아놓고 실행하는 시점에 메모리를 자유롭게 사용하는 것은 동적언어이다. ActionScript 는 동적언어이기 때문에 중간중간에 실행도중 클래스 정의, 함수를 추가/수정/삭제가 가능하다. 이에서 나온 개념이 클로저라는 것인데 이 개념을 잘 이용하면 코딩이 훨씬 간편하고 편리해질 경우가 많다. Flex의 대부분 코딩 방법은 정적언어처럼 되어 있는데 사실 ActionScript는 동적언어이므로 이를 더 잘 사용해서 만들어져야 한다고 생각한다. 

Closure에 대해 : http://diebuster.com/flash/82


추가사항

13. package 격리원칙
클래스 격리에 대해서는 들어봤을 것이다. 클래스내 함수에 public, private를 적절하게 잘 사용해서 외부에 노출되는 메소드는 3~4개정도로 하고 내부 동작방식을 몰라도 클래스를 쉽게 이해할 수 있도록 하는데 필요한 개념이다. 이는 사용하는 관점도 그렇고 상속받는 관점에서도 마찬가지로 적용되도록 한다.

패키지 격리라는 것은 internal을 적극활용한다는 의미이다. 아마도 많은 개발자들이 internal의 강력함을 잘 인지하지 못할것이다. 이에 대해서는 내가 굳이 설명할 필요 없다. 다음글을 보자.

플래시 애플리케이션 개발하기 : http://diebuster.com/flash/86


14. 적절한 코딩규칙
개발하는데 코딩규칙은 무엇보다 중요하다. 가령 클래스 private 변수에는 _을 붙힌다던지 const값은 대문자로 표시한다던지 그런것 말이다. ActionScript 3.0에서 코딩규칙은 더욱 상세화 되면 좋다. 왜냐하면 워낙 동적인데다가 다른 언어에 비해서 명확한 컴파일 단계의 문법체크가 약하기 때문이다.

hika님의 2009년도 코딩 규칙을 잠깐 보자. http://diebuster.com/flash/3 

나는 처음에 클래스에 C, 정적클래스에 CS를 붙이는 의도에 대해서 잘 이해할 수 없었다. 하지만 이에 대한 강력함은 개발해보면 안다. 나는 클래스에 C, 정적클래스에 S, 인자객체는 A, internal 클래스는 앞에 _, 어떤 클래스의 Part라면 클래스명뒤에 _파트명 이런식으로 클래스 이름 규칙을 정했다. 실제로 이렇게 관리하면 클래스가 정확히 어떤 역할을 하는지 눈에 팍 들어온다. 함수를 인자로 받아야하는 경우 FN_intNumberString_Boolean:Function 과 같이 쓰면 이 인자로 들어올 함수는 반드시 function( a:int, b:Number, c:String):Boolean 형태로 만들어야 된다. 이런 규칙은 코딩시 코드힌트기능에도 표시되기 때문에 사용하면 정말 편리하다.

반복구문에 대해 : http://diebuster.com/flash/?s=%EB%B0%98%EB%B3%B5%EA%B5%AC%EB%AC%B8&submit=

결국 코딩규칙을 사용하는 명확한 의미는 한눈에 코드를 파악하고자 하는 의도에서 찾아야 한다.


정리하며
앞서 말했지만 타임라인 개발하는데 많은 도움을 주신 분을 꼽자면 hika님이다. 부족하지만 이 분의 도움으로 나는 스킬업을 할 수 있었다. 사실 hika님 입장에서는 이런 외부활동과 저술활동이 없어도 부족함이 없어보인다. 하지만 그의 공유정신 때문에 개발에 입문하는 많은 사람들에게 큰 도움이 되는 것은 사실이다. 이 글을 통해 감사하다고 전하고 싶다.

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

+ Recent posts