프로그래밍/Android

[안드로이드] Retrofit2 오프라인 캐시 구현하기 (Offline cache interceptor)

Lou Park 2021. 8. 30. 02:06

캐싱의 이점

서버 대역폭 낭비를 줄일 수 있다.

응답을 받기까지 기다리는 시간을 줄일 수 있다.

서버 트래픽을 줄여준다.

 

Cache-Control이란 무엇인가?

Cache-Control은 브라우저단의 캐싱 정책을 정의하는 HTTP 캐시 헤더다. 클라이언트가 서버에게 요청을 호출하면, 브라우저는 자원 사본을 캐싱 하여 다음 번 접근 시에 서버에 처음부터 다시 파일을 요청하는 일이 없도록 할 수 있다. Cache-Control을 통해서 캐시 정책을 세울 수 있다.

 

쉽게 확인 할 수 있는 Cache-Control

 

자주 쓰는 Cache-Control Directives

Directives를 한국말로 뭘로 하면좋을까.. 지시사항? 아무튼 그걸 살펴보려하는데, 여러 Directive들은 콤마(,)로 구분 가능하다. Directive를 이용하여 응답(Response)이나, 요청(Request)이 캐시 가능 여부와 위치, 캐싱전 Origin 서버에게 검증받아야 하는가등을 정의한다.

 

max-age=

max-age는 최대 사용 기간이다. 브라우저가 얼마나 오랫동안 캐시를 이용할 수 있는지를 상술한다. max-age=90 이라고 쓴다면 브라우저에 남아있는 HTTP 응답은 90초 동안 쓸만한(fresh) 캐시라고 간주된다.

냉장고에서 꺼낸지 몇 초된 맥주가 시원하다고 할 수 있느냐-! 하면 max-age=600정도? 더 지나면 냉장고에서 새 맥주를 꺼내오는게 더 낫다.

 

max-stale=

max-stale은 최대 부실 값이다. 콘텐츠 만료 후 상한으로 어느정도 후 까지 받아들일 수 있느냐를 정의한다.

유통기한 지난 후 얼마까지 먹을 수 있는가와 비슷하다. 난 우유는 max-stale=86400 \* 3 정도까지는 괜찮은 듯...

 

no-cache

캐시를 브라우저에 저장하지만 캐시가 유효한지 매번 서버에 요청한다.

 

no-store

캐시를 저장하지 않으며. 매 요청시마다 서버에서 응답을 가져온다. 계좌 정보등 민감한 데이터를 다루는데 적합하다.

 

public

모든 요청에 대해 캐시를 저장한다.

 

private

브라우저의 캐시에'만' 저장된다. CDN에는 캐시를 저장하지 않는다.

 

Cache-Control 사용시 주의점

일반적으로 GET method를 이용해서만 캐싱이 사용가능하다. GET의 경우 URL이 같다면 어떠한 경우에라도 같은 결과를 내기 때문이다. 이를 멱등성이 있다고한다(idempotent operation). 하지만 다른 method는 Body에 실린 값에 따라 다른 결과를 내기 때문에 일반적으로는 캐시 사용이 불가능하다. 하지만 POST method의 경우 잘 찾아보면 캐시를 구현한 예제를 찾을 수 있는 듯하다.

 

안드로이드 Retrofit2에서 OfflineCacheInterceptor 구현하기

앱 이용경험 측면에서 디바이스에서 갑자기 잠시동안 인터넷이 끊기거나, 혹은 기타 다른 사유로 인터넷이 되지 않을때도 안정적으로 네트워크 응답을 내려 주면 좋다. 흠...너무 문장을 잘 못썼는데 아무튼 오프라인 캐시를 구현해볼거란 말이다.

*Interceptor: 네트워크 리퀘스트를 가로채어 중간에서 읽고, 수정할 수 있는 Okhttp의 컴포넌트.

 

1. 인터넷 연결 검사하기

유틸 클래스든 어딘가에 인터넷 연결을 검사할 수 있는 함수를 추가한다.

fun hasNetwork(context: Context): Boolean? {
    val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
    val activeNetwork: NetworkInfo? = connectivityManager.activeNetworkInfo
    return activeNetwork != null && activeNetwork.isConnected
}

 

2. 캐시 사이즈와 저장 디렉토리 정의

예제는 캐시 사이즈를 5MB로 주었다.

val cacheSize = (5 x 1024 x 1024).toLong() // 5MB
val myCache = Cache(context.cacheDir, cacheSize)

 

3. 인터셉터 클래스 작성하기

인터넷 연결이 있다면, 적어도 5초전에 저장된 캐시를 가져온다. 반대로 인터넷 연결이 끊어져있다면 7일 전의 캐시까지 가져와도 괜찮다. only-if-cached Directive는 캐시가 없을때 어차피 인터넷 연결도 없으므로 새 데이터를 가져오지 않도록 하는 역할을 한다.

class CacheInterceptor(private val context: Context) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        var request = chain.request()
        if (request.header("Cache-control") == null) {
            // 캐시 컨트롤 따로 설정 안 해두었다면 캐시 설정
            request = if (hasNetwork(context)) {
                request
                    .newBuilder()
                    .header("Cache-control", "public, max-age=5") // 5초
                    .build()
            } else {
                request
                    .newBuilder()
                    .header("Cache-control", "public, only-if-cached, max-stale=${60 * 60 * 24 * 7}") // 일주일
                    .build()
            }
        }
        return chain.proceed(request)
    }
}

 

4. 모두 합쳐!

이제 Interceptor를 OkHttpClient에 붙이고, Retrofit에 설정해주면 된다.

val cacheSize = (5 x 1024 x 1024).toLong()
val myCache = Cache(context.cacheDir, cacheSize)

val okHttpClient = OkHttpClient.Builder()
            .cache(myCache)
            .addInterceptor(CacheInterceptor(context))
            .build()

val retrofit = Retrofit.Builder()
            .baseUrl(BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .client(okHttpClient)
            .build()

 

참고자료

https://www.cdnetworks.com/ko/knowledge-center/what_is_cache_control/

https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#directiveshttps://shishirthedev.medium.com/retrofit-2-http-response-caching-e769a27af29f

https://docs.microsoft.com/ko-kr/dotnet/framework/network-programming/cache-policy-interaction-maximum-age-and-maximum-staleness

https://www.imperva.com/learn/performance/cache-control/

https://tech.ebayinc.com/engineering/caching-http-post-requests-and-responses/https://bapspatil.medium.com/caching-with-retrofit-store-responses-offline-71439ed32fda