책정리/GoF 디자인 패턴

14장 객체의 공유 문제 : Flyweight 패턴

GONII 2019. 1. 30. 22:25
    프로그램을 개발하다 보면 크게 가지 목적에 '공유' 고려하게 된다. 하나는 논리적인 관점에서 공유가 필요한 경우고, 다른 하나는 구현 측면에서 컴퓨터 자원을 효율적으로 사용하기 위해 공유가 필요한 경우다. 전자의 대표적인 예로는 데이터베이스나 파일 등의 공유에서처럼 동일한 데이터나 자원을 공유하는 경우를 있고, 후자의 대표적인 예로는 동일한 값이나 데이터에 대해 포인터 변수 등을 통해 공유를 수행하는 경우를 있다.
    여기서 공유를 수행하는 가지 목적 논리적 관점에 따른 공유는 보통 논리 모델에 기반해서 공유될 객체가 별도로 정의되기 때문에 객체를 사용하는 형태로만 설계를 하면 공유에 따른 문제는 쉽게 해결될 있다. 반면, 컴퓨터 자원 사용 효율화를 위한 구현 관점에서의 공유는 논리적으로는 공유되지 않는 것들을 자원의 효율적 사용을 위해 공유를 수행하는 것이므로 설계 시에 많은 고려를 필요로 한다.
    • 문제 사례 설명
    휴대폰에서 간단한 슈팅 게임 개발
    적군과 아군으로 구분, 아군은 한명인데 반해 적군은 게이머의 수준에 따라 여러 종류가 등장할 있고, 종류마다 생성되는 적군의 수도 여러명일 있다. 적군은 상황이나 이벤트에 따라 정해진 동작을 수행하게 되는데 이러한 동작들은 이미지 형태로 표현될 것이다.
    여기서 문제는 적군의 동작을 표현하기 위해 필요한 이미를 어떻게 관리할 것인가다. 휴대폰의 특성상 사용할 있는 메모리의 양이 한정되어 있기 때문에 적군의 수가 늘어난다고 하더라도 이상 메모리를 사용하지 못하는 문제에 부딪힐 있다.
    • 다양한 접근 방법 및 FLYWEIGHT 패턴
    문제를 요약하자면 논리적으로 개별 적군 객체들마다 이미지 정보를 저장, 관리하게 하는 것이 바람직하나, 이는 한정된 메모리 공간을 가진 휴대폰에 탑재될 프로그램에는 적합하지 않은 방법이라는 것이다. 따라서 우리가 해결해야 하는 문제는 어떻게 하면 논리적으로는 개별 객체들마다 이미지 정보가 저장, 관리되는 것처럼 보이면서도 실제로는 그만큼 많은 메모리를 사용하지 않고 프로그램이 동작할 있도록 만들어 것인가 하는 것이다.
    • 기본적인 방법: 화면 관리 객체 정의 및 활용 방식
    이미지는 다른 정보보다 많은 메모리 공간을 필요로 한다. 따라서 개별 객체마다 자신의 현재 상태에 따른 이미지를 따로 저장, 관리하게 되면 필요로 하는 메모리 공간이 대폭 늘어날 밖에 없다.
    개별 객체들이 이미지를 저장, 관리하는 목적은 이미지를 화면에 표시하기 위해서다. 그러나 개별 객체들이 저장, 관리하는 이미지 모두가 한꺼번에 화면에 표시되지는 않는다. 왜냐하면 화면의 크기는 고정적이므로, 화면에 표시할 있는 객체의 수도 제한적인 밖에 없기 때문이다.
    이러한 사실을 감안한다면, 개별 객체 단위로 화면에 표시할 이미지를 저장, 관리할 것이 아니라, 전체 화면 차원에서 이미지 정보를 저장, 관리하는 것이 보다 적은 메모리를 사용하는 가지 방법이 있을 것이다. 특히 방법은 객체의 개수가 아무리 늘어난다 하더라도 전체 화면의 크기가 변하지 않는 필요로 하는 메모리 공간은 고정적이기 때문에 객체의 개수가 늘어날수록 메모리 공간 절약 비율은 증가하게 되어 효과적일 것이다. 물론 이런 아이디어를 적용하기 위해서는 전체 화면 차원에서 이미지 정보를 저장, 관리해주는 객체를 별도로 정의해서 사용하는 것이 바람직할 것이다.
    방법이 제대로 동작하려면 개별 객체들이 현재 화면 상에 보여져야 할지 여부와 보여지더라도 어떤 부위가 얼마나 보여질 것인지와 같은 것들이 일일이 계선되어야 한다. 또한 이런 계산 결과를 바탕으로 전체 화면이 관리되어야 한다. 이런 계산이 필요한 이유는 전체 화면 이미지를 관리하는 객체가 현재 화면에 보여지는 이미지 정보만 관리할 어떤 객체들이 어떻게 포함되었는지를 일일이 기억하고 있지 않을 것이기 때문이다.
    • 패턴 활용 방법: Flyweight 패턴
    많은 계산을 수행하지 않기 위해서는 화면에 보여줄 이미지 정보를 계산에 의해 생성할 것이 아니라, 어떤 식으로든 미리 저장, 관리하고 있을 밖에 없다. 그렇지만 메모리 공간 사용량을 줄이기 위해서는 미리 저장, 관리해야 이미지를 최소화시켜야 것이다. 가지 목적을 모두 만족시킬 있는 방법은 이미지 정보를 개별 객체들이 공유하게 만드는 것이다.
    이미지 정보를 공유하기 위해서는 먼저 공유 가능한 정보와 그렇지 않은 정보를 분리할 필요가 있다. 예를 들어 동일한 종류의 객체들이 움직이는 동작에 관련된 비트맵 정보는 모든 객체들이 공유 가능할 것이다. 반면, 이들 비트맵 정보가 화면 상에 표시될 위치 정보는 객체마다 다를 것이므로 공유 불가능할 것이다. 또한 객체별로 현재 어떤 동작을 수행하는 상태인지에 대한 정보도 공유 불가능할 것이다.
    이처럼 공유 가능한 정보와 그렇지 않은 정보가 분리되었으면, 다음으로 정보들을 저장, 관리할 객체를 정의해야 한다. 이때 공유 불가능한 정보는 개별 객체들에게 저장, 관리시키면 되나, 공유 가능한 정보는 별도의 객체를 정의해서 저장, 관리시켜야 것이다. 이런 목적으로 정의되는 객체를 Flyweight 객체라고 한다. 여기서 공유 가능한 정보는 Flyweight 객체의 내부에 저장, 관리되기 때문에 Intrinsic State라고 하고, 공유 불가능한 정보는 Flyweight 객체 외부에 저장 관리되기 때문에 Extrinsic State라고 한다.


     


    [그림 14-1] [그림 14-2] 같은 공유 방법에 따라 이미지 정보를 공유하기 위한 클래스 구조와 실제 이미지 정보가 공유되는 객체 관계도를 보일 것이다. 여기서 Flyweight 객체에 해당하는 것은 EnemyImage 하위 클래스에 의해 생성되는 객체이며, 이들 객체들은 [그림 14-2]에서 보듯이 여러 Enemy 객체들에 의해 공유된다. 한편 Flyweight 객체 내부에는 Intrinsic State 비트맵 이미지가 저장, 관리되는 반면 Enemy 객체에는 실제 비트맵 이미지가 어느 위치에 표시될지에 대한 좌표 값만이 Extrinsic State 형태로 저장, 관리될 것이다.
    이처럼 정보를 공유하기 위해 공유 가능한 정보와 그렇지 않은 정보를 분리하고 공유 가능한 정보를 객체 형태로 정의해서 정보 공유를 수행하는 형태의 설계를 Flyweight 패턴이라고 한다. Flyweight 패턴은 객체를 단위로 정보 공유를 수행하는 설계 형태를 가리킨다.
    • 샘플 코드
    [소스 14-1] 휴대폰 게임에 대한 Flyweight 패턴 적용 샘플 코드
    #include <iostream>
    using namespace std;
     
    class EnemyImage
    {
    public:
    virtual void Display(int x, int y) = 0;
    protected:
    EnemyImage(){}
    EnemyImage(const EnemyImage& rhs);
    };
     
    class EnemyNoActionImage : public EnemyImage
    {
    public:
    static EnemyImage* CreateInstance()
    {
    if (pInstance_ == 0)
    {
    pInstance_ = new EnemyNoActionImage;
    cout << "EnemyNoActionImage" << endl;
    }
    return pInstance_;
    }
     
    void Display(int x, int y)
    {
    // x, y 위치에 비트맵 이미지 표시
    }
    protected:
    EnemyNoActionImage(){}
    EnemyNoActionImage(const EnemyNoActionImage& rhs);
    static EnemyImage* pInstance_;
    };
    EnemyImage* EnemyNoActionImage::pInstance_ = 0;
     
    class EnemyMoveImage : public EnemyImage
    {
    public:
    static EnemyImage* CreateInstance()
    {
    if (pInstance_ == 0)
    {
    pInstance_ = new EnemyMoveImage;
    cout << "EnemyAttackImage" << endl;
    }
    return pInstance_;
    }
     
    void Display(int x, int y)
    {
    }
    protected:
    EnemyMoveImage(){}
    EnemyMoveImage(const EnemyMoveImage& rhs);
    static EnemyImage* pInstance_;
    };
    EnemyImage* EnemyMoveImage::pInstance_ = 0;
     
    class EnemyAttackImage : public EnemyImage
    {
    public:
    static EnemyImage* CreateInstance()
    {
    if (pInstance_ == 0)
    {
    pInstance_ = new EnemyAttackImage;
    cout << "EnemyAttackImage" << endl;
    }
    return pInstance_;
    }
     
    void Display(int x, int y)
    {
    }
    protected:
    EnemyAttackImage(){}
    EnemyAttackImage(const EnemyAttackImage& rhs);
    static EnemyImage* pInstance_;
    };
    EnemyImage* EnemyAttackImage::pInstance_ = 0;
     
    class EnemyDieImage : public EnemyImage
    {
    public:
    EnemyImage* CreateInstance()
    {
    if (pInstance == 0)
    {
    pInstance = new EnemyDieImage;
    cout << "EnemyDieImage" << endl;
    }
    return pInstance;
    }
    void Display(int x, int y)
    {}
     
    protected:
    EnemyDieImage(){}
    EnemyDieImage(const EnemyDieImage& rhs);
    static EnemyImage* pInstance;
    };
    EnemyImage* EnemyDieImage::pInstance = 0;
     
    class Enemy
    {
    public:
    Enemy(int x, int y)
    {
    curX_ = x, curY_ = y;
    pCurImage_ = EnemyNoActionImage::CreateInstance();
    }
     
    void Move(int x, int y)
    {
    curX_ = x, curY_ = y;
    pCurImage_ = EnemyMoveImage::CreateInstance();
    }
     
    void Attack()
    {
    pCurImage_ = EnemyAttackImage::CreateInstance();
    }
     
    void Display()
    {
    pCurImage_->Display(curX_, curY_);
    }
     
    protected:
    int curX_, curY_;
    EnemyImage* pCurImage_;
    };
     
    void main()
    {
    Enemy e1(10, 10), e2(20, 20);
    e1.Move(30, 30);
    e2.Attack();
    e2.Move(40, 40);
    }
    [소스 14-1]에서 Enemy 객체들은 pCurImage_ 데이터 멤버를 통해 자신이 참조하는 이미지 정보를 가리키고 있다. 한편 pCurImage_ 데이터 멤버에 의해 가리켜지는 이미지 객체는 Enemy 객체마다 매번 생성되는 같지만, 실제로는 EnemyImage 하위 클래스들이 모두 Singleton 패턴을 적용하고 있기 때문에 동일한 이미지 정보를 공유하는 형태로 되어 있다.
    Singleton 패턴이 적용 가능한 것은 이미지 정보의 경우 동일한 정보를 가진 객체는 1개만 존재하면 충분히 공유 가능하기 때문이다. 만약 성능 향상을 위해 동일한 정보를 가진 여러 개의 객체를 만들어주고 객체에게 몰리는 부하를 분산하고 싶다면 Singleton 패턴이 아닌 다른 형태로 공유되는 객체들을 관리하는 것이 필요할 수도 있을 것이다. 이럴 경우에는 공유되는 객체를 생성하기 위해 Factory Method 패턴 등이 적용될 수도 있을 것이다.
    • 구현 관련 사항
    Flyweight 패턴의 구현과 관련해서 가장 먼저 고려해야 것은 과연 Flyweight 패턴을 해당 문제에 적용할 것인지 여부를 어떻게 판단할 것인가다. 이때 객체 공유를 통해 얼마나 많은 자원을 절약하게 될지 여부는 공유하려는 객체가 Intrinsic State Extrinsic State 얼마나 쉽게 분리될 있고, 그에 따라 절약되는 자원 양이 얼마나 되는지에 달려 있다.
    만약 Intrinsic State Extrinsic State 쉽게 분리되지 않거나 분리된다 하더라도 서로간의 관계를 유지하기 위해 다른 자원을 필요로 한다면 Flyweight 패턴을 사용해서 객체를 공유하는 것이 오히려 부담만 가중시킬 별다른 이득이 되지 않을 있다.
    Flyweight 패턴을 적용함에 있어 공유할 객체를 생성하는 것은 Client 직접 하지 않는 것이 좋다. 왜냐하면 Client 직접 객체를 생성하게 되면 객체들간의 공유가 제대로 이루어지지 않거나 객체를 생성할 때마다 공유가 이루어지도록 Client 일일이 신경을 써야 것이기 때문이다.
    이런 문제를 해결하기 위한 방법으로는 공유하는 객체를 대신 생성하고, 관리해줄 객체를 정의하거나 공유되는 객체 자체가 한정된 개수만큼 자기 자신을 생성하고, 공유가 일어나도록 관리하는 방법이 있다. 여기서 전자의 방식의 대표적인 형태가 Factory Method 패턴이며, 후자 방식이 Singleton패턴 것이다.
    그리고 이상 객체를 사용하지 않는다면 객체를 소멸시켜주는 것이 보다 많은 메모리 공간을 가용하게 만들어 것이다. 여기서 문제는 공유 객체를 소멸시킬 소멸되는 객체를 참조하는 객체가 이상 없다는 것을 보장할 있어야 한다는 사실이다. 그렇지 않다면 다른 객체가 참조하는 공유 객체를 소멸시켜 프로그램이 오작동을 일으킬 가능성도 있다.
    이를 위한 대표적인 방법이 Reference Counting 기법이다. 기법은 공유 객체의 입장에서 자신을 참조하는 객체가 늘어난다면 Reference Count 증가시키고, 자신을 참조하는 객체가 줄어들면 Reference Count 감소시켜나가는 형태로 자기 자신을 참조하는 객체의 수를 관리하는 방식이다. 공유 객체가 이런 값을 내부적으로 관리하게 되면 실제 객체를 소멸할지 여부는 자기 자신을 참조하는 객체의 개수가 0인지 여부에 따라 판단이 가능해지게 된다.
    • FLYWEIGHT 패턴 정리
    Flyweight 패턴은 객체 공유를 통해 자원 사용량을 줄이기 위한 설계라고 있다. Flyweight 패턴은 공유되는 객체를 생성하는 방식에 따라 크게 Factory Method패턴을 같이 적용하는 경우와 Singleton패턴을 적용하는 경우로 나뉠 있다.


    [그림 14-3]에서 Flyweight 객체들은 FlyweightFactory 클래스에 의해 생성되며, 클래스는 Flyweight 객체들의 (Pool) 관리한다. 다만 그림에서 특이한 사항은 공유 객체를 위한 클래스 뿐만 아니라 비공유 객체를 위한 클래스도 Flyweight 하위 클래스로 정의되어 있다는 점이다. 같이 클래스 구조를 설계한 이유는 Client 입장에서 동일한 인터페이스로 접근 가능하도록 만들어주기 위해서다.
    [그림 14-3] 클래스 구조에 의해 생성될 있는 객체들간의 관계도를 예로 그려보면 [그림 14-4] 같을 것이다. aFlyweightFactory 객체는 개의 aConcreteFlyweight 객체를 생성하였고 aClient 객체들은 이들을 공유하고 있다.
    Flyweight패턴이 유용한 경유
    • 응용 프로그램이 많은 객체를 필요로 하는데 사용 가능한 자원이 한정되어 있을 경우
    • 많은 객체의 사용으로 저장 공간에 부담이
    • 객체의 상태 대부분이 Extrinsic State 만들어질 있을
    • Extrinsic State 부분을 제외하고, 많은 그룹의 객체들이 상대적으로 적은 공유 객체들로 대체 가능할
    • 응용 프로그램이 논리적으로 객체들을 서로 구분할 필요가 없을
    Flyweight패턴의 특성
    • Flyweight패턴을 적용할 경우 Extrinsic State 찾거나 계산하는 일반적인 경우보다 실행 시간 비용이 많이 발생할 있다. 그러나 저장 공간이 절약되는 점과 많은 객체를 다루어야 하는 문제를 해결할 있다는 점에서 같은 실행 시간 비용은 상쇄될 있을 것이다.
    • Flyweight 패턴을 적용할 경우 저장 공간이 절약되는 정도는 다음의 요소들에 의해 결정된다.
      • 객체를 공유함으로써 감소되는 객체의 개수
    많은 객체들이 공유되어 질수록 많은 저장 공간이 절약될 있다.
    • 객체당 Intrinsic State
    Intrinsic State 양이 많을수록 공유가 이루어지면 절약되는 공간의 양도 늘어난다.
    • Extrinsic State 계산되는 것이냐 저장되는 것이냐에 따라 필요한 기억 공간
    State 저장되지 않고 계산될 있으면 많은 저장 공간을 절약할 있다.
    • Flyweight 패턴은 객체들을 공유하므로, 만약 객체들간 동일성 여부 테스트가 프로그램 내에서 사용될 경우에는 개념적으로 서로 다른 객체라 하더라도 동일한 것으로 판단할 있기 때문에 문제의 소지가 된다. 따라서 Flyweight 패턴을 사용하는 프로그램은 객체들간 동일성 여부 테스트를 사용하지 않아야 한다.
반응형