안드로이드 개발자가 알아두면 좋은 HTTP Cache

김정원
9 min readMar 18, 2023

--

Cache-Control 을 알아야 하는 이유

모바일 개발자라면 HTTP 프로토콜을 이용한 네트워크 통신 구현을 한번쯤은 해보셨을 겁니다. 이를 개발을 하다보면 오프라인 모드, 서버 부하 감소, 로딩 속도 개선 등과 같은 이유로 응답 결과를 캐싱해야하는 경우들이 있습니다. 그래서 HTTP 프로토콜 표준에 이를 할 수 있는 규약이 있습니다. 바로 Cache-Control 인데요. 오늘은 이 Cache-Control 이 무엇이고 어떻게 구현할 수 있는지를 정리해보도록 하겠습니다.

Cache-Control 이란?

Cache-Control 이란 HTTP 헤더에 캐시 제어를 할 수 있는 지시문을 담는 필드입니다. 이 지시문을 통해 로컬 캐시를 사용할 것인지, 공유 캐시를 사용할 것인지, 캐시 유효시간은 얼마나 할 것인지, 캐시 유효성 검사는 얼마나 자주할 것인지 등등을 정할 수 있습니다.

우선 캐시의 종류에 대해 먼저 정리해보겠습니다.

로컬 캐시 (Private Cache) vs 공유 캐시 (Shared Cache)

HTTP 캐시에는 크게 2가지 종류가 있습니다. Private Cache 라 불리는 로컬 캐시와 Shared Cache 라고 불리는 공유 캐시가 있습니다. 로컬 캐시와 공유 캐시의 차이점을 이해하신다면 둘을 쉽게 이해하실 수 있으실 겁니다.

둘의 가장 큰 차이점은 “캐시된 데이터를 누구에게 제공할 것인가?” 입니다. 캐시된 데이터를 요청한 클라이언트에게만 제공한다면 로컬 캐시, 다른 클라이언트에게 공유 가능하다면 공유 캐시를 사용할 수 있습니다.

그렇기 때문에 특정 유저에게만 제공해야하는 응답인 경우에는 로컬 캐시, 모든 유저가 동일한 응답을 제공해야하는 응답인 경우에는 공유 캐시를 사용하는 것이 좋습니다.

요청 cache-control 과 응답 cache-control 의 차이점

아래 보이는 표는 MDN Web Docs 에 명시되어 있는 Cache 지시문 테이블인데요. 보시다시피 요청과 응답에 따라 사용할 수 있는 지시문이 다릅니다.

왜 다를 까요?

https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#cache_directives

그 이유는 캐시 제어 주체에 따라 할 수 있는 지시문이 다르기 때문입니다. max-age 를 예를 들어 설명해보겠습니다. 만약 요청 헤더에 Cache-Control: max-age=5 를 추가해 보낸다면 어떤 의미일까요? 이 의미는 클라이언트가 해당 요청을 5초 동안 캐싱하겠다고 서버에 알린다는 의미입니다. 즉, 클라이언트가 캐싱의 주체가 됩니다.

하지만 응답 헤더에 추가해 보낸다면 어떨까요? 그건 서버가 클라이언트에게 5초간 해당 응답을 캐싱하라는 의미가 됩니다. 그렇기 때문에 이에 따라 구현의 차이점이 있기 때문에 기억해두시면 도움이 될 것입니다.

Retrofit 으로 Cache-Control 구현하기

그렇다면 본격적으로 Cache-Control 를 구현해보도록 하겠습니다. 구현하기 앞서 캐시를 테스트할 수 있는 서버가 필요한데요. 이 글에서는 Postman의 Mock Server 를 사용해 구현해보도록 하겠습니다.

Postman 으로 Mock Server 구현하기

  1. 우선 PostMan 을 설치해주시고 로그인합니다
  2. 여기서 Mock Servers 메뉴를 클릭하시고 상단에 “New” 버튼을 누릅니다.

3. Create a mock server 단계에서 임의의 URL path와 Response Body 를 정의하시고 Next 를 눌러주세요.

4. Mock Server Name 을 적고 Create Mock Server 를 합니다

5. Mock Server Url 을 카피해둡니다

6. 다시 Collections > 내가 만든 Mock Server > request 를 클릭하면 응답 예시가 나옵니다

7. 응답 예시 하단에 Header 부분의 키에 Cache-Control , 값에 maxage=5 를 입력하고 저장합니다 (저장하지 않으면 Cache-Control 값이 내려오지 않으니 주의해주세요)

8. Cache-Control 응답을 해주는 Mock Server 만들었습니다!

Retrofit 으로 Cache-Control: max-age 구현하기

위에서 설명했듯이 Cache-Control 은 HTTP 표준이기 때문에 Retrofit 과 같은 네트워크 모듈에서 이미 지원하고 있어 구현이 매우 쉽습니다.

우선 응답 Cache-Control 구현을 해보도록 하겠습니다.
(샘플 코드 : https://github.com/renovatio0424/CacheTestApp)

Response Cache-Control: max-age

  1. 우리가 만든 Cache API 인터페이스를 만듭니다
interface PostManService {
@GET("/cacheTest")
suspend fun getCacheTest()

companion object {
const val BASE_URL = BuildConfig.BASE_URL
}
}

2. 우선 OkhttpClientcache 를 구현해줍니다

const val cacheSize = 10L * 1024 * 1024 //10MB

// 저장할 캐시의 크기와 경로를 설정해줍니다.
val okHttpClient: (Context) -> OkHttpClient = { context ->
OkHttpClient.Builder()
.cache(Cache(context.cacheDir, cacheSize))
.build()
}

3. cache 가 구현된 OkhttpClientRetrofit 에 추가합니다

val postManService: (context: Context) -> PostManService = { context ->
Retrofit.Builder()
.baseUrl(PostManService.BASE_URL)
.client(okHttpClient(context))
.build()
.create(PostManService::class.java)
}

이를 이용해 실제로 요청을 날려보면 서버에 max-age=10 내려준 경우 10초 동안 요청을 날리지 않는 것을 알 수 있었습니다.

10초 동안 같은 요청을 반복했을때 10초뒤에 요청하는 모습

Request Cache-Control: max-age

요청의 경우에는 크게 2가지 방법이 있습니다.

Interceptor 를 이용해 모든 요청에 Cache-Control 을 추가하는 방법과 특정 요청 헤더에 Cache-Control 을 추가하는 방법이 있습니다.

우선 특정 요청 헤더에 Cache-Control 을 추가하는 방법은 retrofit 헤더 어노테이션을 이용하면 됩니다

@Headers("Cache-Control: max-age=5")
@GET("/requestCache")
suspend fun getRequestCache()

Interceptor 를 이용해 구현하는 방식도 위와 유사합니다

// CacheControlInterceptor 구현
class CacheControlInterceptor : Interceptor {
private val cacheControlMaxAgeValue
get() = "max-age=$CACHE_CONTROL_MAX_AGE"

@Throws(IOException::class)
override fun intercept(chain: Chain): Response {
var request = chain.request()
request = request.newBuilder()
.header(HTTP_HEADER_KEY_CACHE_CONTROL, cacheControlMaxAgeValue)
.build()
return chain.proceed(request)
}

companion object {
private const val HTTP_HEADER_KEY_CACHE_CONTROL = "Cache-Control"
private const val CACHE_CONTROL_MAX_AGE = 60
}
}

// okHttpClient 에 Interceptor 추가
val okHttpClient: (Context) -> OkHttpClient = { context ->
OkHttpClient.Builder()
.cache(Cache(context.cacheDir, cacheSize))
.addInterceptor(CacheControlInterceptor())
.build()
}

마무리

무분별한 캐시 사용은 오히려 데이터 동기화에 문제를 일으킬 수 있습니다. 그렇기 때문에 정말 캐시를 사용할 필요성이 있는지를 잘 검토해보고 구현하셨으면 좋겠네요

또한 이글 에서는 cache-control 지시문 전부를 다루는 것이 아닌 일부만 다뤘지만 다른 지시문들도 비슷한 방식으로 구현이 가능하기 때문에 공식문서와 함께 참고하시면 도움이 될 것 같습니다.

끝까지 읽어주셔서 감사합니다!

--

--

김정원

Android Developer who loves programming and sharing dev knowledge