회사의 프로젝트에서는 Kingfisher를 사용해서 이미지를 다운, 세팅해주고 있는데요.
최근에 Cell이 많은 부분에서 메모리 이슈가 발견되서 Kingfisher의 Cache 를 사용했습니다.
하지만 잘 몰라서 거의 예제를 복붙하는 수준으로 했는데...
그래서 오늘은 iOS에서 지원하는 NSCache 객체에 대해 알아보겠습니다.
NSCache란 일시적인 key-value 쌍을 저장할 수 있는 가변적인(mutable) 컬렉션 입니다.
이 컬렉션에 저장된 데이터는 시스템 리소스가 부족할 때 삭제 될 수 있습니다.
따라서, NSCache를 사용하면 일시적으로 필요한 데이터를 메모리에 캐싱할 수 있으며, 필요 없어지면 시스템에 의해 자동으로 삭제될 수 있습니다. 이를 통해 앱 성능을 개선 할 수 있습니다.
캐시 하면 메모리 캐시, 디스크 캐시의 개념이 둘 다 나오는데,
NSCache는 메모리 기반 캐시(cache In Memory) 입니다.
앱이 실행되는 동안 생성되고 저장되며 앱을 종료하면 소멸됩니다.
Disk Cache는 데이터를 디스크에 저장하는 방식이며, 파일 시스템의 하나의 디렉토리에 데이터를 저장하는 방식입니다.
class NSCache<KeyType, ObjectType> : NSObject where KeyType : AnyObject, ObjectType : AnyObject
NSCache 객체는 다른 가변 컬렉션과의 차이가 있습니다.
👩💻 요기서 말한 다른 Mutable Collection으로는..
NSMutableArray, NSMutableCollection, NSMutableSet, NSMutableOrederedSet 등이 있습니다.
NSCache는 캐시 크기, 가용 메모리, 캐시의 객체 수 등을 고려하여 캐시에서 제거할 객체를 선택합니다.
캐시 정책 | 내용 |
---|---|
Default | 캐시 크기를 제한하지 않고, 최대한 많은 객체를 저장하려 한다. 시스템 메모리가 부족하면 캐시에서 객체를 삭제합니다. 이 때 삭제는 LRU 방식으로 정해져있습니다. |
LRU | Least Recently Used Cache Policy 가장 오래 사용되지 않은 객체를 먼저 삭제한다. |
LFU | Least Frequently Used Cache Policy 사용빈도가 가장 적은 객체를 먼저 삭제한다. 커스텀해서 사용해야 합니다. |
Fixed | 캐시에 저장될 최대 객체 수를 미리 지정한다. 새로운 객체를 추가할 때, 가장 오래 사용되지 않은 객체를 제거합니다. |
Custom | 개발자가 캐시 정책을 커스텀 할 수 있습니다. |
이 특징에 대해 더 알아봅시다!
캐시 객체는 여러 스레드에서 동시에 접근될 수 있으므로, 일반적인 다른 Mutable Collection들과 마찬가지로 thread-safe 해야 합니다.
즉, 한 스레드에서 Cache 객체에 접근하는 동안 다른 스레드에서도 접근해서 충돌이 발생하지 않도록 보호해야 한다는 것이죠.
NSCache 클래스에서는 이러한 문제를 처리하기 위해 내부적으로 자동으로 thread-safe를 제공합니다. (굿)
따라서 여러 스레드에서 동시에 Cache에 접근하여 항목을 추가, 제거, 또는 쿼리하려는 경우, 개발자는 스레드를 직접 잠그거나(lock) 대기(wait)하지 않아도 안전하게 사용할 수 있습니다.
이 GOD 특징은 개발자가 별도로 스레드 관리에 신경쓰지 않고도 캐시를 쉽게 구현하고 사용할 수 있도록 돕습니다.
이어서...
일반적으로 NSCache 객체는 일시적인 데이터를 가지는 객체를 저장하는데 사용됩니다.
이런 객체는 생성 비용이 많이 들거나 계산이 복잡한 경우가 많은데, 이 경우 객체를 재사용하면 성능상의 이점을 제공할 수 있습니다.
왜? 캐시된 객체의 값을 다시 계산하지 않아도 되니까요.
하지만 NSCache가 필수도 아니고.. 메모리 제한을 넘어가면 버려질 수 있습니다. 하지만 쓰는게 좋겠죠!
그니까 NSDiscardableContent 프로토콜을 채택한 객체는... 이 객체의 content가 없어질 때 메모리에서 자동으로 제거될 수 있으므로 Cache의 효율을 끌어올릴 수 있다 이거 같습니다.
아주 기본적인 형태의 NSCache를 사용해보겠습니다.
고정된 URL을 통해 이미지를 받아오고, 캐시에 저장하고, 다음 요청이 들어왔을 때 캐시에 이미지 객체가 있다면 캐시에 있는걸 꺼내서 쓰는 코드 입니다.
1️⃣ 캐시 객체 정의
let imageCache = NSCache<NSString, UIImage>()
NSCache 객체는 NSCache<키 타입, 오브젝트 타입>으로 정의합니다.
2️⃣ 캐시 limit 정의
imageCache.countLimit = 20
imageCache.totalCostLimit = 10 * 1024 * 1024
캐시가 또 너무 커지면 안되겠죠?
캐시 안의 객체는 20개, 캐시 크기는 10MB로 정의합니다.
3️⃣ 캐시 등록
async하게 network로 부터 이미지를 받아왔다고 합시다.
받아온 이미지를 캐시에 등록해줘야겠죠?
imageCache.setObject(image, forKey: "원하는 키 String")
너무 간단합니다.
4️⃣ 캐시 검사
다음에 이미지를 새로 불러올 때는 캐시에서 꺼내와야겠죠?
NSCache는 key-value 가변 컬렉션이니 키 값으로 꺼내옵니다.
if let cacheImage = imageCache.object(forKey: "원하는 키 String") {
print("캐싱된 이미지 가져왔나요?")
return cacheImage
}
전체 코드는 이렇겠네요
func fetchImage() async throws -> UIImage? {
// 3. 캐시된 이미지 객체 체크
if let cacheImage = imageCache.object(forKey: "randomImage") {
print("캐싱된 이미지 가져왔나요?")
return cacheImage
}
// Download Image asynchronously
let request = URLRequest(url: self.getImageURL(width: 100, height: 100))
async let (data, response) = URLSession.shared.data(for: request)
guard (try await response as? HTTPURLResponse)?.statusCode == 200 else {
throw HyunndyError.badNetwork
}
guard let image = UIImage(data: try await data) else {
throw HyunndyError.badNetwork
}
// 4. 이미지 캐시
imageCache.setObject(image, forKey: "randomImage")
return image
}
이 이미지 캐시가 가장 잘 쓰일 곳은 어디일까요?
아마 파바바바바박 스크롤되는 CollectionView, TableView 겠죠?
위 코드를 cellForRowAt(_:) 함수에서 사용한다면 스크롤 할 때 마다 이미지를 로드하지 않아도 되니 큰 성능상의 이점을 가질 수 있겠죠!
👩💻 주의할점!
totalCostLimit, countLimit은 최신폰이 아닌 구형 폰이나 용량이 얼마 남지 않은 폰에서도 메모리, 퍼포먼스 체크를 하며 정해야 합니다. 메모리 릭 잡는게 목표인 작업인데 너무 크게 잡혀있으면 최신폰은 감당하지만 구형폰이나 메모리가 부족한 폰으로 갈 수록 드드드득- 현상이 발생할 수 있거든요.
Kingfisher를 약간 기계적으로 쓰면서 항상 맘에 걸렸었는데,
NSCache를 한 번 잡고 다시 코드를 보니 딱 정리된 기분이라 좋네요!