[Swift] Cache

Updated:

들어가며

프로젝트를 진행하는 도중에 서버로부터 이미지를 불러오는 작업이 있다. 이때 매번 이미지를 불러오는 것은 과연 효율적일까?

이미지의 크기가 작다면 그나마 로딩이 없겠지만, 만약 이미지가 크다면 이미지를 불러오는 화면으로 갈 때마다 로딩이 발생하게 될 것이다. 따라서 이를 해결하는 방법인 캐싱에 대해서 알아보고 적용해보자.

Cache

먼저 캐시가 무엇인지 알아보자.

💡 캐시(cache)는 데이터나 값을 미리 복사해 놓는 임시 장소를 가리킨다. 캐시는 캐시의 접근 시간에 비해 원래 데이터를 접근하는 시간이 오래 걸리는 경우나 값을 다시 계산하는 시간을 절약하고 싶은 경우에 사용하며, 캐시에 데이터를 미리 복사해 놓으면 계산이나 접근 시간 없이 더 빠른 속도로 데이터에 접근할 수 있다.

그렇다면 필요할 때마다 서버에서 다운 받는 것이 아니라 임시 저장 장소인 캐시에 다운 받은 이미지를 넣어두고 불러오면 효율적일 것이라고 생각된다.

iOS에서의 Cache

iOS에서는 크게 애플리케이션을 만들 때 2가지 종류의 캐시를 사용하게 된다.
메모리 캐시와 디스크 캐시이다.

📱 Memory Cache

  • iOS에서 자체적으로 제공해주는 캐시
  • App을 끄면 캐시에 저장된 내용이 사라짐
  • NSCache를 통해서 사용 가능
  • 처리속도가 빠르지만 저장 공간이 작다

💾 Disk Cache

  • 캐시에 저장할 데이터를 기기 내부에 아카이빙 하는 방식으로 App을 껐다가 켜도 데이터가 사라지지 않고 남아있다
  • FileManager를 통해 사용 가능
  • App을 삭제할 때 캐시에 저장된 데이터를 삭제하게 만들수도 있고, 그렇지 않고 계속 남아있게 만들수도 있다
    • UserDefault를 사용해 간단하게 저장하면, App 삭제시 데이터도 같이 삭제됨
    • 파일 경로에 이미지를 저장하면, App이 삭제되어도 캐시가 남아있게 됨
      (보통 파일 경로에 이미지 저장)
  • 저장공간은 비교적 크지만, 파일 입출력으로 인해 처리속도가 메모리 캐시보다 느림
    (그러나 네트워크 통신을 통해서 다운로드 하는 것 보다는 훨씬 빠름)
  • 사용 예시로 카카오톡에서 이미지나 동영상을 디바이스에 저장하지 않고 눈으로 보기 위해 다운 받은 경우, Disk caching 되어 앱을 종료했다가 다시 실행해도 볼 수 있게 된다.

이미지 캐싱 과정

이미지를 캐싱하기 위해서는 다음과 같은 step을 따라서 확인해 봐야 한다. (메모리 캐시와 디스크 캐시를 모두 사용하는 경우)

  1. 메모리 캐시에서 이미지를 검색
  2. 없는 경우, 디스크 캐시에서 이미지를 검색
  3. 없는 경우, URL에서 이미지를 비동기 로드
  4. 메모리 캐시와 디스크 캐시에 해당 이미지를 저장
  5. 다음 번 요청시에는 메모리 캐시에서 이미지를 불러옴
  6. 프로세스 재시작 이후의 요청에는 디스크 캐시에서 불러온 후 메모리 캐시에 추가

⚠️ UIImage 이니셜라이저의 차이점은?

앱이 이미지를 읽어 들일때 파일 시스템을 통해 이미지 파일을 열고 데이터를 읽어야하므로, I/O가 발생하는데 매번 이미지를 읽을 때마다 I/O는 성능에 큰 영향을 끼친다. 따라서 이를 해결하기 위한 방법이 캐싱이다.

UIImage(named:)

  • 생성한 이미지 객체는 한 번 읽어본 이미지를 메모리에 저장해둔 다음에 두번째 호출부터 메모리에 저장된 이미지를 가져옴
  • 그러나 이렇게 저장된 메모리는 이미지 객체를 다 사용한 후에도 잘 해제되지 않음

UIImage(contentsOfFile:)

  • 이미지 객체로 인한 메모리 점유가 걱정인 경우 해당 생성자를 사용하면 생성된 이미지 객체는 캐싱되지 않음
  • 그러나 이미지 데이터를 매번 다시 읽어와야 하므로 앞의 방식보다 성능이 약간 저하될 가능성 있음

Swift에서 Caching 적용해보기

NSCache

리소스가 부족하면 삭제될 수도 있는 임시 key, value 쌍을 일시적으로 저장하는 가변 콜렉션이다.

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

Cache 객체는 아래와 같은 이유로 다른 변이 가능한 콜렉션과는 다르다고 할 수 있다.

  • NSCache 클래스는 캐시가 시스템 메모리를 너무 많이 사용하지 않도록 다양한 자동 제거 정책을 수용
    만약 다른 Application에서 메모리 필요한 경우, 캐시에서 일부 항목을 제거하여 메모리 공간을 최소화
  • Thread-safe 하기 때문에 캐시를 lock(잠글)할 필요 없이 다른 스레드에서 캐시의 항목 추가, 제거 등 가능
  • NSMutableDictionary 객체와는 다르게, 캐시는 저장된 key 객체들을 복사하지 않음

따라서 주로 NSCache를 사용해서 생성하는데 비용이 많이 드는 임시 데이터를 저장하는데 사용된다. 재생성하거나, 다시 계산할 필요가 없으므로 성능상의 이점을 가져갈 수 있다. 그러나 메모리가 부족한 경우에는 언제든지 삭제될 수 있고, 삭제되면 필요할 때 다시 생성하거나 계산되어야한다.

💡 이때 사용하지 않을 때 캐시에서 제거되어도 괜찮은 객체는 NSDiscardableContent 프로토콜을 채택하여 캐시 제거에 있어서 개선을 할 수 있다. NSDiscardableContent 객체는 기본적으로 counter가 1로 설정되어있어서 메모리 시스템에 의해서 바로 제거되지 않는다. 따라서 counter를 0으로 설정하면, 캐시는 제거할 때 discardContentIfPossible() 함수 호출 시 바로 메모리로부터 할당 해제 될 수 있다.

⚠️ NSDictionary는 왜 key 값을 복사할까?

먼저 NS(mutable)Dictionary 의 경우, Key에서 사용되는 모든 객체는 NSCopying 프로토콜을 채택하므로 Key로 사용되는 모든 객체가 복사되어 사용하게 된다. 그 이유는 복사본은 키로 사용되는 값이 키로서 사용되는 동안에 바뀌지 않도록 보장하기 때문이다.

만약 key값으로 String 타입을 가지는 Dictionary가 key를 copy하지는 않고 retain만 해서 사용한다고 해보자. 이때 key 값인 String을 수정했을때 그에 해당하는 저장된 값을 찾을 수 없게 된다. 따라서 이러한 실수를 미리 방지하기 위해서 사전에 모든 key를 복사하여 사용하는 것이다.

Memory Cache 코드로 보기 💻

  1. ImageCacheManager 싱글톤 클래스를 만들어 주자.
import UIKit
final class ImageCacheManager {
    static let shared = NSCache<NSString, UIImage>()
    private init() {}
}

NSCache는 <KeyType, ObjectType> 형태로 이루어져 있고, URLString을 키 값으로 구분하고 image를 넣어줄 것이다.

  1. 이미지를 가져오기 전에 캐싱된 데이터에 해당 이미지가 있는지 검사
    • 있다면 이미지를 사용
    • 없다면 3번으로 이동
  2. 네트워크 통신을 하여 비동기처리로 이미지를 가져와서 사용
  3. ImageCacheManager의 캐시에 새롭게 가져온 이미지를 저장
extension UIImageView {
    func loadImage(_ imageID: String) {
        let cacheKey = NSString(string: imageID) 
        if let cachedImage = ImageCacheManager.shared.object(forKey: cacheKey) { // 2번 수행
            self.image = cachedImage
            return
        }
        
      	// 이때 NetworkManager.shared.loadImage() 함수는  
        // 기존의 이미지 id를 받아서 네트워크 통신하여 비동기로 이미지를 가져와서 반환하는 함수
        NetworkManager.shared.loadImage(imageID: imageID) { result in // 3번 수행
            switch result {
            case .success(let data):
                if let data = data, let image = UIImage(data: data) {
                    DispatchQueue.main.async { [weak self] in
                        ImageCacheManager.shared.setObject(image, forKey: cacheKey)// 4번 수행
                        self?.image = image
                    }
                }
            case .failure:
                self.image = UIImage()
                return
            }
        }
    }
}


참고문서

Apple document NSCache
Apple document UIImage init(named:)
Apple document UIImage init(contentsOfFile:)
Caching File Downloads
사용자 냄수
이미지 캐싱 - 메이슨
Why NSDictionary copy key object
이미지 캐시 처리와 NSCache - E tag 처리

Tags:

Categories:

Updated:

Leave a comment