앱을 만들다 보면 비슷비슷한 객체를 수천, 수만 개씩 찍어내야 할 때가 있어요. 지도 위에 마커를 수만 개 뿌리는 기능이 대표적인데, 이런 상황에서는 메모리 그래프가 계속 치솟기 쉽습니다.
이럴 때 꺼내 들 만한 게 바로 플라이웨이트 패턴이에요. 한마디로, 여러 객체가 공통으로 쓰는 데이터를 하나로 합쳐 공유해서 메모리 사용량을 크게 줄이는 기법입니다. 똑같은 걸 매번 새로 만들지 말고 만들어둔 하나를 같이 쓰자는 거죠.
이 글에서는 Swift에서 플라이웨이트 패턴이 뭔지, 언제 쓰면 좋은지, 실제 코드로는 어떻게 구현하는지까지 짚어 볼게요.
플라이웨이트 패턴이 대체 뭔가요?
플라이웨이트(Flyweight)는 ’가벼운 물체’라는 뜻이에요. 권투에서 가장 가벼운 체급 이름이기도 하죠.
아이디어는 단순합니다. 객체가 가진 데이터를 두 종류로 나누는 거예요.
- 내재적 상태(intrinsic): 여러 객체가 공유할 수 있는, 바뀌지 않는 데이터. 예를 들면 나무의 종류, 텍스처, 색상 같은 것.
- 외재적 상태(extrinsic): 객체마다 다른, 그때그때 바뀌는 데이터. 예를 들면 나무의 위치나 크기.
숲을 그린다고 생각해 볼게요. 나무 100만 그루를 그리는데, 그 나무들이 사실 소나무·참나무·단풍나무 세 종류뿐이라면 어떨까요?
텍스처와 색상 정보(내재적 상태)는 딱 세 개만 만들어서 공유하고 위치 값(외재적 상태)만 100만 개 따로 들고 있으면 됩니다.
무거운 공통 데이터는 하나만 만들어 공유하고, 가벼운 개별 데이터만 따로 관리한다. 이게 플라이웨이트의 전부입니다.
Swift 코드로 구현해 볼게요
말로만 하면 감이 잘 안 오니까 바로 코드를 보겠습니다. 아까 말한 나무 예시를 그대로 옮겨볼게요.
먼저 공유할 내재적 상태를 담는 플라이웨이트 객체입니다.
// 공유되는 무거운 데이터 (내재적 상태)
final class TreeType {
let name: String
let color: String
let texture: Data // 용량이 큰 텍스처
init(name: String, color: String, texture: Data) {
self.name = name
self.color = color
self.texture = texture
}
}
다음으로 이 TreeType들을 관리하고 공유해 주는 팩토리를 만듭니다. 이미 만든 종류가 있으면 새로 만들지 않고 그대로 씁니다.
// 같은 종류는 새로 만들지 않고 재사용
final class TreeFactory {
private var pool: [String: TreeType] = [:]
func treeType(name: String, color: String, texture: Data) -> TreeType {
let key = "\(name)-\(color)"
if let existing = pool[key] { return existing }
let type = TreeType(name: name, color: color, texture: texture)
pool[key] = type
return type
}
}
이제 실제 나무는 위치 같은 외재적 상태만 들고, 무거운 데이터는 TreeType을 참조만 합니다.
// 개별 나무는 위치만 갖고 종류는 참조로 공유
struct Tree {
let x: Double
let y: Double
let type: TreeType // 공유 객체 참조
}
나무 100만 그루를 심어도 TreeType 인스턴스는 종류 수만큼(예: 3개)만 존재합니다. 나머지는 전부 그 세 개를 가리키는 참조일 뿐이에요.
언제 쓰고, 언제 쓰지 말아야 할까?
플라이웨이트가 항상 정답은 아니에요. 오히려 안 쓰는 게 나은 경우도 많습니다. 상황별로 정리해 봤어요.
| 상황 | 플라이웨이트 |
|---|---|
| 같은 성격의 객체를 아주 많이 생성 | 적합 |
| 객체들이 공유 가능한 무거운 데이터를 가짐 | 적합 |
| 객체 수가 적음 | 불필요 (오히려 복잡도만 증가) |
| 대부분의 상태가 객체마다 제각각 | 효과 미미 |
대량의 객체와 공유 가능한 무거운 데이터, 이 두 조건이 겹칠 때가 바로 플라이웨이트의 자리입니다.
Swift에서는 참조 타입인 class가 이미 참조로 공유되기 때문에, 값 타입 struct에 무거운 데이터를 통째로 넣어 복사가 남발되는 상황에서 특히 효과가 큽니다.
자주 묻는 질문
Q. 싱글턴 패턴이랑 뭐가 다른가요?
싱글턴은 클래스의 인스턴스를 딱 하나로 강제하는 패턴이에요. 플라이웨이트는 인스턴스를 여러 개 두되, 공유 가능한 것만 골라 공유합니다. 나무 종류는 3개, 나무 자체는 100만 개인 것처럼요.
Q. 캐싱이랑 헷갈려요.
목적이 달라요. 캐싱은 ‘다시 계산·조회하기 싫어서’ 결과를 저장해 두는 거고 플라이웨이트는 ‘메모리를 아끼려고’ 공유 객체를 관리하는 겁니다. 다만 팩토리의 pool이 캐시처럼 동작하긴 해요.
Q. Swift 표준 라이브러리에도 쓰이나요?
네. 대표적으로 문자열 인터닝이나 작은 정수 캐싱 같은 최적화가 비슷한 발상이에요. 똑같은 값을 매번 새로 만들지 않고 공유하는 거죠.
플라이웨이트 패턴은 이름은 어렵지만 발상은 정말 단순해요. “겹치는 무거운 건 하나만 만들어 같이 쓰자”는 거죠.
지도 마커, 파티클, 타일맵, 텍스트 렌더링처럼 비슷한 객체를 대량으로 다뤄야 한다면 한 번쯤 떠올려 보시면 좋겠습니다. 메모리 그래프가 눈에 띄게 얌전해지는 걸 경험하실 거예요.
