본문 바로가기

학습 노트/iOS (2021)

215. Response Caching

Response Caching


항상 네트워크를 통해 작동하도록 하는 것은 매우 비효율적이다.
요정과 응답 사이에 지연이 발생하고, 많은 시스템 자원과 서버 자원을 사용한다.

Cache를 사용하면 이를 조금 해소하는 것이 가능하다.
이를 위해 Cache Policy를 설정해 기본으로 제공하는 Cache Store를 사용하거나 새옵게 정의하는 것이 가능하다.

 

Cache Policy

  • useProtocolCachePolicy
    protocol 형식에 따라 기본 Cahce 정책을 사용한다.
    Server의 Cahce Controller가 존재하면 기본값을 사용한다.
  • reloadIgnoringLocalCacheData
    Local Cache를 무시한다.
    요청 시도마다 매번 네트워크 요청이 발생한다.
    특정 요청에 따른 Cache 갱신이 필요한 경우 사용한다.
  • returnCacheDataDontLoad
    네트워크를 사용하지 않고 항상 Cahce를 사용한다.
    Cache가 존재하지 않는 경우 요청에 실패한 것으로 간주하기 때문에 Cahce 저장 이후에 사용해야 한다.
  • returnCacheDataElseLoad
    Cache가 존재하면 Cache를 불러오고, 존재하지 않으면 네트워크 요청을 발생한다.

 

CacheControl Header

Cahce를 사용하는 경우 데이터를 최신으로 유지하는 것이 중요하다.
서버는 이를 위해 CacheControlHeader를 사용한다.

서버에 요청을 보내고 받은 응답에는 Cache-Control이라는 부분이 존재한다.
값은 public, max-age=10이고, Cache에 저장한 응답을 10초간 사용하겠다는 의미이다.
따라서 해당 시간 내에 다시 요청을 시도하면 서버의 새로운 응답이 아닌 Cache를 사용하게 된다.

따라서 위와 같이 Cache-Control이라는 부분이 존재하지 않는다면
매번 서버의 새로운 요청을 보내게 된다.
따라서 이러한 경우에는 Client(App)에서 직접 Cache를 관리해야 한다.

CachingTableViewController.swift

서버에서 전달해 주는 데이터의 Cache-Control이 10초로 설정돼있기 때문에
앱에서 별 다른 조치를 하지 않더라도 정해진 시간 이후에 새로운 요청이 발생하는 것을 확인할 수 있다.

request.cachePolicy = .returnCacheDataElseLoad

Cache 정책의 변경은 생성된 request의 cachePolicy 속성을 지정하는 것으로 바꿀 수 있다.

지금처럼 returnCahceDataElseLoad를 사용하면 서버에서 지정한 Cache-Control 시간 이후인 10초 뒤에도
서버에 데이터를 요청하지 않는 것을 확인할 수 있다.

즉 Client에서 CachePolicy를 지정하면 서버의 Cache 정책을 무시하게 된다.
이러한 경우 Client의 Cache를 직접 지우고 데이터를 새로 받아 올 수 있도록 구현해야 한다.

 

Cache 삭제하기

urlCache 클래스가 Cache를 삭제하는 메서드를 지원하지만 버그가 다수 존재한다는 문제가 있다.
따라서 Cache를 삭제하는 메서드는 전제 Cache를 삭제하는 경우를 제외하면 권장되지 않는다.
대신 Cache 정책을 일시적으로 변경해 Cache를 덮어쓰도록 구현하는 패턴이 자주 사용된다.

if lastDate.timeIntervalSinceNow < -5 {
   request.cachePolicy = .reloadIgnoringLocalCacheData
   lastDate = Date()
} else {
   request.cachePolicy = .returnCacheDataElseLoad
}

Cache가 마지막으로 저장된 시점부터 일정 시간이 지나면
Cache 정책을 reloadIgnoringLocalCacheData로 변경해 현재 존재하는 Cache를 덮어쓰도록 구현한다.
이후 저장 시점을 갱신함으로써 Cache의 수명 주기를 조절할 수 있다.

config.requestCachePolicy = .reloadIgnoringLocalCacheData

Client의 requestPolicy는 각각 두 군데 적용할 수 있다.
request의 cachePolicy 속성과 sessionConfiguration의 requestCachePolicy 속성이다.

단, 새로 Session의 Cache 생명 주기를 변경했음에도 불구하고,
서버가 반환한 데이터의 Cache-Policy 헤더가 빈 경우 적용이 되지 않는 것을 확인할 수 있다.
이는 다음과 같은 차이가 존재한다.

  • request 정책이 존재하지 않기 때문에 서버의 정책을 무시하고 Session 정책을 사용한다.
  • request 정책이 존재하기 때문에 우선순위가 높은 request 정책을 사용한다.

Session 수준의 설정은 해당 Session으로 생성된 모든 Task에 유효하지만.
request 수준의 설정이 있을 경우 우선순위가 높은 request의 설정을 적용한다.

 

Cache Policy 선택 적용하기

만약 서버의 정책과 Client의 정책을 복수로 사용하려면 '기본 정책'의 명시가 필요하다

request.cachePolicy = .useProtocolCachePolicy

request의 Cache Policy를 useProtocolCachePolicy로 변경한다.

이 상태에서 URLSessionDelegate에 새로운 메서드를 추가한다.

urlSession(_:dataTask:willCacheResponse:completionHandler:)

 

Apple Developer Documentation

 

developer.apple.com

해당 메서드는 서버에서 발생한 응답을 Cache에 저장하기 직전 호출된다.

서버의 응답에는 Binary Data 뿐만이 아닌 메타데이터도 함께 전달되는데 이 메타데이터의 형식은 CachedURLResponse이다.
해당 메서드를 구현하지 않는 경우 DiskCache와 MemoryCache에 둘 다 저장되는데 이를 변경하기 위해 사용된다.

세 번째 파라미터인 willCacheResponse에 Cache에 저장할 인스턴스를 전달하고,
네 번째 파라미터인 CompletionHandler에서 세 번째 파라미터로 전달된 인스턴스를 그대로 저장하는 것이 기본 동작이다.

func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse, completionHandler: @escaping (CachedURLResponse?) -> Void) {
    guard let url = proposedResponse.response.url else {
        completionHandler(nil)
        return
    }
}

우선 세번째 파라미터를 확인해 전달된 인스턴스의 유효성을 검사한다.
유효하지 않다면 completionHandler에 nil을 전달해 Cache에 저장하지 않고 작업을 중지한다.

func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse, completionHandler: @escaping (CachedURLResponse?) -> Void) {
    guard let url = proposedResponse.response.url else {
        completionHandler(nil)
        return
    }

    if url.host == testUrl {
        completionHandler(proposedResponse)
    }
}

만약 응답의 주체가 testUrl과 같다면 해당 인스턴스를 completionHandler에 전달해 Cache에 저장할 수 있도록 한다.

func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse, completionHandler: @escaping (CachedURLResponse?) -> Void) {
    guard let url = proposedResponse.response.url else {
        completionHandler(nil)
        return
    }

    if url.host == testUrl {
        completionHandler(proposedResponse)
    } else if url.scheme == "https" {
        let response = CachedURLResponse(response: proposedResponse.response, data: proposedResponse.data, userInfo: proposedResponse.userInfo, storagePolicy: .allowedInMemoryOnly)
        completionHandler(response)
    }
}

만약 응답의 주체의 url 형식이 'https'라면 MemoryCache에는 저장하지 않도록 설정한다.
CachedURLResponse를 복제하고, storagePolicy를 변경해 이를 completionHandler에 전달한다.

func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse, completionHandler: @escaping (CachedURLResponse?) -> Void) {
    guard let url = proposedResponse.response.url else {
        completionHandler(nil)
        return
    }

    if url.host == testUrl {
        completionHandler(proposedResponse)
    } else if url.scheme == "https" {
        let response = CachedURLResponse(response: proposedResponse.response, data: proposedResponse.data, userInfo: proposedResponse.userInfo, storagePolicy: .allowedInMemoryOnly)
        completionHandler(response)
    } else {
        let response = CachedURLResponse(response: proposedResponse.response, data: proposedResponse.data, userInfo: proposedResponse.userInfo, storagePolicy: .notAllowed)
        completionHandler(response)
    }
}

이외의 경우에는 Cache를 사용하지 않도록 설정한다.
마찬가지로 CachedURLResponse를 복제해 storagePolicy를 변경한다.

CachedURLResponse의 storagePolicy의 종류는 다음과 같다.

URLCache.StoragePolicy

 

Apple Developer Documentation

 

developer.apple.com

위와 같이 구현한 Cache Policy는 기존에 저장되어있던 Cache에는 적용되지 않고, 새로운 Cache부터 적용된다.

Cache 전체 삭제하기

위와 같이 구현한 Cache Policy는 기존에 저장 되어있던 Cache에는 적용되지 않고, 새로운 Cache부터 적용된다.

 

@IBAction func removeAllCache(_ sender: Any) {
   session.configuration.urlCache?.removeAllCachedResponses()
}

기존에 구현했던 대로 정책을 덮어쓰는 방식으로 새로운 Cache를 생성해도 되고,
위와 같이 전체 Cache를 삭제하는 방법도 가능하다.

 

+

실습은 Button을 눌러 수동으로 동작하도록 구현했지만.
실제 개발 시엔 특정 이벤트 발생 시 호출하여 Cache를 관리하도록 구현하는 것이 좋다.

'학습 노트 > iOS (2021)' 카테고리의 다른 글

217. Cellular Connection  (0) 2022.09.07
216. Reachability  (0) 2022.09.06
214. Background Download  (0) 2022.08.31
213. Download Task  (0) 2022.08.30
212. Upload Task  (0) 2022.08.26