오늘은 Android에서 OkHttp - Interceptors에 대해 알아보는 글을 작성하려고 합니다. Interceptors는 네트워크 요청과 응답을 관리하고 조작하는 역할을 합니다.
Interceptors는 OkHttp의 핵심 기능 중 하나로, 네트워크 요청과 응답을 가로채어 추가 작업을 수행하거나 수정할 수 있습니다. 이를 통해 로깅, 인증, 캐싱 등 다양한 기능을 구현할 수 있습니다.
Interceptors는 크게 두 가지 유형으로 나뉩니다.
OkHttp Interceptors를 사용하기 위해서는 먼저 Interceptors 인터페이스를 구현해야 합니다. 그리고 이를 OkHttpClient.Builder에 추가해야 합니다.
간단하게 로깅을 해보면 다음과 같이 할 수 있습니다.
class LoggingInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val startTime = System.nanoTime()
val response = chain.proceed(request)
val endTime = System.nanoTime()
val duration = (endTime - startTime) / 1_000_000.0
println("(${request.method}) ${request.url} took ${duration}ms")
return response
}
}
OkHttpClient.Builder에 생성한 인터셉터를 추가합니다.
val client = OkHttpClient.Builder()
.addInterceptor(LoggingInterceptor())
.build()
여러 인터셉터를 사용할 경우, 순서가 중요할 수 있습니다. 인터셉터를 추가하는 순서에 따라 처리되는 순서가 결정됩니다.
따라서 인터셉터의 순서를 올바르게 관리하려면, 애플리케이션에서 필요한 기능을 순차적으로 수행하는 인터셉터를 적절한 순서로 추가해야합니다.
예를 들어, 아래와 같이 인증 토큰 관리와 로깅 인터셉터를 함께 사용하는 경우가 있습니다.
class AuthenticationInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val requestBuilder = originalRequest.newBuilder()
val authToken = getAuthTokenFromStorage() // 인증 토큰을 얻는 가상의 함수
if (authToken != null) {
requestBuilder.header("Authorization", "Bearer $authToken")
}
val request = requestBuilder.build()
return chain.proceed(request)
}
}
val client = OkHttpClient.Builder()
.addInterceptor(AuthenticationInterceptor()) // 인증 토큰 관리 인터셉터 추가
.addInterceptor(LoggingInterceptor()) // 로깅 인터셉터 추가
.build()
먼저 인증 토큰을 관리하는 인터셉터를 추가한 뒤 로깅 인터셉터를 추가했습니다. 이렇게 하면 인증 토큰이 추가된 요청을 로깅할 수 있습니다.
1,2 로깅, 인증 위의 설명으로 코드를 보면 요청 전송 전 시간과 전송 후 시간을 비교해 걸린 시간을 계산하고 출력할 수 있도록 해주고, 인증 토큰을 요청 헤더에 추가하는 인터셉터를 구현해 헤더에 추가한 후 요청을 진행하는 것을 볼 수 있습니다.
3. 공통 헤더 추가
class CommonHeadersInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val requestBuilder = originalRequest.newBuilder()
requestBuilder.header("User-Agent", "MyApp/1.0")
requestBuilder.header("Content-Type", "application/json")
val request = requestBuilder.build()
return chain.proceed(request)
}
}
모든 요청에 공통 헤더를 추가하는 인터셉터를 구현합니다. 사용자 에이전트와 컨텐츠 유형 헤더를 추가한 후 요청을 진행합니다.
val cacheSize = 10 * 1024 * 1024L // 10MB
val cache = Cache(context.cacheDir, cacheSize)
val client = OkHttpClient.Builder()
.cache(cache)
.addNetworkInterceptor { chain ->
val response = chain.proceed(chain.request())
val maxSaveTime = 60 // 캐시 유지 시간: 60초
response.newBuilder()
.header("Cache-Control", "public, max-age=$maxSaveTime")
.build()
}
.addInterceptor { chain ->
var request = chain.request()
if (!isNetworkAvailable(context)) { // 네트워크 연결 확인
val holdingTime = 60 * 60 * 24 * 7 // 오프라인 상태에서 캐시 유지 시간: 1주
request = request.newBuilder()
.header("Cache-Control", "public, only-if-cached, max-stale=$holdingTime")
.build()
}
chain.proceed(request)
}
.build()
네트워크 인터셉터를 사용하여 응답에 캐시 제어 헤더를 추가하고, 애플리케이션 인터셉터를 사용하여 오프라인 상태에서 캐시된 데이터를 사용하도록 설정합니다.
class RetryInterceptor(private val maxRetry: Int = 3) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
var response: Response? = null
var retryCount = 0
while (response == null && retryCount < maxRetry) {
try {
response = chain.proceed(request)
} catch (e: IOException) {
retryCount++
if (retryCount == maxRetry) {
throw e
}
}
}
return response!!
}
}
요청 실패 시 지정된 횟수만큼 재시도하는 인터셉터를 구현합니다.
class GzipDecompressionInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originalResponse = chain.proceed(chain.request())
val responseBody = originalResponse.body
val contentEncoding = originalResponse.header("Content-Encoding")
if (responseBody != null && "gzip".equals(contentEncoding, ignoreCase = true)) {
val gzipSource = GzipSource(responseBody.source())
val responseBodyCopy = responseBody.newBuilder().source(gzipSource).build()
return originalResponse.newBuilder().body(responseBodyCopy).build()
}
return originalResponse
}
}
Gzip 압축 해제를 수행하는 인터셉터를 구현합니다.
이 예시에서는 MockWebServer를 사용하여 테스트 환경에서 모킹된 응답을 반환합니다. 우선 의존성을 추가해야합니다.
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.9.1'
그런 다음 테스트 코드를 작성해야합니다.
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Test
class MyApiTest {
private lateinit var mockWebServer: MockWebServer
@Before
fun setup() {
mockWebServer = MockWebServer()
mockWebServer.start()
}
@After
fun teardown() {
mockWebServer.shutdown()
}
@Test
fun testGetUser() {
val mockedResponse = MockResponse()
.setResponseCode(200)
.setBody("""{"id":1,"name":"John Doe"}""")
mockWebServer.enqueue(mockedResponse)
val baseUrl = mockWebServer.url("/user/1")
val apiService = MyApiService(baseUrl.toString())
val user = apiService.getUser(1)
// 이제 응답을 검증할 수 있습니다.
}
}
이 예시에서는 MockWebServer를 사용하여 테스트 환경에서 서버를 모킹하고, 원하는 응답을 반환합니다. 이를 통해 실제 서버에 의존하지 않고 개발 및 테스트를 진행할 수 있습니다.
위와 같은 기능들을 통해 네트워크 처리를 더욱 효율적으로 관리하고 애플리케이션의 성능과 견고성을 높일 수 있습니다.