변하지 않는 상태를 공유해 메모리를 절약하는 패턴.
두 상태를 나눈다
-
내재 상태(intrinsic) — 객체마다 같다 → 공유한다
- 예) 글자의 폰트·글리프 모양, 나무의 종류·텍스처
-
외재 상태(extrinsic) — 객체마다 다르다 → 밖에서 넘긴다
- 예) 글자의 위치, 나무의 좌표
// 내재 상태만 담은 공유 객체
class TreeType {
private final String name; private final Texture texture; // 무겁다
}
class TreeFactory {
private static final Map<String, TreeType> cache = new HashMap<>();
static TreeType get(String name) {
return cache.computeIfAbsent(name, TreeType::new); // 같은 종류는 재사용
}
}
// 외재 상태는 사용 시점에 넘긴다
class Tree {
private final int x, y; // 가볍다
private final TreeType type; // 공유 참조
void draw() { type.draw(x, y); }
}
숲에 나무 100만 그루, 종류는 10가지라면
Flyweight 없이 무거운 객체 1,000,000개
Flyweight 적용 무거운 객체 10개 + 가벼운 객체 1,000,000개
실무에서 — 이미 쓰고 있다
Integer.valueOf(127) // -128~127 캐시 — 같은 인스턴스를 돌려준다
String 리터럴 풀 // 같은 리터럴은 하나의 인스턴스
Boolean.valueOf(true)
이 때문에 Integer a = 127, b = 127; a == b 는 true 인데 128 이면 false 가 된다. 자바의 대표적 함정이 사실 Flyweight의 결과다.
대가
- 공유 객체는 반드시 불변이어야 한다 (하나가 바뀌면 전부 바뀐다)
- 외재 상태를 매번 넘겨야 해 코드가 번거로워진다
- 객체 수가 적으면 캐시 관리 비용만 남는다
면접 함정
- ❌ "캐시와 같다" → 캐시는 조회 결과 재사용, Flyweight는 상태 분리를 통한 공유가 설계 의도다.
- ❌ 공유 객체에 가변 필드 → 한 곳의 변경이 전부에 퍼진다.
캐시와 무엇이 다른가
-
캐시 — 조회 결과 저장 — 성능 최적화. 인스턴스가 여러 개여도 된다
-
Flyweight — 내재/외재 상태를 '나누는 설계'. 같은 내재 상태 = 같은 인스턴스
-
캐시는 구현 수단이고 Flyweight 는 설계 결정이다
자바 문자열 풀
String a = "hello", b = "hello";
a == b // true — 풀에서 같은 인스턴스
String c = new String("hello");
a == c // false
c.intern() == a // true
언제 쓰지 말아야 하나
-
객체 수가 적다 → 캐시 자료구조 비용만 남는다
-
내재 상태가 가볍다 → 공유해서 아낄 게 없다
-
외재 상태 전달이 지나치게 번거로워진다
-
측정 먼저다. "객체가 많으니 Flyweight" 는 근거가 아니다
동시성 주의
공유 객체는 여러 스레드가 동시에 참조하므로 반드시 불변이어야 하고, 팩토리의 캐시 맵도 스레드 안전해야 한다(ConcurrentHashMap).