플라이웨이트 패턴
객체를 가볍게 만들어 메모리 사용량을 줄이는 패턴. 자주 변하는 속성과 불변 속성을 분리하고 불변 속성을 재사용함으로써 메모리 사용량을 줄일 수 있다.
구조
- Flyweight - Flyweight가 받아들일 수 있고, 부가적 상태에서 동작해야 하는 인터페이스를 선언한다.
- ConcreteFlyweight - Flyweight 인터페이스를 구현하고 내부적으로 갖고 있어야 하는 본질적 상태에 대한 저장소를 정의한다. ConcreteFlyweight 객체는 공유할 수 있는 것이어야 한다.
- UnsharedConcreteFlyweight - 공유될 필요가 없는 Flyweight 구현 클래스.
- FlyweightFactory - Flyweight 객체를 생성하고 관리하며, Flyweight 객체가 제대로 공유되도록 보장한다.
- Client - Flyweight 객체에 대한 참조자를 관리하며 Flyweight 객체의 부가적인 상태를 저장한다.
기본 인터페이스
using System;
using System.Collections.Generic;
// Flyweight 인터페이스
interface Flyweight
{
void Operation(int extrinsicState);
}
// ConcreteFlyweight 클래스
class ConcreteFlyweight : Flyweight
{
private int intrinsicState; // 내재적인 상태
public ConcreteFlyweight(int intrinsicState)
{
this.intrinsicState = intrinsicState;
}
public void Operation(int extrinsicState)
{
Console.WriteLine($"ConcreteFlyweight: Intrinsic State = {intrinsicState}, Extrinsic State = {extrinsicState}");
}
}
// UnsharedFlyweight 클래스
class UnsharedFlyweight : Flyweight
{
private int allState; // 모든 상태를 갖는 UnsharedFlyweight
public UnsharedFlyweight(int allState)
{
this.allState = allState;
}
public void Operation(int extrinsicState)
{
Console.WriteLine($"UnsharedFlyweight: All State = {allState}, Extrinsic State = {extrinsicState}");
}
}
// Flyweight 팩토리 클래스
class FlyweightFactory
{
private Dictionary<int, Flyweight> flyweights = new Dictionary<int, Flyweight>();
public Flyweight GetFlyweight(int key)
{
if (!flyweights.ContainsKey(key))
{
// 새로운 플라이웨이트 객체 생성
Flyweight flyweight;
if (key % 2 == 0)
{
flyweight = new ConcreteFlyweight(key);
}
else
{
flyweight = new UnsharedFlyweight(key * 2);
}
flyweights[key] = flyweight;
}
return flyweights[key];
}
}
왜 사용해야 할까?
개발을 할 때면 같은 객체를 여러 개 생성하여 사용하는 경우가 종종 있다. 필드 맵의 몬스터를 스폰한다던지, 여러 개의 투사제를 생성한다든지, 수십개의 아이템을 드롭한다던지 여러 가지 이유가 있다. 이 때 생성하는 객체 크기가 얼마 안되면 상관없겠지만, 생성하는 객체가 무겁다면 한번에 수십, 수백 개를 생성하고 관리하는 것은 메모리에 큰 부담이 갈 가능성이 있다.
플라이웨이트 패턴은 객체에 사용되는 메모리를 줄이는 데 도움을 주는 패턴이다.
플라이웨이트 패턴은 인스턴스를 새로 생성하는 대신 기존 인스턴스를 재사용함으로써 메모리 절약 효과를 볼 수 있다.
이를 위해 동일하거나 유사한 객체들을 미리 만들어 놓고, 새로운 요청이 올 때마다 기존 객체 인스턴스를 재활용하는 것이다.
이해를 위해 하나의 예시를 들어보자.
개발자 john은 FPS 게임을 제작 중이다. FPS 게임의 핵심 기능인 '쏘기'를 구현하기 위해 매 초마다 수백발의 총알을 생성해서 발사해야 한다.
하나의 총알은 이름, 3D 모델 데이터, 현재 위치 등의 정보를 가지고 있는 Bullet 클래스로 구현했다.
여기서 3D 모델 데이터는 복잡한 mesh들과 텍스처로 이루어져 있어 약 500KB 정도의 메모리를 사용한다.
이 시스템을 기반으로 1분 동안 1000발을 발사한다고 가정했을 때, 총 6만 발의 총알 객체가 생성되어 총 30GB의 메모리를 사용하게 된다.
개발 중인 john의 개인 컴퓨터의 메모리가 부족하기 때문에 이는 큰 부하가 될 것이다.
중복되는 총알 3D 모델 데이터 때문에 발생하는 때문에 이를 최적화하고 싶다고 생각했다.
위에서 총알이 관리하는 데이터는 이름(name), 모델 데이터(model), 현재 위치(pos)다.
// 총알 클래스
// 총 메모리 사용량 - 약 500KB + 4B
class Bullet {
private String name;
private Model model; // 500KB
public Vector3 pos;
public Bullet(String name, Model model) {
this.name = name;
this.model = model;
}
}
Bullet 클래스의 데이터 중 pos는 총알의 위치에 따라 계속 갱신해야 하지만, name, model은 예외적인 상황이 아니면 변하지 않는 불변 데이터다. 플라이웨이트 패턴을 활용하면 동일한 속성의 총알 객체를 개별 생성하지 않고, 이미 만들어진 객체를 재활용하여 메모리 절약 효과를 끌어낼 수 있다.
class FlyweightBullet{
public String name;
public Model model; // 500KB
public FlyweightBullet(String name, Model model) {
this.name = name;
this.model = model;
}
}
// 총알 팩토리
class BulletFactory {
//총알 데이터를 저장해놓은 pool
Map<String, FlyweightBullet> pool = new HashMap<>();
//관리하는 pool에서 공유할 데이터를 가져와 새로운 Bullet을 생성하고 반환한다.
public Bullet getBullet(String name) {
if(!pool.containsKey(name)) {
Model m = loadModel(name + ".md"); // 새로운 모델 데이터 생성
FlyweightBullet b = new FlyweightBullet(name, m);
pool.put(name, b);
}
Bullet bullet = new Bullet(name, pool.get(name).model);
return bullet;
}
}
Factory는 자주 사용되는 불변 데이터를 저장하는 pool을 가지고 있다. FlyweightBullet는 총알의 이름, 모델을 저장하고, BulletFactory는 FlyweightBullet을 필요한 만큼만 생성하고 Client의 요구에 따라 Bullet 객체를 생성하고 반환한다.
Client가 요구하는 데이터가 없다면 새로운 FlyweightBullet을 생성하고 저장하지만, 요구 데이터가 있을 시 해당 FlyweightBullet 객체이서 Model 객체를 참조함으로써 새로 생성된 Bullet 객체의 메모리 사용량은 500KB+ 4B 가 아닌 4B 가 된다.
public class Client {
public static void main(String[] args) {
// 플라이웨이트 미사용
Bullet[] bullets = new Bullet[1000];
for(int i=0; i<1000; i++) {
bullets[i] = new Bullet("9mm", loadModel("9mm.md"));
}
// 1000 * 500KB = 500MB
// 플라이웨이트 사용
BulletFactory factory = new BulletFactory();
for(int i=0; i<1000; i++) {
Bullet b = factory.getBullet("9mm");
}
// 1개의 FlyweightBullet + 1000개의 Bullet
// 약 500KB + 4KB = 504KB 정도
}
}
기존 500MB에서 FlyweightBullet으로 중복 모델 데이터를 제거한 결과 약 500KB 수준으로 줄일 수 있다. 약 1000배에 달하는 메모리 절약 효과를 얻을 수 있는 것이다.
장점
- 자주 사용하는 객체의 생성에 사용되는 메모리를 줄일 수 있다.
- 객체 생성 및 소멸에 따른 오버헤드(비용)을 줄일 수 있다.
단점
- 코드 복잡도가 증가한다.
전체 예시 코드
// 총알 클래스
// 총 메모리 사용량 - 약 500KB + 4B
class Bullet {
private String name;
private Model model; // 500KB
public Vector3 pos;
public Bullet(String name, Model model) {
this.name = name;
this.model = model;
}
}
class FlyweightBullet{
public String name;
public Model model; // 500KB
public FlyweightBullet(String name, Model model) {
this.name = name;
this.model = model;
}
}
// 총알 팩토리
class BulletFactory {
//총알 데이터를 저장해놓은 pool
Map<String, FlyweightBullet> pool = new HashMap<>();
//관리하는 pool에서 공유할 데이터를 가져와 새로운 Bullet을 생성하고 반환한다.
public Bullet getBullet(String name) {
if(!pool.containsKey(name)) {
Model m = loadModel(name + ".md"); // 새로운 모델 데이터 생성
FlyweightBullet b = new FlyweightBullet(name, m);
pool.put(name, b);
}
Bullet bullet = new Bullet(name, pool.get(name).model);
return bullet;
}
}
public class Client {
public static void main(String[] args) {
// 플라이웨이트 미사용
Bullet[] bullets = new Bullet[1000];
for(int i=0; i<1000; i++) {
bullets[i] = new Bullet("9mm", loadModel("9mm.md"));
}
// 1000 * 500KB = 500MB
// 플라이웨이트 사용
BulletFactory factory = new BulletFactory();
for(int i=0; i<1000; i++) {
Bullet b = factory.getBullet("9mm");
}
// 1개의 FlyweightBullet + 1000개의 Bullet
// 약 500KB + 4KB = 504KB 정도
}
}
참고자료
'프로그래밍 이론 > 디자인 패턴' 카테고리의 다른 글
13. [Design Pattern] 책임 연쇄(Chain of Responsibility) 패턴 (1) | 2024.01.06 |
---|---|
12. [Design Pattern] 프록시(Proxy) 패턴 (1) | 2024.01.06 |
10. [Design Pattern] 퍼사드(Facade) 패턴 (0) | 2023.12.25 |
09. [Design Pattern] 데코레이터(Decorator) 패턴 (1) | 2023.12.10 |
08. [Design Pattern] 컴포지트(Composite) 패턴 (1) | 2023.11.25 |