본문 바로가기
Android

OkHttp Interceptor란 무엇일까? 어떻게 사용할 수 있지?

by 너츠너츠 2024. 9. 3.

글로 들어가기 앞서

이 글은 Square에서 제공하는 Interceptor 아티클을 번역한 것이며, 개인적인 주관에 의해 해석되고 살이 조금 덧붙여질 수 있습니다.

 

Interceptor란?

Interceptor는 네트워크 호출을 모니터링하거나, 고쳐쓰거나, 재시도할 수 있는 강력한 매커니즘입니다.

예를 들어 공통적으로 보내줘야할 헤더가 있을 경우 Interceptor를 통해 모든 네트워크 호출에 포함시킬 수 있습니다. (링크)

 

class LoggingInterceptor implements Interceptor {
  @Override public Response intercept(Interceptor.Chain chain) throws IOException {
    // 서버로 보내는 Request
    Request request = chain.request();

    long t1 = System.nanoTime();
    logger.info(String.format("Sending request %s on %s%n%s",
        request.url(), chain.connection(), request.headers()));

    // 서버로부터 받은 Response
    Response response = chain.proceed(request);

    long t2 = System.nanoTime();
    logger.info(String.format("Received response for %s in %.1fms%n%s",
        response.request().url(), (t2 - t1) / 1e6d, response.headers()));

    return response;
  }
}

 

chain.proceed가 interceptor 매커니즘에서 가장 핵심적인 부분입니다.

간단해 보이는 메소드이지만, 실상은 모든 HTTP 작업이 이뤄지며, request를 만족하는 response를 생성해냅니다.

만약 chain.proceed 메소드가 2번이상 불리게 될 경우 response body들은 닫히게 됩니다.

 

Interceptor들은 연결될 수 있습니다.

Compressing하는 인터셉터와 Checksumming하는 인터셉터가 동시에 존재할 경우를 가정해봅시다.

언제 compress되고 checksum할지 결정해야 합니다. 

OkHttps는 순서대로 Interceptor들을 관리해줍니다.

 

 

Application Interceptors

 

Application Interceptor 또는 Network Interceptor들로 등록될 수 있습니다.

LoggingInterceptor를통해 그 차이를 예시로서 보여드리겠습니다.

 

OkHttpClient.Builder에 addInterceptor()를 통해 Application Interceptor를 등록합니다.

OkHttpClient client = new OkHttpClient.Builder()
    .addInterceptor(new LoggingInterceptor())
    .build();

Request request = new Request.Builder()
    .url("http://www.publicobject.com/helloworld.txt")
    .header("User-Agent", "OkHttp Example")
    .build();

Response response = client.newCall(request).execute();
response.body().close();

 

http://www.publicobject.com/helloworld.txt는 https로 redirect되고,  Okhttp는 자동으로 redirect를 따라갑니다.

Interceptor는 한번만 불리고, redirected된 response를 가지는 chain.proceed()로 부터 response를 반환합니다.

 

INFO: Sending request http://www.publicobject.com/helloworld.txt on null
User-Agent: OkHttp Example

INFO: Received response for https://publicobject.com/helloworld.txt in 1179.7ms
Server: nginx/1.4.6 (Ubuntu)
Content-Type: text/plain
Content-Length: 1759
Connection: keep-alive

 

response.request().url과 request.url()이 다르기 때문에, redirected된 것을 확인할 수 있습니다. 

 

Network Interceptors

Network Interceptor를 등록하는 것도 비슷합니다. addInterceptor() 대신 addNetworkInterceptor를 사용합니다.

OkHttpClient client = new OkHttpClient.Builder()
    .addNetworkInterceptor(new LoggingInterceptor())
    .build();

Request request = new Request.Builder()
    .url("http://www.publicobject.com/helloworld.txt")
    .header("User-Agent", "OkHttp Example")
    .build();

Response response = client.newCall(request).execute();
response.body().close();

 

code를 실행시키면, intercpetor는 두번 실행됩니다. http://www.publicobject.com/helloworld.txt에 초기 요청을 위해 한 번, https://publicobject.com/helloworld.txt로 redirect하기 위해 또 다시 한번 실행됩니다. 

 

INFO: Sending request http://www.publicobject.com/helloworld.txt on Connection{www.publicobject.com:80, proxy=DIRECT hostAddress=54.187.32.157 cipherSuite=none protocol=http/1.1}
User-Agent: OkHttp Example
Host: www.publicobject.com
Connection: Keep-Alive
Accept-Encoding: gzip

INFO: Received response for http://www.publicobject.com/helloworld.txt in 115.6ms
Server: nginx/1.4.6 (Ubuntu)
Content-Type: text/html
Content-Length: 193
Connection: keep-alive
Location: https://publicobject.com/helloworld.txt

INFO: Sending request https://publicobject.com/helloworld.txt on Connection{publicobject.com:443, proxy=DIRECT hostAddress=54.187.32.157 cipherSuite=TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA protocol=http/1.1}
User-Agent: OkHttp Example
Host: publicobject.com
Connection: Keep-Alive
Accept-Encoding: gzip

INFO: Received response for https://publicobject.com/helloworld.txt in 80.9ms
Server: nginx/1.4.6 (Ubuntu)
Content-Type: text/plain
Content-Length: 1759
Connection: keep-alive

 

네트워크 요청은 response compression에 대한 지원을 알리기 위해 OkHttp에 의해 추가된 Accept-Encoding: gzip 헤더와 같은 데이터를 더 가지고 있습니다. Network Interceptor의 Chain에는 null이 아닌 연결(Connection)이 있으며, 이 연결을 사용하여 웹 서버에 연결할 때 사용된 IP 주소와 TLS 설정을 확인할 수 있습니다.

 

Application과 Network 중 어떤 것을 선택해야할까?

애플리케이션 인터셉터 (Application Interceptors)

  1. 리다이렉트나 재시도와 같은 중간 응답에 대해 신경 쓸 필요가 없습니다.
  2. HTTP 응답이 캐시에서 제공되더라도 항상 한 번 호출됩니다.
  3. 애플리케이션의 원래 의도를 관찰하며, If-None-Match와 같은 OkHttp에서 추가된 헤더에는 관여하지 않습니다.
  4. Chain.proceed() 메서드를 호출하지 않고 바로 반환할 수 있습니다.
  5. Chain.proceed() 메서드를 재시도하고 여러 번 호출할 수 있습니다.
  6. withConnectTimeout, withReadTimeout, withWriteTimeout을 사용하여 호출 시간 초과를 조정할 수 있습니다.

네트워크 인터셉터 (Network Interceptors)

  1. 리다이렉트나 재시도와 같은 중간 응답에 대해 작동할 수 있습니다.
  2. 네트워크를 우회하여 캐시된 응답에 대해서는 호출되지 않습니다.
  3. 데이터가 네트워크를 통해 전송되는 그대로의 데이터를 관찰합니다.
  4. 요청을 전달하는 Connection에 접근할 수 있습니다.

 

Rewriting Requests

인터셉터는 요청 헤더를 추가, 제거, 또는 교체할 수 있습니다. 또한, body가 있는 요청의 경우 body를 변환할 수도 있습니다. 예를 들어, Application Interceptor를 사용하여 요청 본문 압축을 추가할 수 있는데, 이는 연결하려는 웹 서버가 해당 기능을 지원하는 것으로 알려진 경우에 유용합니다.

/** This interceptor compresses the HTTP request body. Many webservers can't handle this! */
final class GzipRequestInterceptor implements Interceptor {
  @Override public Response intercept(Interceptor.Chain chain) throws IOException {
    Request originalRequest = chain.request();
    if (originalRequest.body() == null || originalRequest.header("Content-Encoding") != null) {
      return chain.proceed(originalRequest);
    }

    Request compressedRequest = originalRequest.newBuilder()
        .header("Content-Encoding", "gzip")
        .method(originalRequest.method(), gzip(originalRequest.body()))
        .build();
    return chain.proceed(compressedRequest);
  }

  private RequestBody gzip(final RequestBody body) {
    return new RequestBody() {
      @Override public MediaType contentType() {
        return body.contentType();
      }

      @Override public long contentLength() {
        return -1; // We don't know the compressed length in advance!
      }

      @Override public void writeTo(BufferedSink sink) throws IOException {
        BufferedSink gzipSink = Okio.buffer(new GzipSink(sink));
        body.writeTo(gzipSink);
        gzipSink.close();
      }
    };
  }
}

 

Rewriting Responses

대칭적으로, 인터셉터는 응답 헤더를 다시 작성하거나 응답 본문을 변환할 수도 있습니다. 하지만 이는 일반적으로 요청 헤더를 다시 작성하는 것보다 더 위험할 수 있는데, 그 이유는 웹 서버의 기대에 어긋날 수 있기 때문입니다!

만약 어려운 상황에 처해 있고 그로 인한 결과를 처리할 준비가 되어 있다면, 응답 헤더를 다시 작성하는 것은 문제를 해결하는 강력한 방법이 될 수 있습니다. 예를 들어, 서버의 잘못 구성된 Cache-Control 응답 헤더를 수정하여 더 나은 응답 캐싱을 가능하게 할 수 있습니다.

/** Dangerous interceptor that rewrites the server's cache-control header. */
private static final Interceptor REWRITE_CACHE_CONTROL_INTERCEPTOR = new Interceptor() {
  @Override public Response intercept(Interceptor.Chain chain) throws IOException {
    Response originalResponse = chain.proceed(chain.request());
    return originalResponse.newBuilder()
        .header("Cache-Control", "max-age=60")
        .build();
  }
};

일반적으로 이러한 접근 방식은 웹 서버에서의 대응되는 수정과 함께 사용될 때 가장 효과적입니다!

반응형

댓글