정의
- 플라이웨이트 패턴 (Flyweight Pattern)이란 재사용할 수 있는 객체를 “공유 자원”으로 만들어 메모리 사용량을 최소화하는 디자인 패턴이다. 가능한 많은 데이터를 서로 공유하게 하여 최적화를 노리는 패턴이라고 볼 수 있다.
- 예를 들어, 재미있는 총 게임을 앱으로 제작한다고 해보자. 만일 총을 쏠 때마다 총알 하나하나를 일일이 객체로 만들어 구현한다면 어떻게 될까? 처음에야 게임이 돌아가겠지만 나중에는 메모리에 과부하가 일어나 앱이 꺼질지도 모른다.

- 플라이웨이트 패턴 (Flyweight Pattern)은 이렇게 메모리 사용량을 최소화해야 하는 문제를 해결할 때에 사용된다. 총을 쏠 때마다 총알을 만들어서 구현하는 것이 아닌 총알 객체 인스턴스를 딱 하나만 만들고 Client와 공유하여 이를 화면에 흩뿌리는 방법으로 메모리를 경량화 할 수 있다.
플라이웨이트 패턴 (Flyweight Pattern) 의 구조

- Flyweight ⇒
- Concrete Flyweight ⇒
- 플라이웨이트 프로토콜을 구체적으로 구현한 영역.
- “공유가 가능하여 재사용되는 객체”이다. (Intrinsic State)
- Unshared Concrete Flyweight ⇒
- 플라이웨이트 프로토콜을 구체적으로 구현한 영역.
- “공유가 불가능한 객체”이다. (Extrinsic State)
- Flyweight Factory ⇒
- 플라이웨이트 객체 인스턴스를 찍어내는 영역.
- 캐싱 데이터를 가지고 소유하고 있으며, 어떤 인스턴스가 캐시 되어 있다면 그대로 가져와서 반환하고 캐시 되어 있지 않다면 새로 생성하여 반환한다.
- Client ⇒
- Client는 Flyweight Factory를 통해서 Flyweight 타입의 객체를 얻어와서 사용한다.
Intrinsic State & Extrinsic State 의 차이
- 플라이웨이트 패턴을 사용할 때에 가장 중요하게 생각해야 하는 부분은 바로 Intrinsic State와 Extrinsic State를 구분하는 것이다.
- Intrinsic State ⇒
- 인스턴스가 어떤 상황에도 변하지 않는 상태를 의미한다.
- 값이 고정되어 있기에 서로 다른 객체와 공유해도 문제가 발생하지 않는다.
- 안전하게 “공유할 수 있는 객체”이다.
- Concrete Flyweight로 구현하면 된다.
- Extrinsic State ⇒
- 인스턴스가 상황에 따라서 변하는 상태를 의미한다.
- 값이 어디서 변할지 모르기 때문에 이것을 공유 자원으로 사용할 수는 없다.
- 매번 값이 바뀌어 “공유할 수 없는 객체” 이다.
- Unshared Concrete Flyweight로 구현하면 된다.

- 예를 들어 총알의 색깔과 모양은 모두 똑같으니 Intrinsic State에 해당한다. 즉, 총알의 색깔과 모양은 “공유할 수 있는 객체” 가 된다.
- 반면 총알이 흩뿌려지는 x, y 좌표값은 총알마다 모두 다르므로 Extrinsic State에 해당한다. 즉, 총알의 x, y 좌표값은 “공유할 수 없는 객체” 가 된다.
- 이 두 종류의 총알 객체를 Flyweight Factory가 생성하고 캐싱하고 관리를 하는 것이다.
플라이웨이트 패턴을 적용하지 않을 경우
- 우선 플라이웨이트 패턴을 적용하지 않은 채로 총 게임을 앱으로 만들어보자. 그리고 메모리 낭비가 얼마나 되는지도 한 번 확인해보자. 우선 메모리를 확인할 수 있는 모의 클래스를 하나 만들어보자.
class Memory {
static var size: Int64 = 0
static func printMemory() {
print("총 메모리 사용량 : \(Memory.size)MB")
}
}
- 총알을 흩뿌릴 수 있는 Map 객체도 만들자.
class Map {
static let mapSize: Int = 10000
}
- 총알 클래스를 만든다. 총알 하나의 메모리 사이즈는 총 100MB이며, 총알 인스턴스가 생성될 때마다 메모리에 100MB 씩 쌓이게 된다. 해당 클래스에는 총알의 모양, 색상, 위치에 관한 정보가 담겨 있다.
- 총알의 모양과 색상은 Intrinsic State로, 값이 고유하므로 “공유될 수 있는 자원”이다.
- 총알의 위치는 Extrinsic State로, 값이 항상 변하므로 “공유될 수 없는 자원”이다.
class Bullet {
var bulletMemorySize: Int64 = 100
var bulletShape: String
var bulletColor: String
var positionX: Double
var positionY: Double
init(bulletShape: String, bulletColor: String, positionX: Double, positionY: Double) {
self.bulletShape = bulletShape
self.bulletColor = bulletColor
self.positionX = positionX
self.positionY = positionY
Memory.size += self.bulletMemorySize
}
}
- 총알의 모양, 색깔, 위치 등을 조합하여 게임에 활용될 수 있도록 하는 Factory 클래스를 만든다.
class BulletFactory {
func create(bulletShape: String, bulletColor: String, positionX: Double, positionY: Double) {
let bullet = Bullet(bulletShape: bulletShape,
bulletColor: bulletColor,
positionX: positionX,
positionY: positionY)
print("x: \(bullet.positionX) y: \(bullet.positionY) 위치에 \(bullet.bulletColor) \(bullet.bulletShape)의 총알 생성")
}
}
- 게임 내에서 캐릭터로 하여금 총을 쏘는 메소드를 만든다. 해당 메소드는 for 루프를 5번을 돌며 총알 클래스 객체를 일일이 만들어낸다. 따라서 총을 한 번 쏠 때마다 메모리에 값이 500MB나 쌓이게 된다.
class BrawlStars {
let bulletFactory = BulletFactory()
func shellyShootTheGun() {
for _ in 0..<5 {
bulletFactory.create(bulletShape: "원 모양",
bulletColor: "보라색",
positionX: Double.random(in: 0..<Double(Map.mapSize)),
positionY: Double.random(in: 0..<Double(Map.mapSize))
)
}
Memory.printMemory()
}
}
let brawlStars = BrawlStars()
brawlStars.shellyShootTheGun()
플라이웨이트 패턴을 적용할 경우
- 이번에는 플라이웨이트 패턴을 적용한 상태로 총 게임을 만들어보자. 우선 메모리를 관리하는 클래스를 만들어서 메모리 사용량을 확인하자.
class Memory {
static var size: Int64 = 0
static func printMemory() {
print("총 메모리 사용량 : \(Memory.size)MB")
}
}
- 총알을 흩뿌릴 수 있는 맵 객체 역시 만들고
class Map {
static let mapSize: Int = 10000
}
- Flyweight 추상화 프로토콜을 만든다. 해당 프로토콜은 ConcreteFlyweight 영역과 UnsharedConcreteFlyweight에서 구체화된다.
protocol Flyweight {
var bulletColor: String { get }
var bulletShape: String { get }
}
- ConcreteFlyweight (공유할 수 있는 영역 - 총알의 모양과 색깔)에서 Flyweight 프로토콜을 채택하여 구체적으로 구현한다.
class BulletFlyweight: Flyweight {
var bulletMemorySize: Int64 = 90
var bulletColor: String
var bulletShape: String
init(bulletColor: String, bulletShape: String) {
self.bulletColor = bulletColor
self.bulletShape = bulletShape
Memory.size += self.bulletMemorySize
}
}
- UnsharedConcreteFlyweight(공유할 수 없는 영역 - 총알의 위치) 에서는 Flyweight 프로토콜을 프로퍼티에서 받는다.
class BulletPosition {
var bulletPosition: Int64 = 10
var positionX: Double
var positionY: Double
var flyweight: Flyweight
init(positionX: Double, positionY: Double, flyweight: Flyweight) {
self.positionX = positionX
self.positionY = positionY
self.flyweight = flyweight
Memory.size += self.bulletPosition
}
func display() {
print("x: \(positionX) y: \(positionY) 위치에 \(flyweight.bulletColor) \(flyweight.bulletShape)의 총알 생성")
}
}
- 총알의 모양, 색깔, 위치 등을 조합하여 게임에 활용될 수 있도록 하는 Factory 클래스를 만든다. 단, 기존의 Factory와 다른 점은 딕셔너리를 생성한 후 key 값을 "(총알의 색상)-(총알의 모양)" 으로 설정한다는 것이다. 만약에 같은 총알 모양과 색상의 인스턴스가 이미 존재한다면 기존에 존재하던 총알 인스턴스를 불러온다. 만약에 같은 총알 모양과 색상의 인스턴스가 존재하지 않는다면 딕셔너리에 새롭게 할당한다. 이렇게 함으로써 인스턴스를 여러 번 반복해서 사용하지 않고 딱 한 번만 사용할 수 있게 된다!
class BulletFactory {
private var flyweights: [String: Flyweight] = [:]
func getFlyweight(bulletColor: String, bulletShape: String) -> Flyweight {
let key = "\(bulletColor)-\(bulletShape)"
if let flyweight = flyweights[key] {
return flyweight
} else {
let newFlyweight = BulletFlyweight(bulletColor: bulletColor, bulletShape: bulletShape)
flyweights[key] = newFlyweight
return newFlyweight
}
}
}
- Client 게임 객체에서 총을 쏜다. 인스턴스가 한 번 만들어지고 난 이후에는 기존에 저장되어 있는 인스턴스를 딕셔너리를 통해 불러오므로 더 이상 총알 색상이나 모양에 메모리가 낭비되지 않게 된다.
class BrawlStars {
let bulletFactory = BulletFactory()
func shellyShootTheGun() {
for _ in 0..<5 {
let bullet = BulletPosition(positionX: Double.random(in: 0..<Double(Map.mapSize)),
positionY: Double.random(in: 0..<Double(Map.mapSize)),
flyweight: bulletFactory.getFlyweight(bulletColor: "보라색", bulletShape: "원 모양"))
bullet.display()
}
Memory.printMemory()
}
}
let brawlStars = BrawlStars()
brawlStars.shellyShootTheGun()
패턴 사용 시기
- 애플리케이션 내에 생성되는 객체의 수가 너무 많아 메모리를 관리해야 할 필요가 있을 때.
- 공통적인 인스턴스를 많이 생성하는 로직이 있을 경우
패턴의 장점
- 애플리케이션의 메모리 사용량을 크게 줄일 수 있다.
- 프로그램의 속도를 개선할 수 있다.
패턴의 단점
- 여러 가지 계층으로 나누어 인스턴스를 관리하기에 코드의 복잡도가 증가한다.
참고 문헌
총알의 외형과 위치를 나눠서 외형은 재사용한다니..
생각도 못해본 방법이에요
게임 성능 최적화는 이런 것인가 싶기도 하네요
BulletPosition이 Flyweight를 채택한다고 적으셨는데 코드상에서는 채택이 안되어있고 프로퍼티로 보유하고 있네요
코드랑 설명 중에 뭐가 맞는 걸까요?