Self-Dev/Design Patterns R&D

[23] 디자인 패턴 목록 - 구조 패턴 - 플라이웨이트 패턴(Flyweight Pattern or Cash Pattten)

Khadra 2024. 7. 6. 18:41

출처 : 디자인 패턴에 뛰어들기 - 알렉산더 슈베츠 도서


플라이웨이트(Flyweight)

각 객체에 모든 데이터를 유지하는 대신 여러 객체들 간에 상태의 공통 부분들을 공유하여 사용할 수 있는 RAM에 더 많은 객체들을 포함할 수 있도록 하는 구조 디자인 패턴이다.



문제

당신은 재미 삼아 플레이어들이 지도를 돌아다니며 서로에게 총을 쏘는 간단한 비디오 게임을 만들기로 가정해본다.

당신은 폭발들로 인한 방대한 양의 총알들, 미사일들 및 파편들이 지도 전체를 날아다니는 전율 넘치는 경험을 플레이어들에게 선사하기로 했으며, 이를 선사하기 위해 사실적인 입자 시스템을 구현하기로 한다.

당신은 게임을 완성한 후 친구에게 게임을 보내고, 내 컴퓨터에서는 게임이 완벽하게 실행되었지만, 당신의 친구는 오랫동안 게임을 즐길 수 없었다.
왜냐하면 친구의 컴퓨터에서는 시작 후 고작 몇 분 후에 게임이 계속 충돌했기 때문이다.
당신이 디버그 로그를 자세히 살펴본 결과, 친구의 컴퓨터의 RAM이 당신의 컴퓨터처럼 충분하지 않아 게임이 충돌했음이 확인되었다.

문제의 원인은 당신의 입자 시스템과 관련이 있었습니다.

  • 각 총알, 미사일 또는 파편과 같은 입자는 많은 데이터를 포함하는 별도의 객체로 표시되어 있다.
  • 플레이어 화면의 대학살이 절정에 이르렀을 때 새로 생성된 입자들을 더 이상 나머지 RAM이 감당하지 못해서 프로그램이 충돌한 것이다.



해결책

Particle(입자) 클래스를 자세히 살펴본다.

  • color(색상) 및 sprite(스프라이트) 필드들이 다른 필드들보다 훨씬 더 많은 메모리를 사용한다는 것을 알 수 있다.
  • 더 나쁜 것은 이 두 필드가 모든 입자에 걸쳐 거의 같은 데이터를 저장한다는 것이다.
  • 예를 들어, 모든 총알은 같은 색상과 스프라이트를 갖는 것이다.



좌표, 이동 벡터 및 속도와 같은 입자 상태의 다른 부분들은 각 입자마다 고유하고 있다.

  • 이러한 필드들의 값은 시간이 지남에 따라 변경된다.

이 데이터는 입자의 계속 변화하는 콘텍스트를 나타낸다.

  • 반면 색상과 스프라이트는 각 입자마다 일정하게 유지된다.

객체의 이러한 상수 데이터를 일반적으로 고유한 상태라고 한다.

  • 이 데이터는 객체 안에서 살며, 다른 객체들은 이 데이터를 읽을 수만 있고 변경할 수는 없다.
  • 종종 다른 객체들에 의해 '외부에서' 변경되는 객체의 나머지 상태를 공유한 상태라고 한다.

플라이웨이트 패턴은 객체 내부에 공유한 상태의 저장을 중단한다.

  • 대신 이 상태를 이 상태에 의존하는 특정 메서드들에 전달할 것을 제안하고 있다.

고유한 상태만 객체 내에 유지된다.

  • 해당 고유한 상태는 콘텍스트가 다른 곳에서 재사용할 수 있다.
  • 이러한 객체들은 공유한 상태보다 변형이 훨씬 적은 고유한 상태에서만 달라진다.
    • 이로써 훨씬 더 적은 수의 객체만 있으면 됩니다.


이제 당신의 게임을 다시 살펴보도록 한다.

입자 클래스에서 공유한 상태를 추출했다고 가정해본다.

  • 총알, 미사일, 파편의 세 가지 다른 객체만으로도 게임의 모든 입자를 충분히 나타낼 수 있다.
  • 즉, 이를 고유한 상태만 저장하는 객체를 플라이웨이트라고 한다.



공유한 상태 스토리지

  • 공유한 상태는 어디로 이동하는지 알아본다.
    • 일부 클래스가 이 상태를 여전히 저장하고 있는 걸 알 수 있다.

 

  • 대부분의 경우 공유한 상태는 패턴을 적용하기 전에 객체들을 집합시키는 컨테이너 객체로 이동된다.
    • 당신의 게임에서 이것은 particle 필드에 모든 입자를 저장하는 주요 Game 객체이다.
    • 공유한 상태를 이 클래스로 이동하려면 개별 입자의 좌표, 벡터 및 속도를 저장하기 위한 여러 배열 필드들을 생성해야 한다.

 

  • 입자를 나타내는 특정 플라이웨이트에 대한 참조를 저장하려면 다른 배열이 필요하다.
    • 이러한 배열들은 같은 인덱스를 사용하여 입자의 모든 데이터에 액세스할 수 있도록 동기화되어야 한다.

 



이보다 더 훌륭한 해결책은 플라이웨이트 객체에 대한 참조와 함께 공유된 상태를 저장할 별도의 콘텍스트 클래스를 만드는 것아다.

  • 이 접근 방식을 사용하려면 컨테이너 클래스에 단일 배열만 있으면 된다.
  • Q1) 처음에 그랬던 것처럼 이런 콘텍스트 객체들이 많이 있어야 하지 않나요?
    : 맞다. 그러나 이제는 이러한 객체들이 이전보다 훨씬 작다.

 

가장 메모리를 많이 사용하는 필드들이 고작 몇 개의 플라이웨이트 객체들로 이동되었다.

  • 이는 하나의 커다란 플라이웨이트 객체를 몇천 개의 작은 콘텍스트 객체들이 재사용할 수 있다.
  • 더 이상 커다란 플라이웨이트 객체의 데이터의 천 개의 복사본을 저장할 필요가 없다.



플라이웨이트와 불변성

  • 같은 플라이웨이트 객체가 다른 콘텍스트들에서 사용될 수 있다.
    • 해당 플라이웨이트 객체의 상태를 수정할 수 없는지 확인해야 한다.
    • 플라이웨이트는 생성자 매개변수들을 통해 상태를 한 번만 초기화해야 한다.
    • 주의사항으로는 setter 또는 public 필드들을 다른 객체들에 노출해서는 안된다.



플라이웨이트 팩토리

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

 

이 메서드를 배치할 수 있는 몇 가지 옵션이 있으며, 그중 가장 확실한 장소는 플라이웨이트 컨테이너입니다.

  • 대안으로 새 팩토리 클래스를 생성할 수 있다.
  • 팩토리 메서드를 정적으로 만들고 실제 플라이웨이트 클래스에 넣을 수 있다.



구조



  • 1. 플라이웨이트 패턴은 단지 최적화에 불과하다.
    • 이 패턴을 적용하기 전에 프로그램이 동시에 메모리에 유사한 객체들을 대량으로 보유하는 것과 관련된 RAM 소비 문제가 있는지 확인해야 한다.
    • 이 문제가 다른 의미 있는 방법으로 해결될 수 없는지도 확인해야 한다.
  • 2. 플라이웨이트 클래스에는 여러 객체들 간에 공유할 수 있는 원래 객체의 상태의 부분이 포함된다.
    • 같은 플라이웨이트 객체가 다양한 콘텍스트에서 사용될 수 있다.
    • 플라이웨이트 내부에 저장된 상태를 고유한(intrinsic) 상태라고 한다.
    • 플라이웨이트의 메서드에 전달된 상태를 공유한(extrinsic) 상태라고 한다.
  • 3. 콘텍스트 클래스는 공유한 상태를 포함하며, 이 상태는 모든 원본 객체들에서 고유하다.
    • 콘텍스트가 플라이웨이트 객체 중 하나와 쌍을 이루면 원래 객체의 전체 상태를 나타낸다.
  • 4. 일반적으로 원래 객체의 행동은 플라이웨이트 클래스에 남아 있다.
    • 이 경우 플라이웨이트의 메서드의 호출자는 공유한 상태의 적절한 부분들을 메서드의 매개변수들에 전달해야 한다.
    • 반면에, 행동은 콘텍스트 클래스로 이동할 수 있다.
      • 이 클래스는 연결된 플라이웨이트를 단순히 데이터 객체로 사용할 것이다.
  • 5. 클라이언트는 플라이웨이트들의 공유된 상태를 저장하거나 계산한다.
    • 클라이언트의 관점에서 플라이웨이트는 일부 콘텍스트 데이터를 그의 메서드들의 매개변수들에 전달하여 런타임에 설정될 수 있는 템플릿 객체이다.
  • 6. 플라이웨이트 팩토리는 기존 플라이웨이트들의 풀을 관리한다.
    • 이 팩토리로 인해 클라이언트들은 플라이웨이트들을 직접 만들지 않는 대신 원하는 플라이웨이트의 고유한 상태의 일부를 전달하여 공장을 호출한다.
    • 팩토리는 이전에 생성된 플라이웨이트들을 살펴보고 검색 기준과 일치하는 기존 플라이웨이트를 반환하거나 기준에 맞는 플라이웨이트가 발견되지 않으면 새로 생성한다.



의사코드

이 예시에서 플라이웨이트 패턴은 캔버스에 수백만 개의 나무 객체들을 렌더링할 때 메모리 사용량을 줄이는 데 도움을 받을 수 있다.



이 패턴은 주요 Tree(나무) 클래스에서 반복되는 고유한 상태를 추출하여 TreeType(나무 종류) 플라이웨이트 클래스로 이동하는 것을 알 수 있다.

  • 같은 데이터를 여러 객체에 저장하는 대신 이제 몇 개의 플라이웨이트 객체들에 보관되고 콘텍스트 역할을 하는 적절한 Tree 객체들에 연결된다.
  • 클라이언트 코드는 플라이웨이트 팩토리를 사용하여 새 Tree 객체들을 생성한다.
  • 이 팩토리는 올바른 객체를 검색하고 필요한 경우 재사용하는 작업의 복잡성을 캡슐화한다.
// 플라이웨이트 클래스는 트리의 상태 일부를 포함한다. 
// 이러한 필드는 각 특정 트리에 대해 고유한 값들을 저장한다. 
// 예를 들어 여기에서는 트리 좌표들을 찾을 수 없을 것이다. 
// 그러나 많은 트리들이 공유하는 질감들과 색상들은 찾을 수 있을 것이다. 
// 이 데이터는 일반적으로 크기 때문에 각 트리 개체에 보관하면 많은
// 메모리를 낭비하게 된다. 
// 대신 질감, 색상 및 기타 반복되는 데이터를 많은 개별 트리 객체들이 
// 참조할 수 있는 별도의 객체로 추출할 수 있다.
class TreeType is
  field name
  field color
  field texture
  constructor TreeType(name, color, texture) { ... }
  method draw(canvas, x, y) is
    // 1. 주어진 유형, 색상 및 질감의 비트맵을 만드세요.
    // 2. 비트맵을 캔버스의 X 및 Y 좌표에 그리세요.

// 플라이웨이트 팩토리는 기존 플라이웨이트를 재사용할지 아니면
// 새로운 객체를 생성할지를 결정한다.
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

// 콘텍스트 객체는 트리 상태의 공유된 부분을 포함한다. 
// 이러한 부분들은 두 개의 정수로 된 좌표와 하나의 참조 필드만 참조하여
// 크기가 작기 때문에 하나의 앱이 이런 부분을 수십억 개씩 만들 수 있다.
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[…]
This material may be protected by copyright.



적용

플라이웨이트 패턴은 당신의 프로그램이 많은 수의 객체들을 지원해야 해서 사용할 수 있는 RAM을 거의 다 사용했을 때만 사용해야 한다.

  • 이 패턴 적용의 혜택은 패턴을 사용하는 방법과 위치에 따라 크게 달라지며, 다음과 같은 경우에 가장 유용하다.
    • 앱이 수많은 유사 객체들을 생성해야 할 때
    • 이것이 대상 장치에서 사용할 수 있는 모든 RAM을 소모할 때
    • 이 객체들에 여러 중복 상태들이 포함되어 있으며, 이 상태들이 추출된 후 객체 간에 공유될 수 있을 때



구현방법

  • 1. 플라이웨이트가 될 클래스의 필드들을 두 부분으로 나누세요.
    • 고유한 상태: 많은 객체에 걸쳐 복제된 변경되지 않는 데이터를 포함하는 필드들
    • 공유한 상태: 각 객체에 고유한 콘텍스트 데이터를 포함하는 필드들
  • 2. 클래스의 고유한 상태를 나타내는 필드들은 그대로 두되 변경될 수 없도록 하세요.
    • 이 필드들은 생성자 내부에서만 초깃값들을 가져와야 합니다.
  • 3. 공유한 상태의 필드들을 사용하는 메서드들을 살펴보세요.
    • 메서드에 사용된 각 필드에 대해 새 매개변수를 도입하고 필드 대신 사용하세요.
  • 4. 옵션으로, 플라이웨이트들의 풀을 관리하기 위한 팩토리 클래스를 생성하세요.
    • 이 클래스는 새 플라이웨이트를 만들기 전에 기존 플라이웨이트의 존재 여부를 확인해야 한다.
    • 팩토리가 설치되면 고객은 팩토리를 통해서만 플라이웨이트를 요청해야 한다.
    • 그들은 팩토리에 플라이웨이트의 고유한 상태를 전달하여 원하는 플라이웨이트를 설명해야 한다.
  • 5. 클라이언트는 플라이웨이트 객체들의 메서드들을 호출할 수 있도록 공유한 상태의 값들(콘텍스트)을 저장하거나 계산해야 한다.
    • 편의상, 플라이웨이트를 참조하는 필드와 공유한 상태는 별도의 콘텍스트 클래스로 이동할 수 있다.



장단점

장점

  • 당신의 프로그램에 유사한 객체들이 많다고 가정하면 많은 RAM을 절약할 수 있다.

단점

  • 누군가가 플라이웨이트 메서드를 호출할 때마다 콘텍스트 데이터의 일부를 다시 계산해야 하는 경우가 생긴다.
    • CPU 주기 대신 RAM을 절약하고 있는 것일지도 모른다.
  • 코드가 복잡해진다.
    • 이로 인해 새로운 팀원들은 왜 개체(entity)의 상태가 그런 식으로 분리되었는지 항상 궁금해할 것이다.



다른 패턴과의 관계

  • RAM을 절약하기 위하여 복합체 패턴 트리의 공유된 잎 노드들을 플라이웨이트들로 구현할 수 있다.
  • 플라이웨이트는 작은 객체들을 많이 만드는 방법을 보여 주는 반면 퍼사드 패턴은 전체 하위 시스템을 나타내는 단일 객체를 만드는 방법을 보여주고 있다.
  • 만약 객체들의 공유된 상태들을 단 하나의 플라이웨이트 객체로 줄일 수 있는 경우
    • 플라이웨이트는 싱글턴과 유사해질 수 있습니다.
    • 그러나 이 패턴들에는 두 가지 근본적인 차이점이 있다.