[Design pattern] 2-5. 플라이웨이트 패턴 (Flyweight pattern)


Study/Design Pattern  2021. 10. 29. 19:44

안녕하세요. 명월입니다.


이 글은 디자인 패턴의 플라이웨이트 패턴(Flyweight pattern)에 대한 글입니다.


플라이웨이트의 영어의 뜻은 경량화하다라는 뜻입니다. 그러니깐 인스턴스의 생성을 최소화하여서 메모리의 사용을 최대한 아끼는 방법입니다.

구조 패턴의 싱글톤 버전이라고 생각하면 쉽습니다. 그러나 singleton처럼 static을 이용하는 것은 아니고 보통 Map(Dictionary)와 같이 사용합니다.

출처 - https://en.wikipedia.org/wiki/Flyweight_pattern

#pragma once
#include <stdio.h>
#include <iostream>
#include <map>
using namespace std;
// INode 인터페이스
class INode {
public:
  // 추상 함수
  virtual void print() = 0;
  virtual ~INode() {};
};
// Node 인스턴스를 만드는 Builder 클래스
class NodeBuilder {
private:
  // 인라인 클래스(외부에서는 참조할 수 없다.)
  // INode 인터페이스를 상속
  class Node : public INode {
  private:
    // 맴버 변수
    char data;
    int count = 0;
  public:
    // 생성자
    Node(char data) {
      // data를 넣는다.
      this->data = data;
    }
    // 출력 함수
    virtual void print() {
      // 출력, data는 입력 받은 값이고 count는 Builder에서 호출될 때마다 count가 올라간다.
      cout << this->data << " Node counting - " << this->count << endl;
    }
    // 카운팅 함수
    void counting() {
      // count 변수값을 1증가
      this->count++;
    }
  };
  // flyweight 맵
  map<char, Node*> flyweight;
public:
  // Node 클래스를 생성하는 함수
  Node* getNode(char data) {
    // data 값으로 flyweight 맵에서 data를 키로 하는 Node 인스턴스가 있는지 확인
    if (this->flyweight.find(data) == this->flyweight.end()) {
      // 없으면 인스턴스를 생성한다.
      this->flyweight.insert(make_pair(data, new Node(data)));
    }
    // flyweight 맵에서 data를 키로 Node 인스턴스를 취득
    Node* node = this->flyweight.at(data);
    // Node 클래스의 count를 하나 증가한다.
    node->counting();
    // 반환
    return node;
  }
  // 소멸자
  ~NodeBuilder() {
    // flyweight 맵에 있는 인스턴스를 모두 메모리 해제한다.
    for (map<char, Node*>::iterator ptr = this->flyweight.begin(); ptr != this->flyweight.end(); ptr++) {
      // 메모리 해제
      delete ptr->second;
    }
  }
};
// 실행 함수
int main() {
  // NodeBuilder 인스턴스 생성
  NodeBuilder builder;
  // a의 키로 Node 인스턴스를 취득한다.
  INode* node = builder.getNode('a');
  // 출력
  node->print();
  // b의 키로 Node 인스턴스를 취득한다.
  node = builder.getNode('b');
  // 출력
  node->print();
  // a의 키로 Node 인스턴스를 취득한다.
  node = builder.getNode('a');
  // 출력
  node->print();

  return 0;
}

Builder라는 클래스에서 getNode를 통해 Node 인스턴스를 취득합니다.

그런데 a의 키로 넣었을 때는 count가 2가 되었습니다. 그 뜻은 a를 두번 호출했으나 같은 인스턴스를 2번 리턴한 뜻입니다. 즉, 두번 이상 호출이 되면 인스턴스를 새로 생성하지 않고 첫번째 호출 되었을 때 생성된 인스턴스를 map에 넣어서 같은 인스턴스를 취득하는게 플라이웨이트 패턴입니다.

import java.util.HashMap;
import java.util.Map;

// 인터 페이스
interface INode {
  // 추상 메서드
  void print();
}

// INode를 상속한 ANode 클래스
class ANode implements INode {
  // 추상 메서드를 재정의 한다.
  public void print() {
    // 콘솔 출력
    System.out.println("The memory address of ANode class - " + super.hashCode());
  }
}

// INode를 상속한 BNode 클래스
class BNode implements INode {
  // 추상 메서드를 재정의 한다.
  public void print() {
    // 콘솔 출력
    System.out.println("The memory address of BNode class - " + super.hashCode());
  }
}

// 팩토리 클래스
class NodeFactory {
  // flywieght 패턴
  private Map<Class<? extends INode>, INode> flyweight = new HashMap<>();

  // Node 인스턴스를 취득한다.
  public INode getNode(Class<? extends INode> clz) {
    // flywieght변수의 map 안에 key가 없으면
    if (!flyweight.containsKey(clz)) {
      // 파라미터 타입이 ANode면 ANode의 인스턴스를 생성해서 넣는다.
      if (clz == ANode.class) {
        flyweight.put(clz, new ANode());
      }
      // 파라미터 타입이 BNode면 BNode의 인스턴스를 생성해서 넣는다.
      else if (clz == BNode.class) {
        flyweight.put(clz, new BNode());
      } else {
        // 그 외의 예외처리한다.
        throw new UnsupportedOperationException();
      }
    }
    // 한 번 생성한 것은 재사용한다.
    return flyweight.get(clz);
  }
}

// 실행 클래스
class Program {
  // 실행 함수
  public static void main(String[] args) {
    // 팩토리 클래스 생성
    var factory = new NodeFactory();

    // ANode 인스턴스 취득
    factory.getNode(ANode.class).print();
    // BNode 인스턴스 취득
    factory.getNode(BNode.class).print();
    // ANode 인스턴스 취득
    factory.getNode(ANode.class).print();
  }
}

사실 플라이웨이트 패턴은 위의 Factory 메서드 패턴과 같이 자주 사용합니다. Factory에서 인스턴스를 생성하고 한번 생성된 인스턴스는 재사용을 하는 것입니다.

그래도 Factory 메서드 패턴이다 보니 Class를 추가할 때마다 Factory 함수를 수정해야 하는 불편함이 있습니다.

using System;
using System.Collections.Generic;
// IDao 인터페이스
interface IDao
{
  // 추상 함수
  void Select();
}
// IDao 인터페이스를 상속 받은 ADao
class ADao: IDao
{
  // 함수 재정의
  public void Select()
  {
    // 콘솔 출력
    Console.WriteLine("ADao was selected!");
  }
}
// IDao 인터페이스를 상속 받은 BDao
class BDao : IDao
{
  // 함수 재정의
  public void Select()
  {
    // 콘솔 출력
    Console.WriteLine("BDao was selected!");
  }
}
// FactoryDao 클래스
class FactoryDao
{
  // 플라이웨이트 패턴 Dictionary
  private Dictionary<Type, IDao> flyweight = new Dictionary<Type, IDao>();
  // Dao 인스턴스를 취득
  public T GetDao<T>() where T : IDao
  {
    // 제네릭 타입으로 플라이웨이트 딕셔너리에 인스턴스가 있는지 확인
    if (!flyweight.ContainsKey(typeof(T)))
    {
      // 없으면 Reflection 기능을 이용해서 인스턴스 생성
      flyweight.Add(typeof(T), (IDao)Activator.CreateInstance(typeof(T)));
    }
    // 인스턴스 리턴
    return (T)flyweight[typeof(T)];
  }
}

class Program
{
  // 실행 함수
  static void Main(string[] args)
  {
    // FactoryDao 인스턴스 생성
    var factory = new FactoryDao();
    // ADao 인스턴스를 받아와서 Select 함수 실행
    factory.GetDao<ADao>().Select();
    // BDao 인스턴스를 받아와서 Select 함수 실행
    factory.GetDao<BDao>().Select();
    // 아무 키나 누르시면 종료합니다.
    Console.WriteLine("Press any key...");
    Console.ReadKey();
  }
}

위 예제에서는 Java의 예와 비슷한데 FactoryDao안을 Reflection과 Generic기능을 이용해서 인스턴스를 취득하게 만들었습니다.

이 패턴이 어디서 사용하는가 하면, ORM 프레임워크에서 Dao를 취득하는 함수에서 사용하는 방법입니다. 특히 Spring에서 의존성 주입으로 Dao를 취득할 때 프레임워크에서 위와 같은 구조로 인스턴스를 넘기게 되는 것입니다.

즉, 한번 생성된 인스턴스는 계속해서 재사용한다. 라는 뜻이지요.


여기까지 디자인 패턴의 플라이웨이트 패턴(Flyweight pattern)에 대한 글이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.