본문 바로가기

안드로이드

[안드로이드] Retrofit으로 가져온 데이터를 디바이스에 캐싱하기

개요

네트워크가 원할한 환경에서는 HTTP 통신 등을 통해 외부 데이터를 가져오는 것에 제약이 없다. 하지만, 네트워크가 원할하지 않은 경우에도 사용자에게 해당하는 데이터를 제공하여 더 좋은 SW를 작성할 수 있다. 다양한 방법이 존재하지만 그 중 대표적인 것은 캐싱(Caching)이다. 캐싱은 네트워크에서 가져온 데이터를 임시적으로 기기의 일부 공간에 저장하여 필요로 할 때 네트워킹이 아닌 기기의 공간에서 데이터를 가져올 수 있게 한다.

 

이를 통해 얻을 수 있는 이점은 다음과 같다.

 

  • 네트워킹이 불가능한 상황에서도 이전에 캐싱된 데이터를 통해 제공 가능
  • 이미 캐싱된 데이터가 있을 경우 굳이 네트워크를 활용하지 않고 조금 더 빠르게 데이터 획득 가능
  • 서버에서 요구되는 트래픽이 줄어드는 효과

 

안드로이드의 Retrofit 통신 과정에서 캐싱

그렇다면 안드로이드에서는 어떻게 캐싱을 할 수 있을까? 아래와 같은 일반적으로 작성하는 Retrofit 인스턴스 생성 코드로는 불가능하다. 

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

위의 경우 Retrofit에 정의된 Default OkHttpClient를 활용하는데 이 경우 캐싱을 수행하지 않는다. 따라서 직접 OkHttpClient를 생성하여 할당해주어야 한다.

 

OkHttpClient 생성 코드

val mCache = Cache(Context.cacheDir, size) //size는 적당히 설정


val mInterceptor = Interceptor{ chain -> 
    var request = chain.request()
    request = if (hasNetwork(context)!!)
            //네트워크가 있는 경우 5초 이내의 캐시 데이터가 있을 경우만 캐싱 수행하고 그 외는 네트워킹
            request.newBuilder().header("Cache-Control", "public, max-age=" + 5).build()
        else
            //네트워크가 없는 경우 7일 이내의 캐시 데이터가 잇을 경우 캐싱
            request.newBuilder().header("Cache-Control", "public, only-if-cached, max-stale=" + 60 * 60 * 24 * 7).build()
    chain.proceed(request)
}

val okHttpClient = OkHttpClient.Builder()
                .cache(mCache)
                .addInterceptor(mInterceptor)
                .build()

위에서 캐시를 지정해주고 Interceptor를 정의한다. Interceptor는 통신 요청/응답을 가로채 원하는데로 사용자가 직접 정의할 수 있게 한다. 위 과정에서 요청을 보낼 때 네트워크가 있는 경우와 없는 경우에 따라 캐싱 처리를 설정해주었다. (hasNetwork 함수는 현재 인터넷이 연결 되어 있는지 확인하는 함수로 자세한 구현은 생략한다.)

 

OkHttp에서 Interceptor는 App과 OkHttp core 사이에서 발생하는 App Interceptor와  OkHttp core와 Network 사이에서 발생하는 Network Interceptor로 구분된다. Network Interceptor는 contents와 직접적인 관련이 없는 로직을 구현할 때 사용하고, contents와 연관된 로직이 필요한 경우 ApplicationInterceptor를 사용한다. 이 경우는 캐시된 데이터를 가져오는 경우가 필요하기에 Application Interceptors로 추가해야 한다.

 

 

생성한 OkHttpClient를 할당한 Retrofit 코드

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

이제 위의 Retrofit 인스턴스로 API 서비스를 생성하여 실제 사용한다면, 캐싱이 되는 것을 확인할 수 있을 것이다.

 

 

기타. Cache-Control

위의 Interceptor 구현 과정에서 헤더로 Cache-Control 값을 설정해준 것을 확인할 수 있다. 이는 HTTP 통신 중 캐싱을 수행할 경우 옵션을 지정할 수 있게 한다. 클라이언트에서 사용가능한 옵션 혹은 지시문(Directives)는 다음과 같다.

 

1. Cacheability

HTTP 요청 중 캐시를 사용할 지 결정하는 지시문 종류는 다음과 같다.

  • public: Response는 캐시가 불가능한 경우에도 Response는 어느 캐시에나 저장이 가능하다.
  • private: Response는 캐시가 불가능한 경우에도 Response는 브라우저의 캐시에만 저장될 수 있다.
  • no-cache: Response는 캐시가 불가능한 경우에도 Response는 어느 캐시에나 저장이 가능하다. 하지만, 저장된 Response는 항상 원래 서버와의 유효성 검사를 거친 후에 사용 가능하다. 
  • no-store: 어느 캐시에도 저장하지 않는다.

2. Expiration

캐싱된 데이터(Response)의 기한과 관련된 지시문이다.

  • max-age=<seconds>: 데이터가 최신 상태로 간주되는 최대 시간을 의미한다. 예를 들어, max-age=10이라면 10초 간 이 데이터는 최신 데이터이므로, 10초 이전에 다른 요청이 있을 경우 캐시된 데이터를 반환하는 개념이다.
  • max-stale[=<seconds>]: 클라이언트가 캐시의 만료 시간을 초과한 응답을 수용할 지 나타낸다. 추가로 초 단위 시간을 지정하면 응답이 만료되서는 안되는 시간을 의미한다.
  • min-fresh=<seconds>: 클라이언트가 지정된 시간 동안 최신 상태로 간주될 응답을 원한다는 것을 나타낸다.

3. Revalidation and Reloading

  • must-revalidate: 캐시를 사용하기 전에는 무조건 검증이 필요하며, 만료된 경우 사용할 수 없다.
  • proxy-revalidate: 위와 비슷하지만, 공유 캐시에만 적용되며 private 캐시의 경우 적용되지 않는다.
  • immutable: Reponse의 데이터가 계속 변하지 않는 다는 것을 의미한다.

4. Others

  • only-if-cached: 클라이언트는 저장된 Response만 수용할 수 있다. 즉, 캐시된 데이터만 요청한다.
  • no-transform: 응답에 대해 변형이나 변환이 발생해서는 안된다.

 

 

 

참고