보통 아요에서는 이미지를 다룰 때 메모리 캐시로 관리한다.
근데 이번에 다룬건 이미지는 아님
넥스터즈에서 만든 음력기념일 서비스에서 등록된 기념일의 상세 정보를 볼 수 있는 뷰가 있는데, 이 뷰가 나타날(onAppear) 때마다 서버에 이 정보를 요청한다. 그리고 연필모양의 편집 버튼을 누르면 나오는 뷰가 나타날 때나 편집을 취소하고 나올때나 편집을 완료하고 이 화면으로 돌아올 때도 매번 요청한다. 매번 네트워크 기반의 api를 주고받는 것보다, 한 번 받아온 정보는 메모리 캐시에 담아두고 정보가 수정되지 않는 이상은 캐시에 있는 값을 확인하는 게 효율적일 것 같아서 수정해봤다.
import Foundation
final class CacheManager {
static let shared = CacheManager()
private let cacheDetails = NSCache<NSString, AnniversaryDetail>()
init() {
cacheDetails.countLimit = 20
}
func loadDetail(_ anniversaryId: Int) -> AnniversaryDetail? {
let key = NSString(string: "\(anniversaryId)")
if let cached = cacheDetails.object(forKey: key) {
return cached
}
return nil
}
func setDetail(_ detail: AnniversaryDetail) {
let key = NSString(string: "\(detail.dto.anniversaryId)")
cacheDetails.setObject(detail, forKey: key)
}
func removeDetail(_ anniversaryId: Int) {
let key = NSString(string: "\(anniversaryId)")
cacheDetails.removeObject(forKey: key)
}
}
CacheManager라는 싱글턴 객체를 생성한다. 그리고 메모리 캐시로 사용한 NSCache는 이 안에서 private하게 생성해주었다. 이니셜라이저에서 캐시가 최대 20개의 오브젝트만 담을 수 있도록 제한을 걸었다. 디폴트가 0인데, 0이면 limit이 없기 때문에 무한하게 쌓을 수 있게 되고 사용가능한 공간보다 더 많이 담게되면 크래시가 나서 앱이 죽게 된다. NSCache에는 countLimit 말고도 totalCostLimit이라는 프로퍼티도 존재하는데, 이 둘의 차이는 개수에 대한 제한인가, 코스트(size in bytes와 같은)에 대한 제한인가로 볼 수 있다.
costLimit
The maximum number of objects the cache should hold.
If 0, there is no count limit. The default value is 0. This is not a strict limit—if the cache goes over the limit, an object in the cache could be evicted instantly, later, or possibly never, depending on the implementation details of the cache.
totalCostLimit
The maximum total cost that the cache can hold before it starts evicting objects.
If 0, there is no total cost limit. The default value is 0. When you add an object to the cache, you may pass in a specified cost for the object, such as the size in bytes of the object. If adding this object to the cache causes the cache’s total cost to rise above totalCostLimit, the cache may automatically evict objects until its total cost falls below totalCostLimit. The order in which the cache evicts objects is not guaranteed.This is not a strict limit, and if the cache goes over the limit, an object in the cache could be evicted instantly, at a later point in time, or possibly never, all depending on the implementation details of the cache.
그리고 이 둘은 엄격하게 제한하지 않는다. 캐시가 한도에 다다르면 오브젝트를 즉시 내보낼 수도, 나중일지도 혹은 평생 안내보낼지도 모른단다. 캐시의 세부 구현조건에 따른다고 한다. totalCostLimit의 설명에 따르면 캐시에서 오브젝트가 지워지는 순서도 보장되지 않는다. 나는 이미지는 다루지 않았고, '챙겨챙겨'를 이용하며 한번에 등록하거나 조회할 기념일의 수가 그렇게 많을 것이라고 생각되지는 않아서 costLimit만 20정도로 설정해 주었다.
그리고 캐시에 있는 정보를 조회하는 loadDetail(), 캐시에 담는 setDetail(), 캐시에서 제거하는 removeDetail()를 각각 정의해준다.
디테일을 조회해오는 함수가 호출될 때 캐시된 데이터가 있는지 먼저 확인한다. 있으면 api 요청을 보내지 않고 반환된 값을 담는다. 없으면 요청 보내서 받아오고 받아온 데이터를 캐시에 저장한다. 캐시에서 삭제하는 놈은 기념일을 수정이 완료되면서 호출된다. 그럼 수정이 완료되고 편집뷰가 dismiss하면서 상세정보 조회 뷰가 나타나면 다시 캐시를 확인하고, 지워져서 없으니 정보를 요청해올거다. 편집된 정보를 받아서 다시 캐시에 저장하게 된다. 편집하고 돌아오면서도 캐시에 저장된 정보를 수정할까 했는데, 변경된 데이터는 새로 받아오는게 로직상 깔끔할 것 같아서 안건드림ㅎ
그럼 생각한대로 구현이 완료됐다!
- 한 번 조회하면서 서버로부터 상세정보를 받아온 데이터는 캐시에 담아, 나갔다가 다시 들어가도 fetch 요청을 보내지 않음
- 상세정보를 편집하면 캐시에 담긴 데이터가 삭제되고, 편집완료되면 다시 해당 기념일에 대한 정보를 fetch
메모리 캐시는 앱이 종료되어서 메모리에서 해제되면 리소스들도 같이 전부 반환되어 사라진다. 내가 어떤 기념일에 대한 상세정보를 조회해서 NSCache에 해당 정보가 저장되었더라도, 앱을 나갔다오면 비워져서 동일한 기념일을 보려면 서버에 데이터를 요청해야 할 것이다.
앱을 종료해도 데이터가 남아있는 디스크 캐시도 존재한다. Swift에서는 주로 FileManager 객체를 사용한다.
그런데 무조건 api 호출 횟수가 적은게 좋은건진 모르겠다. aws에 들어가서 pricing 정책도 찾아보고 지박사와 서버 개발자에게도 물어봤지만 과금의 요소가 너무 다양하고 호출 횟수가 큰 영향을 미치지는 않아 정답은 못찾았다 ㅎ 일단 캐시 메모리의 목적 자체는 자주 접근하는 정보에 대해 빠른 응답을 내놓기 위함이고, 메모리 캐시의 성능이 좋고, 네트워크 기반의 동작을 줄일 수 있다는 점에서는 아주 조금 더 효율적일 것 같아서 적용해 보았다.
메모리 캐시 편 끗 -
References
- NSCache는 Thread-Safe하다
- 똑같이 Key-Value로 값을 저장하는 딕셔너리와의 차이점, 캐시의 구조을 잘 볼 수 있는 글 (feat. Cache.lock)