프로그래밍/디자인패턴

플라이웨이트 패턴(Flyweight Pattern) 완벽 정리

Jinwookoh 2025. 12. 7. 19:14

"MMORPG 게임을 만드는데, 숲속에 나무를 100만 그루 심어야 합니다."

나무 한 그루 객체가 Mesh(형태), Texture(질감), Position(좌표) 정보를 가지고 있는데 크기가 1KB라고 가정해 봅시다. 100만 그루면 1GB의 메모리가 필요합니다. 단순한 배경 때문에 서버나 클라이언트가 뻗어버리는 상황이죠.

이때 "변하지 않는 정보(형태, 질감)"는 딱 하나만 만들어서 공유하고, "변하는 정보(좌표)"만 개별적으로 관리하면 어떨까요? 이것이 플라이웨이트 패턴의 핵심입니다.


1. 핵심 개념: 두 가지 상태의 분리

플라이웨이트 패턴을 이해하려면 객체의 데이터를 두 가지로 쪼개야 합니다.

  1. 내부 상태 (Intrinsic State): 객체 내부에 저장되며 공유 가능한 정보. (예: 나무의 색깔, 텍스처)
  2. 외부 상태 (Extrinsic State): 상황에 따라 바뀌며 공유 불가능한 정보. (예: 나무의 x, y 좌표)

2. 구현 방식: 팩토리와 캐싱 (Factory & Caching)

이 패턴의 구현은 **"공유 객체를 관리하는 팩토리(Factory)"**가 핵심입니다.

상황 (숲 만들기)

  • 공유할 것: TreeModel (나무 종류, 껍질, 잎사귀 텍스처 - 무거움)
  • 개별적인 것: Tree (x, y 좌표 - 가벼움)

코드 예시

Java
// 1. 내부 상태 (Intrinsic) - 메모리를 많이 차지하는 공유 객체
class TreeModel {
    String type; // "Oak", "Pine"
    Object mesh; // 무거운 3D 데이터
    Object texture; // 무거운 이미지 데이터

    public TreeModel(String type) {
        this.type = type;
        // mesh, texture 로딩 로직 (비용 큼)
    }

    // 2. 외부 상태를 인자로 받아서 행동 수행
    public void draw(int x, int y) {
        System.out.println(type + " 나무를 (" + x + "," + y + ") 위치에 그립니다.");
    }
}

// 3. 팩토리 (Factory) - 객체를 캐싱하여 공유
class TreeFactory {
    // 캐시 저장소 (핵심!)
    private static final Map<String, TreeModel> cache = new HashMap<>();

    public static TreeModel getTreeModel(String type) {
        // 없으면 만들고, 있으면 재사용 (Flyweight 핵심)
        return cache.computeIfAbsent(type, TreeModel::new);
    }
}

// 4. 클라이언트 (Client) - 가벼운 객체만 생성
class Tree {
    int x, y; // 외부 상태 (Extrinsic)
    TreeModel model; // 내부 상태 (공유 객체 참조)

    public Tree(int x, int y, String type) {
        this.x = x;
        this.y = y;
        this.model = TreeFactory.getTreeModel(type); // 팩토리에서 가져옴
    }

    public void render() {
        model.draw(x, y); // 그릴 때 좌표만 넘겨줌
    }
}

결과

Java
// 나무 100만 그루 생성
for (int i = 0; i < 1000000; i++) {
    // TreeModel 객체는 단 2개(Oak, Pine)만 생성됨!
    // Tree 객체(좌표+참조값) 100만 개는 생성되지만 매우 가벼움.
    new Tree(randomX, randomY, (i % 2 == 0 ? "Oak" : "Pine")); 
}

3. 실무 예시: 자바의 숨겨진 플라이웨이트

자바 개발자라면 알게 모르게 이미 이 패턴을 매일 쓰고 있습니다.

1) String Constant Pool

Java
 
String s1 = "Hello";
String s2 = "Hello";
System.out.println(s1 == s2); // true

자바는 리터럴("")로 생성된 문자열을 힙 메모리의 String Pool에 저장하고 공유합니다. 똑같은 문자열을 여러 번 만들어도 메모리 낭비가 없는 이유입니다.

2) Integer Cache (Integer.valueOf)

Java
Integer a = Integer.valueOf(100);
Integer b = Integer.valueOf(100);
System.out.println(a == b); // true (캐시된 객체)

Integer c = Integer.valueOf(200);
Integer d = Integer.valueOf(200);
System.out.println(c == d); // false (새로 생성됨)

자바는 -128부터 127까지의 숫자는 자주 쓰인다고 판단하여 미리 **캐시(Cache)**해 둡니다. new Integer(100) 대신 Integer.valueOf(100)을 써야 하는 이유가 바로 이 플라이웨이트 패턴의 혜택을 받기 위함입니다.


4. 요약: 언제 써야 할까?

구분 일반 객체 생성 (new) 플라이웨이트 패턴 (Factory)
동작 방식 요청할 때마다 무조건 new 이미 있으면 반환, 없으면 new
메모리 사용 $N$개 × (무거운 데이터) 1개 × (무거운 데이터) + $N$개 × (가벼운 참조)
상태 관리 모든 속성을 객체가 가짐 **Intrinsic(공유)**과 **Extrinsic(개별)**으로 분리
장점 구현이 단순함 메모리 사용량을 획기적으로 줄임
단점 메모리 부족(OOM) 위험 코드 복잡도 증가 (상태 분리 필요)

결론

"객체 100개를 만들 때는 필요 없습니다."

하지만 **"객체를 10만 개, 100만 개 만들어야 하는데, 그 객체들이 대부분 비슷한 속성(색상, 폰트, 이미지 등)을 가지고 있다"**면 반드시 플라이웨이트 패턴을 도입해야 합니다.

서버 개발 시에도 **자주 변하지 않는 공통 데이터(마스터성 데이터)**를 매번 DB에서 조회해서 객체로 만드는 대신, 캐싱해두고 공유해서 쓰는 것이 넓은 의미의 플라이웨이트 패턴 적용입니다.