🐸

회사의 프로젝트에서는 Kingfisher를 사용해서 이미지를 다운, 세팅해주고 있는데요.
최근에 Cell이 많은 부분에서 메모리 이슈가 발견되서 Kingfisher의 Cache 를 사용했습니다.
하지만 잘 몰라서 거의 예제를 복붙하는 수준으로 했는데...
그래서 오늘은 iOS에서 지원하는 NSCache 객체에 대해 알아보겠습니다.

NSCache


NSCache란?

NSCache란 일시적인 key-value 쌍을 저장할 수 있는 가변적인(mutable) 컬렉션 입니다.
이 컬렉션에 저장된 데이터는 시스템 리소스가 부족할 때 삭제 될 수 있습니다.

따라서, NSCache를 사용하면 일시적으로 필요한 데이터를 메모리에 캐싱할 수 있으며, 필요 없어지면 시스템에 의해 자동으로 삭제될 수 있습니다. 이를 통해 앱 성능을 개선 할 수 있습니다.

Memory Cache vs Disk Cache

캐시 하면 메모리 캐시, 디스크 캐시의 개념이 둘 다 나오는데,
NSCache는 메모리 기반 캐시(cache In Memory) 입니다.
앱이 실행되는 동안 생성되고 저장되며 앱을 종료하면 소멸됩니다.

Disk Cache는 데이터를 디스크에 저장하는 방식이며, 파일 시스템의 하나의 디렉토리에 데이터를 저장하는 방식입니다.

정의

class NSCache<KeyType, ObjectType> : NSObject where KeyType : AnyObject, ObjectType : AnyObject

OverView

NSCache 객체는 다른 가변 컬렉션과의 차이가 있습니다.

  • 다양한 자동 삭제 정책을 포함하고 있어, 캐시가 시스템의 메모리를 너무 많이 사용하지 않도록 보장합니다. 다른 앱이 메모리를 필요로 할 경우, 이러한 정책에 따라 캐시에서 일부 항목이 제거되어 메모리 footprint가 최소화 됩니다.
  • 캐시에 있는 항목을 다른 스레드에서 추가, 제거 및 쿼리할 때 스스로 캐시를 잠그지 않아도 됩니다. (??)
  • NSMutableDictionary 객체와 달리 NSCache는 삽입된 키 객체를 복사하지 않습니다.

👩‍💻 요기서 말한 다른 Mutable Collection으로는..
NSMutableArray, NSMutableCollection, NSMutableSet, NSMutableOrederedSet 등이 있습니다.


👩‍💻 다양한 자동 삭제 정책이 뭐죠..?

NSCache는 캐시 크기, 가용 메모리, 캐시의 객체 수 등을 고려하여 캐시에서 제거할 객체를 선택합니다.

캐시 정책내용
Default캐시 크기를 제한하지 않고, 최대한 많은 객체를 저장하려 한다.
시스템 메모리가 부족하면 캐시에서 객체를 삭제합니다. 이 때 삭제는 LRU 방식으로 정해져있습니다.
LRU
Least Recently Used Cache Policy
가장 오래 사용되지 않은 객체를 먼저 삭제한다.
LFULeast Frequently Used Cache Policy
사용빈도가 가장 적은 객체를 먼저 삭제한다.
커스텀해서 사용해야 합니다.
Fixed캐시에 저장될 최대 객체 수를 미리 지정한다.
새로운 객체를 추가할 때, 가장 오래 사용되지 않은 객체를 제거합니다.
Custom개발자가 캐시 정책을 커스텀 할 수 있습니다.

👩‍💻 캐시에 있는 항목을 다른 스레드에서 추가, 제거 및 쿼리할 때 스스로 캐시를 잠그지 않아도 됩니다.

이 특징에 대해 더 알아봅시다!
캐시 객체는 여러 스레드에서 동시에 접근될 수 있으므로, 일반적인 다른 Mutable Collection들과 마찬가지로 thread-safe 해야 합니다.
즉, 한 스레드에서 Cache 객체에 접근하는 동안 다른 스레드에서도 접근해서 충돌이 발생하지 않도록 보호해야 한다는 것이죠.

NSCache 클래스에서는 이러한 문제를 처리하기 위해 내부적으로 자동으로 thread-safe를 제공합니다. (굿)
따라서 여러 스레드에서 동시에 Cache에 접근하여 항목을 추가, 제거, 또는 쿼리하려는 경우, 개발자는 스레드를 직접 잠그거나(lock) 대기(wait)하지 않아도 안전하게 사용할 수 있습니다.

이 GOD 특징은 개발자가 별도로 스레드 관리에 신경쓰지 않고도 캐시를 쉽게 구현하고 사용할 수 있도록 돕습니다.


이어서...
일반적으로 NSCache 객체는 일시적인 데이터를 가지는 객체를 저장하는데 사용됩니다.
이런 객체는 생성 비용이 많이 들거나 계산이 복잡한 경우가 많은데, 이 경우 객체를 재사용하면 성능상의 이점을 제공할 수 있습니다.
왜? 캐시된 객체의 값을 다시 계산하지 않아도 되니까요.

하지만 NSCache가 필수도 아니고.. 메모리 제한을 넘어가면 버려질 수 있습니다. 하지만 쓰는게 좋겠죠!


사용하지 않는 하위 구성요소(subcomponents)를 가진 객체는, NSDiscardableContent 프로토콜을 채택해서 Cache의 제거 동작을 개선할 수 있습니다. 디폴트로 Cache의 NSDiscardableContent 객체는 해당 객체의 내용(content)이 삭제되면 자동으로 제거됩니다. 이러한 자동 제거 정책은 변경할 수 있습니다. NSDiscardbleContent 객체가 Cache에 저장되면, 해당 객체가 캐시에서 제거될 때, 캐시는 discardContentIfPossible() 함수를 호출합니다.

그니까 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를 한 번 잡고 다시 코드를 보니 딱 정리된 기분이라 좋네요!

profile
https://hyunndyblog.tistory.com/163 티스토리에서 이사 중

0개의 댓글

Powered by GraphCDN, the GraphQL CDN