블로그 내 검색

2012. 2. 28.

자바스크립트 패턴 #3 - 플라이웨이트 패턴 (Flyweight Pattern)

자바스크립트 라이브러리 레벨에서 자주 쓰이는 플라이웨이트 패턴(Flyweight Pattern)은 사용시 굉장한 자원의 절약과 메모리의 낭비를 막아줍니다.

자바스크립트는 따로 C나 Java같은 컴파일 과정이 없어(현대식의 자바스크립트 엔진, 가령 구글 크롬의 V8엔진의 경우 컴파일 비슷한 과정과 재정렬을 한다고 들었던 것 같습니다만) 프로그램 시 프로그래머가 직접 컴파일러가 해주는 코드의 효율화에 더욱 신경써야 합니다.

플라이웨이트 패턴이란 프로그래밍시에 많은 수의 유사성을 가진 객체들이 생성되어질 때, 그 객체끼리는 공통된 부분과 공통되지 않은 부분을 분리하여 관리하여 공통되는 부분을 객체들이 공유하도록 하는 패턴입니다.

그리고 한번 생성된 공유 객체는 저장하여 재생성을 (풀링이나 캐싱 등으로)막아 메모리 사용을 줄입니다.

  • 공유되는 부분은 이미 생성되었다면 생성된 것을 공유하고, 아니라면 생성하고 공유함.
  • 한번이라도 생성한 객체는 다시 생성하지 않음

이때 객체를 생성하는 방법으로 팩토리를 이용하는 것이 일반적이지만, 객체 자신이 제공하는 특정 메서드를 통해 직접 공유 자원을 자신이 참조하는 방식이 되기도 합니다.

말로 하니까 굉장히 어렵습니다.

일단 어떠한 방식으로 구현하더라도 다음과 같은 개념들을 구현하게 됩니다.

Factory
객체를 제공하는 팩토리 구현입니다. 프로그램에서 쓰일 객체들을 공급합니다.

Intrinsic
객체들이 공유하는 본연의 특성들을 모은 것입니다.
공유되는 부분이죠.
간혹 불변(immutable) 이 되기도 합니다.

Extrinsic
Intrinsic 부분 외의 서로 다른 외부 특성입니다.

플라이웨이트 패턴에서 제일 많이 예제로 표현되는 것은 문서 편집기의 구현입니다.
편집기 구현때 문자 객체는 문서가 몇페이지가 넘어가든 단 1개만 생성되고, 그것에 대한 참조만을 가지고 위치를 달리하여 문서를 구성하게 됩니다. 실제 문자 하나하나 생성하다간 문서가 몇페이지 넘어가지 않고 Out of Memory를 볼 수 있겠지요.

또 다른 하나는 Java 언어의 스트링(String) 구현입니다.
스트링이 기본 타입으로 생성될 때, (즉 String h = "hello" 같은) JVM은 특정한 내부 팩토리 함수를 호출합니다. 팩토리 함수는 자신이 가진 플라이웨이트 사전을 뒤져 이미 있다면 그것을 리턴하고 없다면 내부적으로 스트링을 생성하여 사전에 넣고 리턴합니다.
// 두 String은 변수 같은 String 인스턴스를 참조한다.
String msg1 = "Hello world1";
String msg2 = "Hello world1";

if(msg1 == msg2 && msg1.equals(msg2)) {
    System.out.println("두 문자열은 완전히 동일하다");
}
String 객체는 훌륭한 플라이웨이트 패턴 구현이지요.

이제 자바스크립트 코드로 구현해 봅시다.

친숙한 게임인 스타크래프트를 예로 들어보겠습니다.
A 유저와 B 유저가 대결합니다. 그리고 게임이 시작되었고 서로 같은 종족인 저그를 선택했다고 가정합니다.

유닛의 구조를 다음과 같이 잡아봅시다.
var Unit = function(name, maxhp, cost, player, upgrade) {
    this.name = name;
    this.maxhp = maxhp;
    this.cost = cost;
    this.player = player;
    this.upgrade = upgrade;
    this.curentHp = maxhp;
}
Unit.prototype = {
    move: function() { ... },
    attack: function() { ... },
    defence: function() { ... }
}
서로의 유닛은 같은 객체입니다.
A가 생산한 히드라리스크와 B가 생산한 히드라리스크는 유닛이름, 최대체력, 생산비용이 같겠지요. 게임은 공정해야 하니까요.

이 부분은 그럼 공통 속성 (Intrinsic) 이 됩니다.

하지만 조종하는 플레이어와, 그 플레이어가 적용한 업그레이드 레벨은 다를 것입니다.
이 부분은 외부 특성 (Extrinsic)이 됩니다.

본격적으로 패턴을 적용해 봅시다
외부 특성은 빼고 공통 속성만 남겨보면 다음과 같이 됩니다.
var Unit = function(name, maxhp, cost) {
    this.name = name;
    this.maxhp = maxhp;
    this.cost = cost;    
}
이제 공통 속성을 관리해주면서 유닛을 생산할 팩토리 객체를 만듭니다.
모듈 패턴을 사용합니다.
var UnitCreator = (function() {
    
    // 한번 생성한 객체는 다시 생성하지 않기 위한 저장소
    // private 이다.
    var units = {};

    // 모듈 패턴을 사용한 public 속성들
    return {
        create: function(name, maxhp, cost) {
 
            var unit = units[name];

            // 저장소에 이미 있다면 그것을 리턴
            if(unit) {
                return unit;
            }

            // 없다면 생성하고 저장소에 저장
            else {
                units[name] = new Unit(name, maxhp, cost);
                return units[name];
            }
        }
    } 
    
})();
이제 게임에서 유닛의 조종자와 각종 기능을 수행할 컨트롤러를 만듭니다.
var UnitController = (function() {
    
    // 플라이웨이트 객체를 관리할 맵
    // 역시 private 속성.
    var unitsPlayedDictionary = {};

    var self = this;

    // 모듈 패턴을 사용한 public 속성들
    return {
        produceUnit: function(id, name, maxhp, cost, player) {
            
            var unit = UnitCreator.create(name, maxhp, cost);
            
            // 공유 자원과 객체 특성을 조합 (컴포지트 패턴)
            var unitOnPlay = {
                unit: unit,
                id: id,
                player: player,
                currentHp: unit.maxHp,
                upgrade: 0                
            };
            unitsPlayedDictionary[id] = unitOnPlay;
        },     

        kill: function(id) {
            var u = unitsPlayedDictionary[id];
            if(unit) {
                u.currentHp = 0;
                delete unitsPlayedDictionary[id];
            }
        },
        
        upgradePower: function(id) {
            var u = unitsPlayedDictionary[id];
            if(unit) {
                u.upgrade = u.upgrade + 1;
            }
        },
        
       /* 기타 메서드들 ... */
    }    
    
})();
위에 든 예제가 좀 허접합니다. 기회가 된다면 좀더 좋은 예제를 들어 설명해보고 싶네요.

패턴을 잘 살펴보면 몇가지 패턴이 모여 플라이웨이트 패턴을 만드는 것을 볼 수 있습니다. (모듈 패턴, 컴포지트 패턴)

유명 자바스크립트 라이브러리인 ExtJs, jQuery는 이 플라이웨이트 패턴을 적극적으로 활용하여 속도 및 메모리 효율성을 끌어올리고 있습니다.

공통된 속성은 라이브러리가 제공하는 유용한 기능이 되겠지요.

플라이웨이트 패턴은 분명 컴퓨터 입장에서는 효율이 좋습니다.
그러나 프로그래머 입장에서는 굉장히 불편한 패턴입니다. 과용시 코드의 가시성이 굉장히 떨어지며 유지보수가 힘든 스파게티 코드가 될 위험이 많습니다.

디자인 패턴이란 언제나 과용은 금물이죠;

포스팅하며 참고한 사이트들입니다.

댓글 없음:

댓글 쓰기