Flyweight

chris·2021년 7월 17일
0

design pattern

목록 보기
10/11

Intent

Flyweight는 각 객체의 모든 데이터를 유지하는 대신 여러 객체간에 공통 상태 부분을 공유하여 사용 가능한 RAM 양에 더 많은 객체를 맞출 수 있는 Structural design pattern입니다.

Problem

긴 근무 시간 후에 재미를 느끼기 위해 간단한 비디오 게임을 만들기로 결정했습니다. 플레이어는 지도를 돌아다니며 서로를 쏘게 됩니다. 사실적인 입자 시스템을 구현하고 이를 게임의 독특한 기능으로 만들기로 결정했습니다. 폭발로 인한 방대한 양의 총알, 미사일 및 파편이 맵 전체를 날아다니며 플레이어에게 스릴 넘치는 경험을 선사해야 합니다.

완료되면 마지막 커밋을 푸시하고 게임을 빌드하고 테스트 드라이브를 위해 친구에게 보냈습니다. 게임이 당신의 컴퓨터에서 완벽하게 실행되고 있었지만 당신의 친구는 오랫동안 게임을 할 수 없었습니다. 그의 컴퓨터에서 게임은 몇 분의 게임 플레이 후에도 계속 충돌했습니다. 디버그 로그를 파헤치는 데 몇 시간을 보낸 후 RAM이 충분하지 않아 게임이 중단되었음을 발견했습니다. 친구의 장비가 당신의 컴퓨터보다 훨씬 덜 강력하다는 것이 밝혀졌고, 그래서 그의 컴퓨터에서 문제가 너무 빨리 나타났습니다.

실제 문제는 입자 시스템과 관련되었습니다. 총알, 미사일 또는 파편 조각과 같은 각 입자는 많은 데이터를 포함하는 별도의 개체로 표시되었습니다. 어느 시점에서 플레이어 화면의 대학살이 절정에 이르렀을 때 새로 생성된 입자가 더 이상 나머지 RAM에 맞지 않아 프로그램이 충돌했습니다.

Solution

Particle 클래스를 자세히 살펴보면 색상 및 sprite 필드가 다른 필드보다 훨씬 더 많은 메모리를 사용한다는 것을 알 수 있습니다. 더 나쁜 것은 이 두 필드가 모든 입자에 걸쳐 거의 동일한 데이터를 저장한다는 것입니다. 예를 들어, 모든 총알은 동일한 색상과 스프라이트를 갖습니다.

좌표, 이동 벡터 및 속도와 같은 입자 상태의 다른 부분은 각 입자에 고유합니다. 결국 이러한 필드의 값은 시간이 지남에 따라 변경됩니다. 이 데이터는 입자가 존재하는 항상 변화하는 컨텍스트를 나타내는 반면 색상과 스프라이트는 각 입자에 대해 일정하게 유지됩니다.

객체의 이 상수 데이터를 일반적으로 고유 상태라고 합니다. 그것은 물체 안에 산다. 다른 개체는 읽을 수만 있고 변경할 수는 없습니다. 종종 다른 객체에 의해 "외부에서" 변경되는 나머지 객체 상태를 외부 상태라고 합니다.

플라이웨이트 패턴은 객체 내부에 외부 상태 저장을 중단할 것을 제안합니다. 대신 이 상태를 의존하는 특정 메서드에 이 상태를 전달해야 합니다. 고유 상태만 개체 내에 유지되므로 다른 컨텍스트에서 재사용할 수 있습니다. 결과적으로 이러한 객체는 외부보다 변형이 훨씬 적은 고유 상태에서만 다르기 때문에 더 적은 수의 객체가 필요합니다.

우리 게임으로 돌아가자. 입자 클래스에서 외부 상태를 추출했다고 가정하면 총알, 미사일, 파편의 세 가지 다른 개체만 게임의 모든 입자를 나타내기에 충분합니다. 지금쯤 짐작하셨겠지만 고유 상태만 저장하는 객체를 Flyweight라고 합니다.

Extrinsic state storage

외적 상태는 어디로 이동하는가? 일부 클래스는 여전히 저장해야 합니다. 맞죠? 대부분의 경우 패턴을 적용하기 전에 개체를 집계하는 컨테이너 개체로 이동합니다.

우리의 경우, 그것은 파티클 필드에 모든 Particles을 저장하는 메인 Game 객체입니다. 외부 상태를 이 클래스로 이동하려면 각 개별 입자의 좌표, 벡터 및 속도를 저장하기 위한 여러 배열 필드를 만들어야 합니다. 하지만 그게 다가 아닙니다. 입자를 나타내는 특정 플라이웨이트에 대한 참조를 저장하려면 다른 배열이 필요합니다. 동일한 인덱스를 사용하여 입자의 모든 데이터에 액세스할 수 있도록 이러한 배열은 동기화되어야 합니다.

보다 우아한 솔루션은 플라이웨이트 개체에 대한 참조와 함께 외부 상태를 저장하는 별도의 컨텍스트 클래스를 만드는 것입니다. 이 접근 방식을 사용하려면 컨테이너 클래스에 단일 배열만 있으면 됩니다.

잠깐만! 초기에 가졌던 것처럼 이러한 컨텍스트 객체를 많이 가질 필요가 없을까요? 기술적으로 그렇습니다. 그러나 문제는 이러한 개체가 이전보다 훨씬 작다는 것입니다. 가장 메모리를 많이 사용하는 필드가 몇 개의 플라이웨이트 개체로 이동되었습니다. 이제 천 개의 작은 컨텍스트 개체가 데이터의 천 개의 복사본을 저장하는 대신 단일 중량 플라이웨이트 개체를 재사용할 수 있습니다.

Flyweight and immutability

동일한 플라이웨이트 객체가 다른 컨텍스트에서 사용될 수 있으므로 해당 상태를 수정할 수 없는지 확인해야 합니다. 플라이웨이트는 생성자 매개변수를 통해 상태를 한 번만 초기화해야 합니다. setter나 public 필드를 다른 객체에 노출해서는 안 됩니다.

Flyweight factory

다양한 플라이웨이트에 보다 편리하게 액세스하기 위해 기존 플라이웨이트 개체 풀을 관리하는 팩토리 메서드를 만들 수 있습니다. 이 메서드는 클라이언트에서 원하는 플라이웨이트의 고유 상태를 수락하고 이 상태와 일치하는 기존 플라이웨이트 개체를 찾고 발견되면 반환합니다. 그렇지 않은 경우 새 플라이웨이트를 생성하여 풀에 추가합니다.

이 방법을 배치할 수 있는 몇 가지 옵션이 있습니다. 가장 눈에 띄는 장소는 플라이웨이트 컨테이너입니다. 또는 새 팩토리 클래스를 생성할 수 있습니다. 또는 팩토리 메소드를 정적으로 만들고 실제 플라이웨이트 클래스 안에 넣을 수 있습니다.

Structure


1. Flyweight pattern은 단지 최적화일 뿐입니다. 적용하기 전에 프로그램이 동시에 메모리에 유사한 개체를 대량으로 보유하는 것과 관련된 RAM 소비 문제가 있는지 확인하십시오. 이 문제가 다른 의미 있는 방법으로 해결되지 않도록 하십시오.

2. Flyweight 클래스에는 여러 객체 간에 공유할 수 있는 original 객체의 상태 부분이 포함되어 있습니다. 동일한 flyweight 객체가 다양한 상황에서 사용될 수 있습니다. Flyweight 내부에 저장된 상태를 intrinsic이라고 합니다. Flayweight의 메소드에 전달된 상태를 extrinsic이라고 합니다.

3. Context 클래스는 모든 원본 객체에서 고유한 외부 상태를 포함합니다. 컨텍스트가 flyweight 객체 중 하나와 쌍을 이루면 original 객체의 전체 상태를 나타냅니다.

4. 일반적으로 original 객체의 동작은 flyweight 클래스에 남아 있습니다. 이 경우 flyweight의 메소드를 호출하는 사람은 외부 상태의 적절한 비트도 메소드의 매개변수에 전달해야 합니다. 다른 한편으로, 동작은 연결된 flyweight를 단순히 데이터 객체로 사용하는 컨텍스트 클래스로 이동할 수 있습니다.

5. Client는 flyweight의 외부 상태를 계산하거나 저장합니다. 클라이언트의 관점에서 flyweight는 일부 컨텍스트 데이터를 메서드의 매개변수에 전달하여 런타임에 구성할 수 있는 템플릿 개체입니다.

6. Flyweight Factory는 기존 flyweight pool을 관리합니다. Factory에서 client는 flyweight를 직접 만들지 않습니다. 대신에 원하는 flyweight의 고유 상태 비트를 전달하여 팩토리를 호출합니다. 팩토리는 이전에 생성된 flyweight를 살펴보고 검색 기준과 일치하는 기존 flyweight를 반환하거나 아무것도 발견되지 않으면 새 flyweight를 생성합니다.

Pseudocode

이 예에서 Flyweight 패턴은 캔버스에 수백만 개의 나무 개체를 렌더링할 때 메모리 사용량을 줄이는 데 도움이 됩니다.

패턴은 기본 Tree 클래스에서 반복되는 고유 상태를 추출하고 이를 플라이웨이트 클래스 TreeType으로 이동합니다.

이제 동일한 데이터를 여러 개체에 저장하는 대신 몇 개의 플라이웨이트 개체에 보관하고 컨텍스트 역할을 하는 적절한 Tree 개체에 연결합니다. 클라이언트 코드는 올바른 개체를 검색하고 필요한 경우 재사용하는 복잡성을 캡슐화하는 플라이웨이트 팩토리를 사용하여 새 트리 개체를 만듭니다.

// The flyweight class contains a portion of the state of a
// tree. These fields store values that are unique for each
// particular tree. For instance, you won't find here the tree
// coordinates. But the texture and colors shared between many
// trees are here. Since this data is usually BIG, you'd waste a
// lot of memory by keeping it in each tree object. Instead, we
// can extract texture, color and other repeating data into a
// separate object which lots of individual tree objects can
// reference.
class TreeType is
    field name
    field color
    field texture
    constructor TreeType(name, color, texture) { ... }
    method draw(canvas, x, y) is
        // 1. Create a bitmap of a given type, color & texture.
        // 2. Draw the bitmap on the canvas at X and Y coords.

// Flyweight factory decides whether to re-use existing
// flyweight or to create a new object.
class TreeFactory is
    static field treeTypes: collection of tree types
    static method getTreeType(name, color, texture) is
        type = treeTypes.find(name, color, texture)
        if (type == null)
            type = new TreeType(name, color, texture)
            treeTypes.add(type)
        return type

// The contextual object contains the extrinsic part of the tree
// state. An application can create billions of these since they
// are pretty small: just two integer coordinates and one
// reference field.
class Tree is
    field x,y
    field type: TreeType
    constructor Tree(x, y, type) { ... }
    method draw(canvas) is
        type.draw(canvas, this.x, this.y)

// The Tree and the Forest classes are the flyweight's clients.
// You can merge them if you don't plan to develop the Tree
// class any further.
class Forest is
    field trees: collection of Trees

    method plantTree(x, y, name, color, texture) is
        type = TreeFactory.getTreeType(name, color, texture)
        tree = new Tree(x, y, type)
        trees.add(tree)

    method draw(canvas) is
        foreach (tree in trees) do
            tree.draw(canvas)

Applicability

프로그램이 사용 가능한 RAM에 거의 맞지 않는 엄청난 수의 객체를 지원해야 하는 경우에만 플라이웨이트 패턴을 사용하십시오.

패턴 적용의 이점은 패턴이 사용되는 방법과 위치에 따라 크게 달라집니다. 다음과 같은 경우에 가장 유용합니다.

  • 응용 프로그램은 수많은 유사한 개체를 생성해야 합니다.
  • 이것은 대상 장치에서 사용 가능한 모든 RAM을 소모합니다.
  • 객체에는 여러 객체 간에 추출 및 공유할 수 있는 중복 상태가 포함되어 있습니다.

How to Implement

  1. Flyweight가 될 클래스의 필드를 두 부분으로 나눕니다.
  • the intrinsic state: 많은 개체에 걸쳐 복제된 변경되지 않는 데이터를 포함하는 필드
  • the extrinsic state: 각 객체에 고유한 컨텍스트 데이터를 포함하는 필드
  1. 클래스의 고유 상태를 나타내는 필드를 그대로 두되, 변경할 수 없는지 확인하십시오. 생성자 내부에서만 초기 값을 가져와야 합니다.

  2. 외부 상태의 필드를 사용하는 방법을 살펴보십시오. 메소드에 사용된 각 필드에 대해 새 매개변수를 도입하고 필드 대신 사용하십시오.

  3. 선택적으로 flyweight pool을 관리하기 위한 팩토리 클래스를 생성합니다. 새 flyweight를 만들기 전에 기존 flyweight를 확인해야 합니다. Factory가 설치되면 client는 factory를 통해서만 flyweight를 요청해야 합니다. 그들은 factory에 고유 상태를 전달하여 원하는 flyweight를 설명해야 합니다.

  4. 클라이언트는 flyweight 객체의 메서드를 호출할 수 있도록 외부 상태(컨텍스트) 값을 저장하거나 계산해야 합니다. 편의상, 플라이웨이트 참조 필드와 함께 외부 상태는 별도의 컨텍스트 클래스로 이동될 수 있다.

Pros and Cons

O. 프로그램에 유사한 개체가 많이 있다고 가정하면 많은 RAM을 절약할 수 있습니다.

X. 누군가가 플라이웨이트 메서드를 호출할 때마다 컨텍스트 데이터의 일부를 다시 계산해야 하는 경우 CPU 주기보다 RAM을 거래할 수 있습니다.

X. 코드가 훨씬 더 복잡해집니다. 새로운 팀원들은 항상 엔티티의 상태가 왜 그런 식으로 분리되었는지 궁금해 할 것입니다.

Relations with Other Patterns

  • Composite 트리의 공유 리프 노드를 Flyweight로 구현하여 RAM을 절약할 수 있습니다.
  • Flyweight는 많은 작은 개체를 만드는 방법을 보여 주는 반면 Facade는 전체 하위 시스템을 나타내는 단일 개체를 만드는 방법을 보여 줍니다.
  • Flyweight는 오브젝트의 모든 공유 상태를 단 하나의 플라이웨이트 오브젝트로 어떻게든 줄일 수 있다면 Singleton과 비슷할 것입니다. 그러나 이러한 패턴에는 두 가지 근본적인 차이점이 있습니다.
  1. There should be only one Singleton instance, whereas a Flyweight class can have multiple instances with different intrinsic states.
  2. Singleton 개체는 변경할 수 있습니다. 플라이웨이트 객체는 변경할 수 없습니다.
profile
software engineer

0개의 댓글