Search

Caching Content With NSCache

*Caching Content With NSCache 글을 번역한 글입니다.

들어가며

우리는 우리 앱의 lifecycle동안 많은 일시적인 데이터를 저장하고 되찾는 것이 가능합니다.
필요에 따라 디스크에 데이터를 캐싱하고 직접 관리해야 할 수도 있고, 메모리에 데이터를 캐싱하는 데만 필요할 수도 있습니다.
Apple은 NSCache를 제공합니다.
NSCache : key-value 쌍을 사용해서 메모리에 파일을 캐시할 수 있는 가변 컬렉션
NSCache가 좋은 이유들 :
메모리에만 데이터를 저장합니다. 우리 앱이 종료되면 메모리에게 할당 해제가 되고 디스크에 남아있지 않아요.
key-value 쌍 매커니즘이 쉽게 캐싱된 콘텐츠를 설정하고 가져올 수 있게 합니다. Dictionary와 비슷하지만 dictionary와는 다르게 key를 복사하지 않습니다. 그렇기 때문에 dictionary보다 메모리 효율이 조금 더 높습니다.
우리는 자동 제거 상태(automatic eviction conditions)를 설정해서 NSCache가 자동적으로 객체 삭제하도록 돕습니다.
멀티 스레딩이 편리합니다. 우리는 우리가 직접 스레딩을 관리하지 않고도 캐시를 읽고 쓸 수 있습니다.
just one reason it is not perfect :
Objective-C API 이므로 String과 같은 기본 객체로 작업할 때도 캐스팅을 하게 됩니다.
NSCache를 사용하여 만드는 데 Cost가 많이 들지만 필요한 경우 다시 만들 수 있는 임시 객체를 저장합니다.
if 사용자에게 동적으로 표시할 많은 이미지를 다운로드하는 앱이 있고 매우 크다고 가정,
다운로드하는 데 시간이 오래 걸리고 많은 데이터를 소비하게 됩니다.
사용자가 필요할 때마다 이러한 이미지를 다시 다운로드하는 것은 좋지 않습니다. 다운로드하는 대신 그 이미지들을 캐시할 수 있습니다.
시스템이 메모리를 요구하면 시스템은 캐시된 이미지를 제거할 수 있고, 필요한 경우 다시 다운로드할 수 있습니다.
NSCache는 모든 Apple’s platforms(watchOS, iOS, iPadOS, macOS, TVOS)에서 사용 가능합니다.

NSCache Basics

Creating a NSCache Object

NSCache 객체 생성자는 두 가지 generic objects를 사용합니다.
1.
key type
2.
cached object type
나중에 식별하기 위해 선택적으로 이름을 지정할 수 있습니다.
let cache = NSCache<NSString, UIImage>() cache.name = "Remote Image Cache"
Swift
복사
이 API는 Objective-C에 뿌리는 두고 있으며, generic parameter는 AnyObject를 준수하는 것이 제한되기 때문에 우리는 struct를 사용할 수 없고 그 대신 class를 사용해야만 합니다.
  따라서 우리는 String 대신 NSString를 사용해야만 합니다.
NSString & String
key와 object가 모두 클래스라면 어떤 type이라도 될 수 있다. 위의 예에서는 key는 String를 선택했고, object는 UIImage를 선택했다.

Storing Objects

object를 저장하는 것은 setObject 메서드를 호출하는 것만큼 쉽습니다.
let webImage = UIImage(named: "pullip_doll.png")! cache.setObject(webImage, forKey: "top banner")
Swift
복사
또한 overload된 setObject(Object: forKey: cost:) 메서드가 있는데, 이 메서드에 대해서는 나중에 설명하겠습니다.
(API가 이런 작업에 대한 subscript를 제공한다면 좋겠지만, 안타깝게도 그렇지 않습니다.)

Retrieving Objects

객체를 검색하는 것도 간단합니다. object(forKey:) 라는 메서드 하나뿐입니다. 이 메서드는 optional ObjectType(우리의 경우, optional UIImage)을 반환하므로 객체가 있는지 쉽게 확인할 수 있습니다. 객체는 더 이상 존재하지 않거나 제거되면 nil를 반환합니다.
if let webImage = cache.object(forKey: "top banner") { print("The Object is stall cached") } else { print("Web image went away") }
Swift
복사

Removing Objects

객체 삭제는 복잡하지 않고, 단일 객체나 전체 캐시를 제거하는 방법이 존재합니다.
1.
removeObject(forKey:) : 단일 객체 삭제
cache.removeObject(forKey: "top banner")
Swift
복사
2.
removeAllObjects() : 전체 객체 삭제
cache.removeAllObjects()
Swift
복사

Automatic Eviction Conditions

캐시를 수동으로 제어하는 것은 중요하며 많은 경우 NSCache를 사용하면 자동으로 정리할 수 있는 조건을 설정해줍니다. limited amount의 객체를 보유하도록 제한할 수 있으며, maximum “cost”을 지정할 수 있습니다.
제거 조건을 설정하지 않은 경우에도 시스템이 메모리를 많이 필요로 하는 경우 NSCache는 객체를 삭제하기 시작합니다. 따라서 우리가 직접 제거 조건을 설정하지 않더라도 객체가 항상 존재한다고 기대할 수 없습니다.

Limiting the Amount of Objects in the Cache

캐시가 보유해야 하는 객체의 양을 제한하려면 countLimit을 0보다 큰 값으로 설정하면 됩니다. 0은 제한이 없다는 의미이미, 캐시는 객체를 무한으로 저장합니다.(시스템에 메모리가 필요하지 않은 경우)
cache.countLimit = 10 // limit 10 cache.countLimit = 0 // limit infinite
Swift
복사
어떤 사이즈가 좋은지는 전적으로 우리의 응용 프로그램에 달려 있습니다.
큰 이미지를 다루는 경우 → 낮은 값
문자열과 같은 작은 값 → 훨씬 더 높은 값
이것이 Strict limit이 아니라는 것을 주목할 필요가 있습니다. 객체의 제거는 캐시의 구현에 의해 제어됩니다. 캐시가 제한을 초과하면 즉시 또는 나중에 객체를 제거하거나 아예 제거하지 않을 수 있습니다. 그것은 모두 주어진 시간에 시스템이 필요로 하는지에 따라 달라질 겁니다.

Setting a Maximum Cost

Definition of Cache Object Cost

캐시에 있는 객체의 “Cost”는 약간 추상적이며, 캐시가 작동하는 상황에 따라 달라집니다.
이미지를 캐시에 저장하는 예로 돌아가볼게요.
이미지의 “Cost”를 바이트 단위로 크기를 정의할 수 있습니다. 이미지가 클수록 비용이 더 많이 듭니다. 크기(무게 및 높이)와 같은 다른 정의를 찾을 수 있습니다.
문자열을 저장하려면 각 문자열의 문자 수를 기준으로 “Cost”를 정의할 수 있습니다.
그래서 “Pullip Calssical Alice”(22자)가 “Pullip Alura”(12자)보다 비용이 더 많이 듭니다.

Limiting the Maximum Total Cost of the Cache

최대 Cost를 설정하려면 캐시의 totalCostLimit을 설정하세요. Int 타입이며, 정확히 무엇을 나타내는지는 각 캐시의 컨텍스트에 따라 달라집니다.
// 우리가 사용하는 이미지 캐시의 경우 최대 비용은 50,000,000바이트 또는 50MB로 설정합니다. cache.totalCostLimit = 50_000_000
Swift
복사
이제 Cost를 포함하여 객체를 추가하려면 위에서 언급한 setObject(object: forKey: cost:) 메서드를 사용합니다.
if let topBannerData = webImage.pngData() { cache.setObject(webImage, forKey: "top banner", cost: topBannerData.count) }
Swift
복사
maximum total object를 설정하는 것과 마찬가지로, 이는 strict limit가 아니며, Limit이 초과되면 캐시가 객체를 어떻게 처리할지를 결정합니다. 객체 제거를 시작해야 하는 경우 캐시의 total cost가 totalCostLimit보다 낮아질 때까지 일부 삭제를 시작합니다. 객체가 제거되는 순서는 임의입니다.
캐시가 the biggest cost object(ex. 가장 큰 이미지)를 먼저 제거하기 시작할 것으로 예상할 수 없으며, 특정 순서를 적용할 방법이 없습니다.

The NSDiscardableContent Protocol

NSDiscardableContent 프로토콜은 객체가 사용되지 않을 때 폐기될 수 있는 하위 구성 요소가 있을 때 구현될 수 있다.
Person calss가 있다고 가정해봅시다.
class Person { let firstName: String let lastName: String var avatar: UIImage? = nil init(firstName: String, lastName: String, avatar: UIImage?) { self.firstName = firstName self.lastName = lastName self.avatar = avatar } }
Swift
복사
이 정보를 캐시하고 싶지만 FirstName LastName이 너무 작아서 오랫동안 지속할 수 없습니다. 반면에 avatar는 크기 때문에 시스템이 필요할 때 avatar속성만 제거하고자 합니다.
이 경우, Person은 content-object고, avatar는 버릴 수 있는 하위 구성요소입니다.
NSCache를 사용하면 객체에 NSDiscardableContent를 구현하여 이 작업을 수행할 수 있습니다.
NSDiscardableContentsimple variable counter system과 함께 작동합니다.
메모리를 읽고 있거나 현재 필요한 경우 counter의 값은 1입니다.
필요하지 않고 사용되지 않는 경우 counter는 0이 됩니다.
NSDiscardableContent가 새로 생성되면 counter 값은 1로 시작합니다.
NSDiscardableContent를 준수할 때, 우리는 4가지 메소드를 채택해야 합니다.
// 내용을 계속 사용할 수 있고 성공적으로 액세스한 경우 참입니다. func beginContentAccess() -> Bool { } // 콘텐츠에 더 이상 액세스하지 않을 때 호출됩니다. func endContentAccess() { } // counter가 0이면 이미지를 폐기할 수 있습니다. func discardContentIfPossible() { } // 내용이 삭제된 경우 True입니다. func isContentDiscarded() -> Bool { }
Swift
복사
다음과 같은 방법으로 Protocol을 준수하는 Person를 구현할 수 있습니다 :
class Person: NSDiscardableContent { let firstName: String let lastName: String var avatar: UIImage? = nil // Our counter variable var accessCounter = true init(firstName: String, lastName: String, avatar: UIImage?) { self.firstName = firstName self.lastName = lastName self.avatar = avatar } // MARK: - NSDiscardableContent func beginContentAccess() -> Bool { if avatar != nil { accessCounter = true } else { accessCounter = false } return accessCounter } func endContentAccess() { accessCounter = false } func discardContentIfPossible() { avatar = nil } func isContentDiscarded() -> Bool { return avatar == nil } }
Swift
복사
이제 Person 캐시를 만들 수 있습니다. 하지만 우리가 해야 할 일이 한 가지 더 있습니다.
기본적으로 NSCache는 포함된 모든 객체를 제거합니다.
이 경우 avatar뿐만 아니라 필요에 따라 Person를 삭제합니다. 이런 문제를 해결하려면 cache에서 제공하는 evictsObjectsWithDiscardedContent 속성을 false로 설정하세요.
cache.evictsObjectsWithDiscardedContent = false
Swift
복사
이 속성은 기본적으로 true 값을 가집니다. 이 속성은 캐시에서 전체 객체를 제거할 지 아니면 해당 객체에서 삭제 가능한 내용만 제거할 지를 제어합니다.
false로 설정하면 전체 사용자가 아닌 avatar만 폐기됩니다.
우리는 Persons의 새 캐시 객체를 새로 만들고 여기에 객체를 추가할 수 있습니다.
let cache = NSCache<NSString, Person>() cache.name = "Person Cache" cache.evictsObjectsWithDiscardedContent = false let andy = Person(firstName: "Andy", lastName: "Ibanez", avatar: UIImage(named: "silight.png")) cache.setObject(andy, forKey: "me")
Swift
복사
이제 캐시가 Person 클래스를 삭제할 때, avatar 값만 삭제될 수 있어요.

The NSCacheDelegate Protocol

포스트를 마치며, 특정 캐시가 무엇을 하는지 확인할 수 있는 NSCacheDelegate 프로토콜에 대해 설명하겠습니다. 현재, delegate에는 cache(_:willEvictObject)라는 객체가 제거되는 시기를 알 수 있는 한 가지 메서드만 있습니다.
func cache(_ cache: NSCache<AnyObject, AnyObject>, willEvictObject obj: Any) { if let person = obj as? Person { print("Cache \(cache.name) will evict person \(person.firstName) \(person.lastName)") } }
Swift
복사
객체가 삭제되려고 할 때 알림이 표시되므로 작업을 수행할 수 있습니다. 지금은 제거되는 대상이 누구인지만 프린트하겠습니다.
(위의 다른 API 예제와 마찬가지로, Objective-C에서 나온 것이므로 우리는 약간의 캐스팅을 해야 합니다.)

마치며

NSCache는 메모리에만 필요한 콘텐츠를 캐싱하는 데 좋은 API입니다. 내용을 수동으로 제어하거나 캐시가 자체적으로 관리할 수 있도록 조건을 설정할 수 있습니다. Core Objective-C 객체이기 때문에 몇가지 요령도 필요하지만, 여전히 사용하기 쉽습니다.