"MMORPG 게임을 만드는데, 숲속에 나무를 100만 그루 심어야 합니다."
나무 한 그루 객체가 Mesh(형태), Texture(질감), Position(좌표) 정보를 가지고 있는데 크기가 1KB라고 가정해 봅시다. 100만 그루면 1GB의 메모리가 필요합니다. 단순한 배경 때문에 서버나 클라이언트가 뻗어버리는 상황이죠.
이때 "변하지 않는 정보(형태, 질감)"는 딱 하나만 만들어서 공유하고, "변하는 정보(좌표)"만 개별적으로 관리하면 어떨까요? 이것이 플라이웨이트 패턴의 핵심입니다.
1. 핵심 개념: 두 가지 상태의 분리
플라이웨이트 패턴을 이해하려면 객체의 데이터를 두 가지로 쪼개야 합니다.
- 내부 상태 (Intrinsic State): 객체 내부에 저장되며 공유 가능한 정보. (예: 나무의 색깔, 텍스처)
- 외부 상태 (Extrinsic State): 상황에 따라 바뀌며 공유 불가능한 정보. (예: 나무의 x, y 좌표)
2. 구현 방식: 팩토리와 캐싱 (Factory & Caching)
이 패턴의 구현은 **"공유 객체를 관리하는 팩토리(Factory)"**가 핵심입니다.
상황 (숲 만들기)
- 공유할 것: TreeModel (나무 종류, 껍질, 잎사귀 텍스처 - 무거움)
- 개별적인 것: Tree (x, y 좌표 - 가벼움)
코드 예시
// 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); // 그릴 때 좌표만 넘겨줌
}
}
결과
// 나무 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
String s1 = "Hello";
String s2 = "Hello";
System.out.println(s1 == s2); // true
자바는 리터럴("")로 생성된 문자열을 힙 메모리의 String Pool에 저장하고 공유합니다. 똑같은 문자열을 여러 번 만들어도 메모리 낭비가 없는 이유입니다.
2) Integer Cache (Integer.valueOf)
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에서 조회해서 객체로 만드는 대신, 캐싱해두고 공유해서 쓰는 것이 넓은 의미의 플라이웨이트 패턴 적용입니다.
'프로그래밍 > 디자인패턴' 카테고리의 다른 글
| 인터프리터 패턴(Interpreter Pattern) 완벽 정리 (0) | 2025.12.07 |
|---|---|
| 메멘토 패턴(Memento Pattern) 완벽 정리 (0) | 2025.12.07 |
| 방문자 패턴(Visitor Pattern) 완벽 정리 (0) | 2025.12.07 |
| 브리지 패턴(Bridge Pattern) 완벽 정리 (0) | 2025.12.07 |
| 중재자 패턴(Mediator Pattern) 완벽 정리 (0) | 2025.12.07 |