액션스크립트의 객체 재사용을 위한 오브젝트 풀(Object Pool)
by 세계의끝 on 12.02, 2010, under OOP, 고수들은 가르쳐주지 않는 AS3.0 입문
이 포스트는 “플래시 플레이어의 가비지 컬렉션(gabage collection)에 대한 이해” 로부터 이어지는 내용 입니다.
A. Object Pool의 의미
객체 재사용 이라고 해서 뭐 엄청난 방법이 필요한 것은 아닙니다. 가장 쉽게 사용할 수 있는 방법으로는 Array 나 Vector 또는 Object 와 같은 컬렉션(Collection) 형태의 자료구조에 객체를 만들어 넣어두고 필요한 만큼 사용한 후, 필요없어진 객체를 다시 반납하는 방식으로 로직을 구성하면 됩니다.

Object in Pool !!!
이러한 구조를 객체 풀 또는 오브젝트 풀(Object Pool) 이라고 부르고, 오브젝트 풀로부터 객체를 획득하는 행위를 풀링(pooling) 한다고 표현합니다.Pool은 우리도 흔히 사용하는 단어 입니다. [구글 사전 링크] 수영장의 그것을 일컫는 단어이기도 하고요, 제안서에서도 자주 “인력 풀” 과 같은 형태로 자주 등장합니다. 스타크래프트의 저그 종족에서 저글링을 생산하기 위한 기본 건물을 스포닝 풀(Spawning Pool : 산란못)이라고 부르죠. 어떠한 대상이 모여있는 특정 장소라는 원래의 뜻을 가지고 있습니다.
오브젝트 풀을 다음과 같이 두 가지로 형식으로 구분할 수 있습니다.
B. 정적 오브젝트 풀
애플리케이션이 시작될때 미리 필요한 객체를 (정적으로) 만들어 놓고 시작하는 방식으로
- 필요한 객체 숫자가 정해져 있는 경우
- 필요한 객체의 숫자가 아주 많지 않은 경우
에 사용합니다.
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | package { import flash.display.Sprite; public class FixedPoolTestDrive extends Sprite { private var _itemList:Array = []; public function FixedPoolTestDrive() { createItem( 10 ); // 객체는 미리 생성 var item:Item = getItem( 3 ); // 3번 인덱스는 4번째 원소 item.y += 100; } private function createItem( $num:uint ):void { var i:uint, item:Item; for ( i = 0; i < $num; ++i ) { item = new Item;// 고정된 숫자만큼의 객체를 미리 생성 item.x = ( item.width + 5 ) * i; addChild( item );// 더 이상 객체를 생성할 일이 없기 때문에 addChild()와 _itemList[ i ] = item;// 배열에 넣는것을 미리 다 해 놓는다. } } private function getItem( $index:uint ):Item { if ( $index <= _itemList.length ) throw new Error( "Item 객체의 인덱스는 0 부터 " + ( _itemList.length - 1 ) + "번 까지 있고 " + $index + "번은 목록에 존재하지 않는 인덱스입니다." ); return _itemList[ $index ]; } } } |
C. 동적 오브젝트 풀
애플리케이션이 시작되는 시점에는 객체의 숫자가 0으로 시작하고 요청이 들어오면 필요한 만큼 new 로 생성해서 사용하는 방식으로
- 필요한 객체의 숫자를 정할 수 없는 경우
- 필요한 객체의 숫자가 많은 경우
- 또는 객체의 사용 갯수의 폭이 실행때마다 상당히 다른 경우(어떤 경우는 조금 사용하고, 어떤 경우는 많이 사용하는)
에 사용하면 적합 합니다.
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | package { import flash.display.Sprite; public class DynamicPoolTestDrive extends Sprite { private var _itemList:Array = []; public function DynamicPoolTestDrive() { var index:uint = 3; var item:Item = getItem( index ); item.y += 100; item.x = ( item.width + 5 ) * index; } private function getItem( $index:uint ):Item { // 요청한 인덱스에 해당하는 _itemList 배열을 변수에 대입. 인덱스에 해당하는 원소가 존재할수도 있고 아닐수도 있다. var item:Item = _itemList[ $index ]; // 위의 변수에 대입된 객체가 null 이라면 아래의 조건문이 실행된다. ( not 연산자 ! 로 조건을 걸었으므로. ) if ( ! item ) { item = new Item; // 객체를 생성 addChild( item ); // addChild() 하고 _itemList[ $index ] = item; // 해당하는 배열 인덱스에 넣은후 return 해준다. } return item; } } } |
어떻습니까? 간단한 오브젝트 풀이지만 이것으로 얻어지는 효과는 상당히 뛰어납니다.
비행기 슈팅게임을 만드는 경우를 예로 들어볼까요? 총알을 발사할 때마다 new Bullet() 을 하여 계속 총알을 만든다면 계속 메모리 사용량이 증가할 수 밖에 없겠죠. 총알이라면 크기가 작으니 그나마 다행입니다. 적 비행기라면 어떨까요?
어차피 비행기가 총알을 발사하는 한 화면에서 동시에 공존하는 총알의 최대 갯수가 존재합니다. 그만큼의 총알만 미리 만들어 놓고 총알이 화면 밖으로 나가서 더 이상 의미가 없게 되면 다시 회수에서 재사용 하는 것입니다.
메모리 사용량은 일정 수준에서 더 이상 늘어나지 않게 되고 최종보스를 만날때까지 안정적인 동작을 할 수 있겠죠.
한 가지 주의해야 할 점은, 현재 새로 사용하고자 하는 인덱스의 객체가 사용중인지 아닌지 판단할 수 있는 장치를 마련해야 하는 점입니다. 사용중인 객체를 마구 끌어다 쓰면 곤란해지겠죠. 객체 내부의 변수, 예컨대 public var isIdle:Boolean 과 같이 플래그를 두어 구분 하는 등의 방법을 사용하면 되겠습니다.
D. 오브젝트 풀 클래스
위에서 알아본 컬렉션을 이용한 오브젝트 풀은 호스트 코드에 섞여 있는 형태이기 때문에, 오브젝트 풀이 존재하는 클래스의 코드가 매우 길어서 가독성이 떨어진다거나, 객체들이 복잡한 구조를 가지고 있는 애플리케이션 이라거나 캡슐화를 업격하게 적용해야의하는 경우에는 사용하기 어려울 때가 있습니다. 이런 고민을 여러분만 했을리는 없겠죠. 그래서 전세계 여러 개발자들이 오브젝트 풀을 클래스 화 하여 관리하는 방법을 사용합니다.
액션스크립트에서 사용할 수 있는 오브젝트 풀 클래스 중에서는 우리나라 카페쪽 커뮤니티나 개인 개발자 블로그 등에서 여러번 소개된 바 있는 polygonal.de 의 라이브러리가 가장 유명합니다. 폴리고날에서 2008년 발표한 ObjectPool 라이브러리를 이 [링크]에서 다운받아 사용해 볼 수 있습니다.
이 글을 쓰기로 처음 마음먹은 무렵에는 저 역시 폴리고날의 오브젝트 풀 클래스를 여러분에게 소개하고 원리와 사용방법을 기술하려 했지만, 사실 제가 아니더라도 이미 다른 분들이 소개하고 있는데다가, 폴리고날 오브젝트 풀의 코드를 분석해 보고 난 후 코드를 분석하는것 자체가 의미가 별로 없다는 것을 깨달았습니다. 여러분도 소스코드를 보시면 느끼시겠지만, 이해하기 어렵습니다. 사실 이해할 필요 없이 제대로 동작하기만 해도 무방하죠. 그래서 더더욱…
객체의 재사용을 다룬 이 연속 포스팅들은 원래 “제3회 PFG 트렌드 공유 오픈세미나 ” 의 세션 발표의 목적으로 초고가 쓰여진 것인데, 발표 전날 방향을 살짝 바꿔서 오브젝트 풀을 간단하게 제작해보기로 했습니다.
아래의 오브젝트 풀은 여러분에게 동작 방식을 설명하기 위해 아주 기본적인 기능만을 가지고 있습니다. 누구든지 자유롭게 수정하여 사용하실 수 있습니다. (단, 수정하실 경우에는 제 이름을 원저자로, 수정하신 분을 수정저자 정도로 표기해 주시면 고맙겠습니다.)
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | package { import flash.display.DisplayObjectContainer; /** * Simple Object Pool Class with DisplayObjectContainer * @author 원종선[세계의끝] cuebrick[ at ]gmail.com http://ufx.kr/blog */ public class ObjectPool { private var _class:Class; private var _activeList:Array = []; private var _deactiveList:Array = []; private var _container:DisplayObjectContainer; /** * 할당받을 클래스(형:type)을 지정한다. * @param $class */ public function allocation( $class:Class ):void { _class = $class; } /** * 객체를 부모 컨테이너에 붙여서 반환받고 싶다면 설정한다. * @param $container */ public function setContainer( $container:DisplayObjectContainer ):void { _container = $container; } /** * 이 메서드를 이용해 객체를 요청한다. * _deactiveList 배열을 조회하여 객체가 존재하면 반환하고, 없으면 새로 생성하여 반환해준다. * 새로 생성하여 반환해준 경우 객체를 _activeList 에 push 해 놓는다. * @return */ public function getInstance():* { var instance:*; if ( _deactiveList.length == 0 ) { instance = new _class(); if ( _container ) _container.addChild( instance ); } else { instance = _deactiveList.shift(); } _activeList.push( instance ); return instance; } /** * 사용이 끝난 객체를 오브젝트 풀에 반환해준다. * 반환하면 _deactiveList 배열에 등록되고, 다시 사용할 수 있는 상태로 바뀐다. * @param $instance */ public function returnInstance( $instance:* ):void { var instance:*; for each ( instance in _activeList ) { if ( instance == $instance ) { trace( "instance 반환되었음 :", $instance ); _deactiveList.push( $instance ); _activeList.splice( _activeList.indexOf( $instance ), 1 ); } } } /** * 현재 사용중인 객체들의 목록을 조회 */ public function get activeList():Array { return _activeList; } /** * 현재 사용할 수 있는 객체들의 목록을 조회 */ public function get deactiveList():Array { return _deactiveList; } } } |
클래스 내부에 사용중 객체 목록과 미사용중 객체 목록이 배열로 두개 있고, 호스트 코드에서 getInstance() 메서드로 요청하면 미사용중 객체 목록 배열에서 반환해주거나, 노는 객체가 없으면 새로 만들어 반환해 주는 형태입니다. 호스트 코드가 대여하거나 반납한 객체는 그에 알맞은 상태의 배열로 옮겨 놓는 방식으로 간단하게 구현하였습니다. public 메서드에 주석을 자세하게 달아 놓았으므로 그 밖의 코드에 대한 별도의 설명은 필요하지 않을것 같습니다.
호스트 코드에서는 이렇게 사용하겠죠.
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // 오브젝트 풀 객체를 생성 var _pool:ObjectPool = new ObjectPool; // MyClass 형의 객체로 클래스 할당 _pool.allocation( MyClass ); // 풀에 객체 요청하고 변수에 대입 var instance:MyClass = _pool.getInstance(); // 객체를 컨테이너에 붙임 addChild( instance ); // 사용이 끝나면 풀에 객체를 반환함 removeChild( instance ); _pool.returnInstance( instance ); |
pool 에다가 getInstance() 해서 객체를 새로 받아오다니, 마치 손안대고 코를 푸는것 같은 느낌같지 않습니까?
E. 객체 재사용의 효과
객체를 재사용하게 되면 다음의 몇 가지 긍정적인 효과를 기대해 볼 수 있습니다.
- 런타임 성능 향상 (Improve run-time performance)
- 메모리 누출 방지 (Prevent memory leak)
- 안정적인 동작 (Running stable)
E-1. 런타임 성능 향상
애플리케이션이 시작, 초기화 하면서 사용될 객체를 모두 만들어 놓은 경우라면, 런타임에 new 를 연산하느라 버벅일 필요가 없겠죠. 따라서 런타임에 성능 향상이 일어날 수 있습니다. 동적으로 풀을 구성한 경우라도 어쨌건 무분별하게 객체를 생성할 가능성이 줄어들므로 마찬가지로 성능 향상을 꾀할 수 있겠습니다.
E-2. 메모리 누출 방지
어딘가에 버려졌지만 addEventListener 가 달려있다는 죄로 GC(가비지 컬렉션)가 수집도 안해가는 불쌍한 객체들이 없어지게 되므로, 메모리 누출 현상이 획기적으로 줄어들 가능성이 있습니다.
E-3. 안정적인 동작
쓸데 없이 객체를 생성하지 않고 계속 재사용 하게 되므로, GC가 돌아도 별로 수거할 것이 없게 될 가능성이 높습니다. “가비지 컬렉션에 대한 이해” 편에서 설명한것과 같이 GC가 돌게 되면 (특히 플래시 플레이어 같이 싱글 쓰레드라면 더욱더) 모든 동작을 멈추고 GC를 돌리게 되므로 플래시 애플리케이션이 순간적으로 멈칫거리는 현상이 발생하게 되는데, 오브젝트 풀을 사용한다면 GC가 할일이 거의 없게 되므로 전체적으로 랙이 줄어드는 안정적인 동작을 기대해 볼 수 있습니다.
그런가 하면
- 전체적인 메모리 사용 총량은 약간 올라가는 경향이 있고,
- 에러발생율이 낮아지는 효과가 있습니다. (특히 null 객체 에러)
- 또한 GC를 사용 하지 않는 것을 전제로 하기 때문에 메모리 관리를 개발자가 직접 해야 하는 귀찮음 정도는 감수해야겠죠. (머릿속으로 메모리를 계산)
이렇게 액션스크립트에서의 객체 재사용(재활용) 과 오브젝트 풀에 관한 세 개의 연속 포스팅을 살펴보았습니다. 객체를 재사용 하는 것 정도는 객체지향 프로그래밍을 하는 개발자들에게는 기본기라고 할 수 있을 정도 이므로 여러분들도 꼭 알아두시고, 실천에 옮겨, 프로그램의 수준을 한 단계 높이길 바라겠습니다.
Blog under the Creative Commons Attribution-NoDerivs 3.0 License
12월 6th, 2010 on am 9:40
와우… 좋은 글 항상 잘 읽고 가네요.ㅎㅎ
항상 객체의 재사용성에 대해 말로만 들었지 실제로 어떠한 식으로
구현이 되는지는 잘 모르고 있었는데.. 오늘도 월욜 아침부터 좋은 정보
알아가네요.ㅎㅎ 감사합니다~ ^^
12월 8th, 2010 on pm 2:17
가벼운 애플리케이션은 배열과 같은 컬렉션을 이용한 간이 오브젝트 풀로도 충분히 좋은 효과를 낼 수 있습니다. 보통 플래시는 가벼운 애플리케이션인 경우가 많기 때문에, 더 잘 맞아 떨어진다고도 할 수 있죠.
4월 3rd, 2011 on pm 12:16
좋은 글 잘 보고갑니다
읽고 싶은 포스트가 너무 많지만 지금은 너무 바빠서 ㅜㅜ
졸업작품만 끝난다면 한동안 여기와서 공부를 하고 가야겠습니다 ~~
정말 좋은 정보 알려주셔서 감사드립니다 ~~
4월 3rd, 2011 on pm 12:59
지금 작업하시는 쇼핑몰과 같이 대규모 서비스를 고려한 RIA 애플리케이션을 만든다면 오브젝트 풀은 거의 필수라고 봐야 할겁니다.
졸업작품용이 아닌 실제 서비스도 할 수 있게 잘 만들어 주세요 ㅎㅎ
4월 13th, 2011 on pm 5:22
잘 읽고 갑니다.
아직 공부중이라 구체적인 이해는 안되었지만
전반적인 이해를 잘 했습니다~
감사합니다!!
5월 2nd, 2011 on pm 2:19
글 잘 읽었습니다. Flyweight패턴과 비슷한 개념인것 같네요
5월 3rd, 2011 on pm 7:53
오랜만에 들러주셨네요 ^^
6월 1st, 2011 on pm 5:40
이번 프로젝트에서 객체풀을 사용하려고 기존것 그냥사용하긴 싫어서
polygonal 의 라이브러리를 펼쳐놓고 보다가 씁쓸한미소를 짓다 검색했는데..
이해하기 쉽게 풀어 주셔서 잘 보고 갑니다.^^
위의 클래스 수정해서 프로젝트에 사용해도 될까요?
@author부분은 그대로 놓겠습니다.
6월 2nd, 2011 on am 12:53
본문에도 써 놓았듯이 누구나 자유롭게 사용하실 수 있습니다. ^^
최근에 이 오브젝트 풀을 기본으로 풀 리스트를 관리할 수 있는 클래스를 만들어 프로젝트에 사용하고 있는데, 그에대해서 조만간 관련 포스팅을 할 예정이라는것, 알려드립니다.
6월 2nd, 2011 on am 9:37
감사합니다.^^
안그래도 객체풀사용시에 풀을 하나로 모두 사용은 힘들것 같아서..
기존에 데이터를 묶어쓰던 리스트 처럼 모으거나
컴포넌트에서 개별적으로 사용하는것 외에 범용으로 쓸것을 따로 구분지어야
겠다고 생각하고 있었습니다. 다음 포스팅도 기대가 되네요 ^^
풀에 저장하는것도 좋은데..
풀에서 검색하는 부분이 중요할듯해서..
계속 고민해 봐야겠습니다 ^^