[디자인 패턴] Flyweight Pattern

전성훈·2023년 11월 3일
0

DesignPattern

목록 보기
12/18
post-thumbnail

주제: 메모리 관리 패턴


Flyweight pattern은 동일하거나 유사한 객체들 사이에 가능한 많은 데이터를 서로 공유하여 사용하도록 하여 메모리 사용량을 최소화하는 소프트웨어 디자인 패턴이다. 종종 오브젝트의 일부 상태 정보는 공유될 수 있는데, flyweight pattern에서는 이와 같은 상태 정보를 외부 자료 구조에 저장하여 플라이웨이트 오브젝트가 잠깐 동안 사용할 수 있도록 전달한다.

플라이웨이트 패턴이란

  • Flyweight pattern은 메모리 소비를 낮게 사용하게 하여 방대한 양의 객체를 지원할 수 있도록 하는 디자인 패턴이다.
  • 유사하거나 같은 객체를 다량으로 생성해야 하는 경우, 혹은 객체 간 추출 및 공유할 수 있는 데이터가 있는 경우에 사용하기 좋다. 두 가지 경우 모두 객체를 공유하면 재사용할 수 있기 때문에 메모리를 절약할 수 있다는 장점이 있다.

플라이웨이트 패턴의 구조

플라이웨이트

  • Flyweight Factory

    객체를 생성하고 관리하는 역할을 한다. 클라이언트가 요청할 때, 팩토리는 이미 생성된 객체를 제공하거나 새로운 객체를 생성한다.

  • Flyweight Interface

    모든 Concrete Flyweight가 구현해야 하는 인터페이스이다. 이 인터페이스는 내부 상태와 외부 상태를 기반으로 동작을 정의한다.

  • Concrete Flyweight

    Flyweight 인터페이스를 구현하는 클래스로, 내부 상태를 가진다.

  • Unshared Concrete Flyweight

    필요한 경우 Flyweight 인터페이스를 구현하는 클래스로, 모든 상태가 외부적인 클래스이다.

장단점

장점

  • 메모리 사용량을 크게 줄일 수 있다.
  • 객체 생성, 삭제, 가비지 컬렉션 비용을 줄일 수 있다.

단점

  • 패턴을 사용하면 코드가 복잡해질 수 있다.
  • 공유된 객체가 변경되면 해당 객체를 사용하는 모든 곳에 영향을 미친다.

결론

  • Flyweight 패턴은 이미지 렌더링, 텍스트 스타일링 등에 Flyweight 패턴을 사용할 수 있다. 예를 들어 텍스트 스타일링에 Flyweight 패턴을 적용하면 동일한 텍스트 스타일을 여러 번 재생성하는 대신 공유하여 메모리 사용량을 줄일 수 있다.
  • 따라서, Flyweight 패턴은 메모리 최적화가 중요한 상황에서 매우 유용한 디자인 패턴이다. 그러나 이 패턴을 사용할 때는 코드의 복잡성과 공유된 객체의 변경에 따른 영향을 고려해야 한다.

예시 코드

기본 Flyweight 패턴

import UIKit

// flyweight
protocol Flyweight {
    var sharedState: String { get }
}

// concrete flyweight
class ConcreteFlyweight: Flyweight {
    var sharedState: String
    
    init(sharedState: String) {
        self.sharedState = sharedState
    }
}

// flyweight factory
class FlyweightFactory {
    private var flyweights: [String: Flyweight] = [:]
    
    init(states: [String]) {
        states.forEach { state in
            flyweights[state] = ConcreteFlyweight(sharedState: state)
        }
    }
    
    func getFlyweight(for state: String) -> Flyweight {
        if let flyweight = flyweights[state] {
            return flyweight
        } else {
            let flyweight = ConcreteFlyweight(sharedState: state)
            flyweights[state] = flyweight
            return flyweight
        }
    }
}

let factory = FlyweightFactory(states: ["1","2","3"])
let flyweight = factory.getFlyweight(for: "2")

텍스트 스타일링

import UIKit

// Flyweight 
protocol TextStyle { 
	var font: UIFont { get }
	var color: UIColor { get }
}

// ConcreteFlyweight 
class ConcreteTextStyle: TextStyle { 
	var font: UIFont
	var color: UIColor
	
	init(font: UIFont, color: UIColor) { 
		self.font = font 
		self.color = color
	}
}

// FlyweightFactory
class TextStyleFactory { 
	private var textStyles: [String: TextStyle] = [:]
	
	func getTextStyle(for name: String) -> TextStyle { 
		if let textStyle = textStyles[name] { 
			return textStyle
		} else { 
			let newStyle = TextStyle(font: .systemFont(ofSize: 12), color: .black)
			styles[name] = newStyle
			return newStyle
		}
	}
}

이미지 캐시처리

flyweight 만 활용한 경우

  • 이미지를 로드하고 캐시하는 것은 메모리 사용량을 줄이는 데 매우 효과적인 방법이다. 특히 테이블 뷰에서는 스크롤하는 동안 동일한 이미지를 여러 번 로드할 가능성이 있으므로, Flyweight 패턴을 사용하면 이러한 중복 로드를 피할 수 있다.
  • 다만, 실제로 이미지를 URL에서 로드하려면 비동기 작업이 필요하며, 이는 Flyweight 패턴의 범위를 벗어난다. 이를 처리하기 위해 'NSCache'나 'URLSession'의 이미지 캐싱 기능을 사용하거나, 외부 라이브러리를 사용할 수 있다.
import UIKit

class ImageCache {
    private var imageCache = NSCache<NSString, UIImage>()
    
    func getImage(url: String, completion: @escaping (UIImage?) -> Void) {
        if let cachedImage = imageCache.object(forKey: url as NSString) {
            completion(cachedImage)
        } else {
            DispatchQueue.global().async {
                if let url = URL(string: url),
                   let data = try? Data(contentsOf: url),
                   let image = UIImage(data: data) {
                    self.imageCache.setObject(image, forKey: url.absoluteString as NSString)
                    DispatchQueue.main.async {
                        completion(image)
                    }
                } else {
                    DispatchQueue.main.async {
                        completion(nil)
                    }
                }
            }
        }
    }
}

let imageCache = ImageCache()

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
    let imageUrl = "https://example.com/image.jpg"
    imageCache.getImage(url: imageUrl) { image in
        cell.imageView?.image = image
    }
    return cell
}

cache 데이터를 singleton으로 만들고 flyweight을 적용한 경우

  • 먼저, ImageCache 클래스를 싱글톤으로 구현하고, 이미지를 캐시하는 메서드를 제공한다. 이 클래스는 Flyweight 패턴의 Factory 역할을 담당한다.
class ImageCache { 
	static let shared = IamgeCache()
	
	private init() { }
	
	private let cache = NSCache<NSString, UIImage>()
	
	func getImage(for url: URL) -> UIImage? { 
		return cache.object(forKey: url.absoluteString as NSString)
	}
	
	func setImage(_ image: UIImage, for url: URL) { 
		cache.setObject(image, forKey: url.absoluteString as NSString)
	}
}
  • 그런 다음, 이미지를 다운로드하고 캐시하는 'ImageDownloader' 클래스를 구현한다.
  • 이 클래스는 이미지 다운로드 작업을 관리하고, 다운로드된 이미지를 'ImageCache'에 저장한다.
class ImageDownloader { 
	func downloadImage(from url: URL, completion: @escaping (UIImage?) -> Void) { 
		// 이미지가 캐시에 있는지 확인
		if let cachedImage = ImageCache.shared.getImage(for: url) { 
			completion(cachedImage)
			return
		} 
		
		// 이미지가 캐시에 없으면 다운로드
		let task = URLSession.shared.dataTask(with: url) { data, response, error in 
			guard let data = data, let image = UIImage(data: data) else { 
				completion(nil)
				return
			}
			
			// 다운로드 된 이미지를 캐시에 저장
			ImageCache.shared.setImage(image, for: url)
			
			// 다운로드된 이미지를 반환 
			completion(image)
		}
		
		task.resume()
	}
}
  • 마지막으로 'ImageDownloader'를 사용하여 이미지를 다운로드하고 표시하는 UIImageView 확장을 구현한다.
extension UIImageView { 
	func setImage(from url: URL) { 
		ImageDownloader().downloadImage(from: url) { [weak self] image in 
			DispatchQueue.main.async { 
				self?.image = image
			}
		}
	}
}
  • 이제 'UITableViewDataSource'에서 'cellForRowAt' 메서드를 구현하면서 'UIImageView'의 'setImage(from:)' 메서드를 사용하여 이미지를 다운로드하고 셀에 표시할 수 있다.
class MyTableViewController: UITableViewController {
    
    var imageURLs: [URL] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.register(MyCustomCell.self, forCellReuseIdentifier: "MyCustomCell")
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return imageURLs.count
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "MyCustomCell", for: indexPath) as! MyCustomCell
        
        let url = imageURLs[indexPath.row]
        cell.setImage(from: url)
        
        return cell
    }
}

출처(참고문헌)

제가 학습한 내용을 요약하여 정리한 것입니다. 내용에 오류가 있을 수 있으며, 어떠한 피드백도 감사히 받겠습니다.

감사합니다.

0개의 댓글