본문 바로가기
iOS

iOS Cache

by SeoB-P 2022. 4. 24.

Cache?

캐시(cache, 문화어: 캐쉬, 고속완충기, 고속완충기억기)는 컴퓨터 과학에서 데이터나 값을 미리 복사해 놓는 임시 장소를 가리킨다. 캐시는 캐시의 접근 시간에 비해 원래 데이터를 접근하는 시간이 오래 걸리는 경우나 값을 다시 계산하는 시간을 절약하고 싶은 경우에 사용한다. 캐시에 데이터를 미리 복사해 놓으면 계산이나 접근 시간 없이 더 빠른 속도로 데이터에 접근할 수 있다. - 위키백과

정의에서 볼수 있다시피, 다시 계산하거나 데이터에 접근하는 시간을 줄이기 위해 미리 값을 복사해 놓는 임시 저장소를 의미한다.

특히, 서버와의 작업이 있을때 서버에게 지속적으로 요청하는 것이 아니라, 메모리에 저장되어 있는 Cache를 사용하기 때문에 적절히 사용을 한다면 DBMS나 서버의 부하를 줄이고 빠르게 작업을 처리 할 수있다.

이때문에 여러 시스템에서 현재 활용중이며 대표적으로 Browser Cache, Apacha Cache, DNS Cache등 이 있다.

 

NSCache

Swift에서는 캐싱을 할 때 주로  NSCache클래스를 사용한다.

리소스가 부족할 때 제거될 수 있는 임시 키-값 쌍을 임시로 저장하는 데 사용하는 가변적인 컬렉션.

주의점 및 특징

1. NSCache 클래스는 Auto-eviction policies과 더불어 캐시가 시스템 메모리를 너무 많이 사용하지 않도록 한다.

만약, 다른 응용 프로그램에 메모리가 필요한 경우 Auto-eviction policies들은 캐시에서 일부 항목을 제거하여 메모리 공간을 최소화함.

+ NSCache의 Eviction policy는 LRU + LFU의 Hybrid라고 한다?

참고: https://jeonyeohun.tistory.com/383 

 

- 애플의 기기들은 메모리가 부족해지거나 하면 우선순위에서 밀리는 것들을 제거해서 메모리를 확보하는 경향이 있다.

그런 정책들이 Auto-eviction plicies라고 생각이 되며, NSCache도 다른 Auto-eviction policies과 함께 메모리가 필요한 경우 캐시의 항목들을 지울 수 있다.

 

2. 우리가 직접 캐시를 Lock 않고도 다른 스레드에서 캐시의 항목을 추가, 제거 및 쿼리할 수 있다.

이부분이 잘 이해가 안가긴 했는데, 만약 Cache기능을 사용하기 위해서 Dictionary등을 사용할 경우에 멀티 스레드 환경에서는 

캐시된 값을 안전하게 사용하기 위해서 Lock해주는 등의 작업이 필요하지만, NSCache클래스를 사용하면 좀더 안전하게? 사용할 수 있다는 의미인 듯 하다..

+ NSLock을 이용하여 Lock을 걸기 때문에 Thread Safe하다. 참고

 

3. NSMutableDictionary 개체와 달리 캐시는 캐시에 포함된 Key Objects를 복사하지 않는다.

- NSMutableDictionary는 왜 key를 복사하나?

NSMutableDictionary혹은 NSDictionary는 Key에서 사용되는 모든 객체가 NSCopying 프로토콜을 채택하기 때문에 Key로 사용되는 모든 객체가 복사되어 사용이 된다.

키로 사용되는 값이 사용되는 동안에 바뀌지 않도록 보장이 되어야 하기 때문이다.

만약, key값을 복사하지 않고 사용하게 된다면 key값을 가지고 오는 시점과 쓰는 시점이 다를 때 Dictionary가 바뀌어버린다면, 문제가 생길 수 있을 것이라 생각이 든다.

 

4. Caching할 객체의 최대 수를 지정하거나 Cost Limit를 줄 수 있다.

- 어떠한 개체를 캐시를 추가할때 Cost를 줄 수 있는데 이 Cost들의 총합의 Limit를 주는 것이다.

개체가 추가될때마다 Cost는 자동으로 합산이 되고 만약 설정한 Cost Limit보다 높아지면 Limit보다 낮아 질때까지 캐시를 지운다.

단, NSCache는 개체를 자동으로 지워주지만, 확실한 순서는 보장하지 않는다.

Cost Limit의 Default값은 0이며 만약 0이라면 Cost Limit는 없다.

 

+ 그렇다면 Limit Cost를 설정하지 않으면 얼마까지 Cost를 가질 수 있을까?

Kingfisher의 "KingfisherManager.shared.cache.memoryStorage.config.totalCostLimit"메서드를 이용

측정결과 8192 MB로 측정이 된다.

 

 

주의! Cost를 계산하는 비용이 클 경우 사용 하지 말 것.

- 보통, Cost는 객체의 Byte값등을 이용해 넣는것이 일반적인데 만약, 이 값을 가져오는데 많은 비용이 든다면 좋지 않은 성능을 낼 수 있다.

예를 들어 byte크기를 알기 위해 다시 서버에 요청을 한다던지,, DB를 뒤진다던지 하는 작업은 나쁜 성능을 유발한다.

 

5. NSCache는 연결리스트와 Dictionary를 함께 사용한다.

 - NSCache를 살펴보면 Dictionary와 연결리스트를 동시에 사용하는 것을 확인 할 수 있다.

그 이유는 캐싱은 중간에 있는 데이터를 추가, 삭제가 빈번해서 배열을 사용하면 당기거나 미는 작업이 필요할 수 있다.

이때 당기거나 미는 작업은 많은 시간 복잡도를 유발하므로 연결리스트로 해결 하고자 하는 것 같다.

연결리스트로 해결할수 있지만 연결 리스트는 탐색에 O(n)의 시간이 발생하므로 Dictionary를 이용해서 key로 데이터를 접근해 O(1)로 빠르게 탐색 가능하다.

 

+ 데이터를 추가할때, Cost를 기준으로 NSCache를 오름차순 정리한다.

 

 

Cost Limit를 주지 않는 경우에는 어떻게 계산해서 정렬하고 지울까..?🤷‍♂️

 

6. limitation을 넘어갈 경우 적은 용량(cost)의 데이터부터 삭제한다.

- Limitation을 넘어 갈 경우 cost limitation을 체크하고 count limitation을 체크함.

NSDiscardableContent 객체의 삭제는 연결리스트를 통해 삭제가 되는데 cost로 오름차순 정렬이 되어있다.

따라서, 연결리스트의 Head부터 삭제가 진행되므로 적은 cost의 데이터 부터 삭제 된다.

 

7. 사용되지 않을 때 폐기될 수 있는 subcomponents가 있는 개체는 NSDiscardableContent 프로토콜을 채택하여 캐시 제거 동작을 개선 할 수 있다.

기본적으로 캐시의 NSDiscardableContent 개체는 해당 콘텐츠가 삭제되면 자동으로 제거된다.

하지만 이 자동 제거 정책은 변경가능함!! (자동으로 제거하지 않고 좀더 Custom하게 캐시를 관리 할 수 있다)

 

NSDiscardableContent 프로토콜은 내부적으로 마치 iOS의 ARC처럼 객체 라이프 사이클을 다루는 Counter 변수가 있다.

Counter는 초기에 1로 시작이된다. 

여전히 데이터가 있고 읽히는 동안에는 카운터 변수를 증가시키고 beginContentAccess()메서드 호출시 True를 반환한다.

데이터가 삭제된 시점에는 당연히 No를 반환한다.

따라서, 이 프로토콜을 따르는 객체에 접근하기 전에 beginContentAccess()메서드를 호출하여 현재 값이 접근 가능한지 알아봐야한다. 데이터에 액세스 한 뒤 endContentAccess()를 호출하게 되면 카운터가 감소하게 되며 시스템이 자동으로 삭제가 가능한 상태가 됨.

만약 이 객체를 바로 제거하고 싶다면 discardContentIfPossible()를 호출 하면 된다.

기본적으로 Foundation 에는 이 프로토콜의 기본 구현을 가지고 있는 클래스인 NSPurgeableData를 포함한다.

 

NSDiscardableContent 프로토콜을 준수하는 NSMutableData의 서브 클래스

NSPurgeableData 객체가 NSCache에 추가되었다면 반납될 수 있는 데이터 객체가 반납되면 자동으로 캐시에서 제거됩니다.

 

예시.

let cache = NSCache<NSString, NSPurgeableData>()
let image = UIImage()
let data = NSPurgeableData(data: image.pngData()!)
cache.setObject(data, forKey: "image")

if let cachedData = cache.object(forKey: "image" as NSString) {
    // counter += 1
    if cachedData.beginContentAccess() {
        let image = UIImage(data: cachedData as Data)
        // counter -= 1
        cachedData.endContentAccess()
        // counter == 0
    }
}

코드 출처: https://junyng.tistory.com/41


Cache의 종류(iOS) 

iOS에서 Cache는 크게 두 종류로 구분이 된다.

 

Memory Caching: 어플리케이션에게 할당된 메모리의 일부분을 Caching에 사용함.

- iOS에서 자체적으로 제공해주는 기능

 

- 어플을 끄면 저장된 내용이 삭제됨

 

- NSCache를 통해 사용 가능

 

- 속도가 빠르지만 공간이 작음

 

Disk Caching: 데이터를 파일형태로 기기 내부 Disk에 저장함.

- 어플을 꺼도 기기 내부에 저장되어 있기 때문에 남아 있음.

 

- FileManger를 통해 사용이 가능함.

 

- App을 삭제시에도 캐시에 저장된 데이터를 삭제하지 않을 수 있다.

    App삭제시 데이터 삭제  ->  UserDefault사용

    App이 삭제되어도 캐시가 남아있고 싶다 -> 파일 경로에 저장 

 

- 파일 입출력으로 인해 속도가 Memory Caching보다는 느리지만 저장공간이 비교적 크다.

Ex) 카카오톡

 


이미지 캐싱(Memory Cache와 Disk Cache를 동시에 사용)

이미지를 가져오는 작업은 많은 비용을 유발 할 수 있다.

Memory Cache와 Disk Cache를 동시에 사용해서 성능상 이점을 만들어 보자.

 

1. Memory Cache에 이미지가 있는지 검색

 

2. 없으면, Disk Cache에서 검색

 

3. 없으면, URL에서 이미지 async하게 로드

 

4. 메모리 캐시와 디스크 캐시에 이미지 저장

 

5. 앱이 실행되는 동안에, 이 후 요청시 메모리 캐시에 저장

 

6. 앱이 다시 실행되었다면 디스크에서 캐시를 불러온 후 메모리 캐시에 저장.

 

 

예시.

//imageCache를 담당할 싱글톤 클래스
class ImageCacheManger {
    static let shared = NSCache<NSString,UIImage>()
    private init() { }
}

 

import UIKit

//imageCache를 담당할 싱글톤 클래스
class ImageCacheManger {
    static let shared = NSCache<NSString,UIImage>() //memory
    private init() { }
}

class ViewController: UIViewController {
    
    @IBOutlet weak var imageView0: UIImageView!
    @IBOutlet weak var imageView1: UIImageView!
    @IBOutlet weak var imageView2: UIImageView!
    @IBOutlet weak var imageView3: UIImageView!
    @IBOutlet weak var imageView4: UIImageView!
    
    private var imageViewOrder = 0
    
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    func loadimage(imageView: UIImageView) {
        
        guard let imageURL = URL(string: "https://user-images.githubusercontent.com/80263729/165081176-7a7c6f3a-7fdd-44ce-9037-91f20b3112ec.png") else { return }
        
        //URL의 마지막 Path Component로 Key를 만듬.
        let cacheKey = NSString(string: imageURL.lastPathComponent)
        
        print(ImageCacheManger.shared.object(forKey: cacheKey))
        
        //1. Cache에 이미 image가 있다면 image를 가져와서 넣고 함수를 종료함. (Memory Cache확인)
        DispatchQueue.main.async {
            if let cachedImage = ImageCacheManger.shared.object(forKey: cacheKey) {
                imageView.image = cachedImage
                return
            }
        }
        
        //2. Memory Cache에 없다 -> diskCache를 찾아봄
        let fileManager = FileManager()
        
        //Cache가 저장되는 path(Disk Caching)
        guard let path = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first else { return }
        print("캐쉬가 저장되는 폴더 path(String): \(path)")
        
        var filePath = URL(fileURLWithPath: path)
        print("폴더 Path에서 file을 찾을 수 있는 Path(URL): \(filePath)")
        
        filePath.appendPathComponent(imageURL.lastPathComponent)
        print("폴더 Path에서 특정 file을 찾기 위한 PathComponent을 붙임.\(filePath)")
        print("다시 파일을 찾을때 fileManger에서 쓸 Path: \(filePath.path)")
        
        //disk에서 cache된 파일이 있다.
        //3. Cache된 데이터를 이용해서 이미지를 띄운다.
        //4. 다음을 위해서 memoryCache에 넣는다.
        if fileManager.fileExists(atPath: filePath.path) {
            guard let imageData = try? Data(contentsOf: filePath), //원격 서버 X , 내부파일
                  let image = UIImage(data: imageData) else { return }
            DispatchQueue.main.async {
                imageView.image = image
            }
            ImageCacheManger.shared.setObject(image, forKey: cacheKey)    //cacheKey로 image를 등록함.
            return
        }
        
        URLSession.shared.dataTask(with: imageURL) { [weak self] data, _, _ in

            //disk에도 cache된 파일이 없다.
            //5. disk에 파일을 생성한다.
            //6. MemoryCache와 Disk 모두 없기 때문에 URL에서 비동기로 가지고 온다.
            //7. Memorycache와 Disk 모두 저장해둔다.
            guard let imageData = data,
                  let image = UIImage(data: imageData) else { return }
            if !fileManager.fileExists(atPath: filePath.path) {
                fileManager.createFile(atPath: filePath.path, contents: image.pngData(), attributes: nil)
                ImageCacheManger.shared.setObject(image, forKey: cacheKey)    //cacheKey로 image를 등록함.
                DispatchQueue.main.async {
                    imageView.image = image
                }
                return
            }
        }.resume()
    }
    
    //로드 image
    @IBAction func ShowBonoButton(_ sender: UIButton) {
        let imageViews = [imageView0, imageView1, imageView2, imageView3, imageView4]
        
        loadimage(imageView: imageViews[imageViewOrder]!)
        
        if imageViewOrder < 4 {
            imageViewOrder += 1
        } else {
            imageViewOrder = 0
        }
    }

Cache가 저장되는 path

만약, 이 path를 터미널에 open명령과 함께 입력하면 finder로 Cache파일을 찾을 수 있다.(단, 매 실행마다 UUID가 바뀌니 조심할 것!)

finder에서 폴더 찾기
path로만든 file을 찾아갈 경로 잘 보면 앞에 file이라는 것을 알려주는 것이 추가가 되었다.
지정된 file을 찾아가는 최종 경로
Cache를 Print했을때 나오는 값.

Cache된 파일이 우리가 지정한 이름으로 잘 들어가 있음을 확인 가능!!

 

또한, 이미지를 불러오는 시간이 초반에 비해 많이 줄어들음을 체감할 수 있다!!!

 

추가.여기서 저 왼쪽폴더로 들어가게되면..

fsCachedData라는 폴더가 있다. 신기하게도 disk에 저장하는 로직을 작성하지 않아도 Cache된 데이터가 있음을 확인할 수있는데..

분명 나는 disk에 저장하는 로직을 작성하지 않았는데 왜 Cache된 파일이 있지?? 

 여기에는 URLCache가 저장되는 곳이다.

DiskCache에 저장하고 싶지 않아도 자동으로 생성되는 폴더이며 우리가 요청한 이미지가 들어 가 있을 수 있다.

만약, 자신이 DisckCache를 전혀 쓰고 싶지 않고 메모리로만 관리를 하고 싶다면 밑을 참고!

https://stackoverflow.com/a/34808289 

 

 

참고: 

https://jaehun2841.github.io/2018/11/07/2018-10-03-spring-ehcache/#%EB%93%A4%EC%96%B4%EA%B0%80%EB%A9%B0

https://jryoun1.github.io/swift/Cache/

https://developer.apple.com/documentation/foundation/nscache

https://junyng.tistory.com/41 

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

https://developer.apple.com/library/archive/documentation/Performance/Conceptual/ManagingMemory/Articles/CachingandPurgeableMemory.html

https://www.swiftbysundell.com/articles/caching-in-swift/

https://nsios.tistory.com/58 

'iOS' 카테고리의 다른 글

iOS의 뷰가 그려지는 과정  (0) 2022.06.19
서버없이 Networking Test하기 with URLProtocol  (2) 2022.05.15
Responder Chain  (3) 2022.03.29
iOS에서 일어난 touch가 처리되는 과정.  (0) 2022.03.29
AutoLayout - 2  (0) 2021.08.23

댓글