본문 바로가기
🍎 iOS/DevNote

[Swift] NSCache 이해하기

by @Eddy 2024. 3. 17.
728x90

활용 개념

Cache

  • CPU와 HDD의 속도 차이를 메꾸기 위해 고안된 개념으로, Cache라는 임시 메모리를 두어 CPU에서 자주 사용하는 데이터를 임시 저장한 후, 필요할 때 사용한다.
  • 컴퓨터 과학에서는 데이터나 값을 미리 복사해두는 임시 저장소를 의미하며, 위에서 CPU와 HDD 간의 속도 간극을 메우기 위해 사용한 것처럼 데이터 접근 시간이 오래 걸리는 경우나 값을 다시 계산하는 시간을 절약하고 싶은 경우 사용한다.

Memory Cache(메모리 캐시) vs Disk Cache(디스크 캐시)

  Memory Cache Disk Cache
종류 NSCache 등 FileManager, UserDefaults, CoreData 등
특징 휘발성 - 앱 종료 시 메모리에서 해제 비휘발성(영속성) - 파일 형태로 디스크에 저장하여 앱을 종료해도 데이터가 남아있게 되며, 이러한 이유로 앱이 차지하는 용량이 커지게 된다.
저장공간 작다 크다
처리속도 빠름 상대적으로 느리다 ( 네트워크 통신보다는 빠름 )

NSCache

  • Resource가 부족하면 데이터를 소거할 수 있는, 임시 key-value를 저장하는 데 사용하는 mutable Collection을 말한다.
    • Auto-eviction policy(자동 퇴거 정책)을 채택하고 있어 메모리 관리에 유리하다.
    • 저장된 데이터를 재사용하기 때문에 성능상 이점을 가지며, 컨텐츠가 삭제되면 캐시 객체도 자동으로 제거된다.

특징

구조: Doubly Linked List ( 양방향 연결 리스트 )

  1. 중간에 있는 데이터를 추가/삭제 할 때, 메모리 공간 및 시간 효율에 유리.
    • Array는 데이터를 추가/삭제 시 메모리 공간 및 시간 효율이 떨어져서 Linked List로 구현
      ( array.remove(at: )은 O(n) 의 시간복잡도를 가짐. )
    • Singly Linked List ( 단방향 연결 리스트 )는 특정 위치에 데이터 추가/삭제 시,
      O(n)이기 때문에 성능 개선을 위해 Doubly Linked List로 구현
  2. 내부적으로 Dictionary를 두어 접근 시 시간복잡도는 O(1)이다.
    • LinkedList는 Random Access(subscript)가 불가하기 때문에 Dictionary도 함께 구현된 것으로 보임.
 open class NSCache<KeyType : AnyObject, ObjectType : AnyObject> : NSObject { 
 	private var _entries = Dictionary<NSCacheKey, NSCacheEntry<KeyType, ObjectType>>() 
 }

데이터 교체 알고리즘

  • Cached Data에 Cost값을 부여하며, 이를 기준으로 오름차순으로 정렬한다.
    • Cached Data를 소거하는 방식: LowCost를 가진 데이터부터 삭제하여 totalCostLimit을 유지한다.
    • 일반적으로 캐싱된 데이터를 소거하는 방식에 있어, 페이지 교체 알고리즘으로 FIFO, LRU, LFU 등을 사용하는 것과는 다소 차이가 있다.

캐시 데이터 소거 시점

  • 단순히 cache memory에 너무 많은 데이터가 쌓였을 때도 소거되지만, NSDiscardableContent protocol을 사용하면 소거 시점을 변경할 수 있다.

고민해볼만한 부분

1. NSCache vs Dictionary

처음 NSCache를 사용할 때, 어차피 앱이 종료될 때 삭제된다면 Dictionary를 사용해도 되지 괜찮지 않을까? 라고 생각했다. Cache의 역할을 하기 위해 필요한 것은 다음과 같다.

 

  1. 시스템 메모리 공간 부족한지 확인
  2. 메모리 공간 부족 시, ‘기준’에 따른 우선순위가 낮은 데이터 소거
    • 기준: count(갯수), cost(일반적으로 용량)
    • countLimit, totalCostLimit으로 관리되며, 이 기준은 엄격하고 정확하게 따르지는 않는다.
      • 예를 들어, count를 10개로 지정했더라도, 12개의 데이터가 캐싱될 수 있다는 의미이다.
  3. Thread safe
    • 일반적으로 NSCache는 작업 시간이 긴 데이터를 임시 저장하기 위해 사용하고, 작업 시간이 긴 데이터는 보통 multi Thread에서 작업하기 때문에 Thread safe해야하며, 이를 위해 NSLock 객체를 사용하고 있다.
    • open class NSCache<KeyType : AnyObject, ObjectType : AnyObject> : NSObject {
      	
          private let _lock = NSLock() 
          
          ... 
          
          public override init() {}
          
          ... 
          
          open func object(forKey key: KeyType) -> ObjectType? { 
          
          ...
          
          _lock.lock() 
          
          if let entry = _entries[key] { 
          	object = entry.value 
          }
          
      	_lock.unlock()
          
          ...
          
          return object 
      }
  4. 컨텐츠가 삭제되면 NSCache에서도 삭제되어야 한다.
    • Dictionary는 NSCopying 프로토콜을 채택하여 Key값을 복사해 사용한다.
    • 만약 Key값이 복사되지 않고 참조 상태를 유지한다면 Key값이 수정되었을 때, 수정된 key에 대한 value를 찾을 수 없어 문제가 될 수 있기 때문이다.
      • let mutableDic = NSMutableDictionary() 
        var dicKey: NSMutableString = "key"         // 1. 최초 key 생성 
        mutableDic.setObject("one", forKey: dicKey) // 2. 현재 key에 값 할당 
        dicKey.setString("changedKey")              // 3. key 변경 
        mutableDic.setObject("two", forKey: dicKey) // 4. 변경된 key로 새로운 값 할당 
        
        print(mutableDic.object(forKey: "key") ?? "없음") // "one" 
        print(mutableDic.object(forKey: "changedKey") ?? "없음") // "two"
    • 하지만 NSCache는 Key값을 복사하지 않고, 메모리 주소를 참조한다.
      • 여기서, cacheKey = “changedKey” 로 재할당하게 된다면, cache의 기존 key값에 대한 value인 one이 출력되는데, cacheKey를 변경하는 게 아니라 새로운 문자열 객체를 만들어 해당 객체를 가리키는 방식이기 때문이다.
      • 따라서 setString()을 사용해 해당 메모리주소의 값(문자열)을 대체하면 결과값이 제대로 도출된다.
      • let cache = NSCache<NSString, NSString>()
        var cacheKey: NSMutableString = "key"    // 1. 최초 key 생성
        cache.setObject("one", forKey: cacheKey) // 2. 현재 key에 값 할당
        cacheKey.setString("changedKey")         // 3. key 변경
        cache.setObject("two", forKey: cacheKey) // 4. 변경된 key로 새로운 값 할당
        
        print(cache.object(forKey: "key") ?? "없음")        // "없음" 
        print(cache.object(forKey: "changedKey") ?? "없음") // "two"

 

2. NSCache vs URLCache

  • 가장 큰 차이점은 URLCache는 OnDisk와 InMemory의 혼합 방식이고, 자동 제거 정책이 없다는 것이다.
  • 즉, RAM, HDD 영역을 모두 활용 가능해 비휘발성 Cache 메모리 활용이 가능하며, NSCache로는 저장하기 어려운 크기의 데이터도 Cache 메모리로 저장할 수 있다는 장점이 있지만,
  • 자동 제거 정책이 없어 메모리가 가득찰 때까지 캐시 데이터를 유지하게 된다는 단점이 있다.

사용 사례

  • 일반적으로 생성 비용이 많이 드는 객체를 임시 저장할 때 사용. ( 이미지 등 )
    • TableView나 CollectionView의 경우, cell이 Reusable한 특성을 갖고 있어 cell을 Scroll하게 되면 화면 밖으로 사라진 cell을 Reuse하여 cell이 누적되지 않는다.
    • 다시 반대로 Scroll하게 되면 TableView나 CollectionView의 Cell에 새롭게 데이터를 반영해야 하므로 불필요하게 데이터를 여러번 호출하는 상황이 발생한다.
    • 이를 줄이기 위해 Cache의 개념을 활용해 Scroll된 cell의 데이터를 NSCache에 임시로 저장하면, 필요한 데이터를 곧바로 불러올 수 있어 유용하다.
    • 특히, 이미지 url을 통해 image를 decoding할 때에는 네트워크 통신이 필요하기 때문에 매번 새롭게 호출하게 되면 사용자 경험을 저해할 가능성이 높다.
    • 또한 NSCache는 너무 많은 데이터가 누적될 시, 자동으로 일부 데이터를 소거함으로써 메모리 오버헤드를 방지하여 App Crush를 예방할 수 있다.

참고자료

1. https://github.com/apple/swift-corelibs-foundation/blob/main/Sources/Foundation/NSCache.swift

2. https://hcn1519.github.io/articles/2018-08/nscache

3. https://stackoverflow.com/questions/69377994/does-the-different-way-of-handling-key-between-nsmutabledictionary-and-nscache/69378461#69378461

4. https://beenii.tistory.com/187

반응형

댓글