구조 패턴 - 플라이웨이트 패턴 (Flyweight Pattern)

French Marigold·2024년 4월 18일
0

디자인패턴

목록 보기
6/10
post-thumbnail

정의

  • 플라이웨이트 패턴 (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가 생성하고 캐싱하고 관리를 하는 것이다.

플라이웨이트 패턴을 적용하지 않을 경우

  1. 우선 플라이웨이트 패턴을 적용하지 않은 채로 총 게임을 앱으로 만들어보자. 그리고 메모리 낭비가 얼마나 되는지도 한 번 확인해보자. 우선 메모리를 확인할 수 있는 모의 클래스를 하나 만들어보자.
class Memory {
    static var size: Int64 = 0 // 메모리 사용량

    static func printMemory() {
        print("총 메모리 사용량 : \(Memory.size)MB")
    }
}
  1. 총알을 흩뿌릴 수 있는 Map 객체도 만들자.
class Map {
    static let mapSize: Int = 10000
}
  1. 총알 클래스를 만든다. 총알 하나의 메모리 사이즈는 총 100MB이며, 총알 인스턴스가 생성될 때마다 메모리에 100MB 씩 쌓이게 된다. 해당 클래스에는 총알의 모양, 색상, 위치에 관한 정보가 담겨 있다.
    • 총알의 모양과 색상은 Intrinsic State로, 값이 고유하므로 “공유될 수 있는 자원”이다.
    • 총알의 위치는 Extrinsic State로, 값이 항상 변하므로 “공유될 수 없는 자원”이다.
class Bullet {
    var bulletMemorySize: Int64 = 100 // 100MB
    
    // Intrinsic State 값
    var bulletShape: String
    var bulletColor: String
    
    // Extrinsic State 값
    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
        
        // 총알 인스턴스를 찍어낼 경우, 메모리에 100MB씩 쌓인다.
        Memory.size += self.bulletMemorySize
    }
}
  1. 총알의 모양, 색깔, 위치 등을 조합하여 게임에 활용될 수 있도록 하는 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)의 총알 생성")
    }
}
  1. 게임 내에서 캐릭터로 하여금 총을 쏘는 메소드를 만든다. 해당 메소드는 for 루프를 5번을 돌며 총알 클래스 객체를 일일이 만들어낸다. 따라서 총을 한 번 쏠 때마다 메모리에 값이 500MB나 쌓이게 된다.
class BrawlStars {
    // 총알의 색상, 모양, 좌표 등을 지정해주는 영역인 Factory 객체를 게임에서 생성
    let bulletFactory = BulletFactory()
    
    // 쉘리가 총을 한 번 쏠 때마다 5발의 총알 인스턴스를 일일이 만들어내기 때문에
    // 총을 쏠 때마다 메모리에 값이 쌓이게 된다. (메모리 과부하)
    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() // 500MB

플라이웨이트 패턴을 적용할 경우

  1. 이번에는 플라이웨이트 패턴을 적용한 상태로 총 게임을 만들어보자. 우선 메모리를 관리하는 클래스를 만들어서 메모리 사용량을 확인하자.
class Memory {
    static var size: Int64 = 0 // 메모리 사용량

    static func printMemory() {
        print("총 메모리 사용량 : \(Memory.size)MB")
    }
}
  1. 총알을 흩뿌릴 수 있는 맵 객체 역시 만들고
class Map {
    static let mapSize: Int = 10000
}
  1. Flyweight 추상화 프로토콜을 만든다. 해당 프로토콜은 ConcreteFlyweight 영역과 UnsharedConcreteFlyweight에서 구체화된다.
protocol Flyweight {
    var bulletColor: String { get }
    var bulletShape: String { get }
}
  1. ConcreteFlyweight (공유할 수 있는 영역 - 총알의 모양과 색깔)에서 Flyweight 프로토콜을 채택하여 구체적으로 구현한다.
// ConcreteFlyweight
class BulletFlyweight: Flyweight {
		// 총알의 모양과 색상을 할당하는 데는 90MB의 메모리 값이 필요함
    var bulletMemorySize: Int64 = 90 // 90MB
    
    var bulletColor: String
    var bulletShape: String

    init(bulletColor: String, bulletShape: String) {
        self.bulletColor = bulletColor
        self.bulletShape = bulletShape
        
        // 총알 객체가 생성이 되면 메모리 사용량을 증가시킨다. 
        Memory.size += self.bulletMemorySize
    }
}
  1. UnsharedConcreteFlyweight(공유할 수 없는 영역 - 총알의 위치) 에서는 Flyweight 프로토콜을 프로퍼티에서 받는다.
// UnsharedConcreteFlyweight
class BulletPosition {
		// 총알의 위치를 설정하는 데에는 10MB의 메모리 값이 필요하다. 
    var bulletPosition: Int64 = 10 // 10MB
    
    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
        
        // 총알의 모양 & 색상과 총알의 위치를 모두 반영하여 인스턴스로 찍어내면
        // 총 100MB의 메모리가 사용되는 것이다. 
        Memory.size += self.bulletPosition
    }

    func display() {
        print("x: \(positionX) y: \(positionY) 위치에 \(flyweight.bulletColor) \(flyweight.bulletShape)의 총알 생성")
    }
}
  1. 총알의 모양, 색깔, 위치 등을 조합하여 게임에 활용될 수 있도록 하는 Factory 클래스를 만든다. 단, 기존의 Factory와 다른 점은 딕셔너리를 생성한 후 key 값을 "(총알의 색상)-(총알의 모양)" 으로 설정한다는 것이다. 만약에 같은 총알 모양과 색상의 인스턴스가 이미 존재한다면 기존에 존재하던 총알 인스턴스를 불러온다. 만약에 같은 총알 모양과 색상의 인스턴스가 존재하지 않는다면 딕셔너리에 새롭게 할당한다. 이렇게 함으로써 인스턴스를 여러 번 반복해서 사용하지 않고 딱 한 번만 사용할 수 있게 된다!
// Flyweight Factory
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
        }
    }
}
  1. Client 게임 객체에서 총을 쏜다. 인스턴스가 한 번 만들어지고 난 이후에는 기존에 저장되어 있는 인스턴스를 딕셔너리를 통해 불러오므로 더 이상 총알 색상이나 모양에 메모리가 낭비되지 않게 된다.
class BrawlStars {
    // 총알의 색상, 모양, 좌표 등을 지정해주는 영역인 Factory 객체를 게임에서 생성
    let bulletFactory = BulletFactory()
    
    func shellyShootTheGun() {
		    // 총알이 5번 생성되어도 인스턴스가 한 번 만들어지고 난 이후에는 
			  // 기존에 저장되어 있는 인스턴스를 딕셔너리를 통해 불러오는 로직이 존재하므로
			  // 더 이상 총알 색상이나 모양에 메모리가 낭비되지 않게 된다!
        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() // 140MB

패턴 사용 시기

  • 애플리케이션 내에 생성되는 객체의 수가 너무 많아 메모리를 관리해야 할 필요가 있을 때.
  • 공통적인 인스턴스를 많이 생성하는 로직이 있을 경우

패턴의 장점

  • 애플리케이션의 메모리 사용량을 크게 줄일 수 있다.
  • 프로그램의 속도를 개선할 수 있다.

패턴의 단점

  • 여러 가지 계층으로 나누어 인스턴스를 관리하기에 코드의 복잡도가 증가한다.

참고 문헌

profile
꽃말 == 반드시 오고야 말 행복

2개의 댓글

comment-user-thumbnail
2024년 4월 19일

총알의 외형과 위치를 나눠서 외형은 재사용한다니..
생각도 못해본 방법이에요
게임 성능 최적화는 이런 것인가 싶기도 하네요

BulletPosition이 Flyweight를 채택한다고 적으셨는데 코드상에서는 채택이 안되어있고 프로퍼티로 보유하고 있네요
코드랑 설명 중에 뭐가 맞는 걸까요?

1개의 답글