SpringBoot- OkHttp

dragonappear·2021년 12월 19일
2

HTTP

목록 보기
1/3


개요

HTTP는 최신 애플리케이션 네트워크 방식이다. 우리는 현재 주로 HTTP를 통해 데이터와 미디어를 교환한다 Http를 효율적으로 사용하면 리소스를 더 빠르게 로드하고 대역폭을 절약할 수 있다.

OkHttp는 기본적으로 효율적인 HTTP 클라이언트이다.

  • HTTP/2 지원을 통해 동일한 호스트에 대한 모든 요청이 소켓을 공유할 수 있다.
  • 연결 풀링은 요청 대기 시간을 줄여준다.(HTTP/2를 사용할 수 없는 경우)
  • 투명 GZIP은 다운로드 크기를 줄인다.
  • 응답 캐싱은 반복 요청에 대해 네트워크를 완전히 피한다.

의존성 추가

라이브러리를 build.gradle에 추가하자.
https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp

// https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp
implementation 'com.squareup.okhttp3:okhttp:4.9.3'

Get

아래 예제는 URL에서 response를 받아온 뒤 그 내용을 문자열로 출력한다.

public class GetUrlExample {
    OkHttpClient client = new OkHttpClient();

    String run(String url) throws IOException {
        Request request = new Request.Builder()
                .url(url)
                .build();

        try(Response response = client.newCall(request).execute()) {
            return response.body().toString();
        }
    }
    
    public static void main(String[] args) throws IOException {
        GetUrlExample urlExample = new GetUrlExample();
        String response = urlExample.run("https://raw.github.com/square/okhttp/master/README.md");
        System.out.println(response);
    }
}

Post


public class PostExample {
    public static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
    OkHttpClient client = new OkHttpClient();

    String post(String url, String json) throws IOException {
        RequestBody body = RequestBody.create(json, JSON);
        Request request = new Request.Builder()
                .url(url)
                .post(body)
                .build();
        try (Response response = client.newCall(request).execute()) {
            return response.body().toString();
        }
    }

    String bowlingJson(String player1, String player2) {
        return "{'winCondition':'HIGH_SCORE',"
                + "'name':'Bowling',"
                + "'round':4,"
                + "'lastSaved':1367702411696,"
                + "'dateStarted':1367702378785,"
                + "'players':["
                + "{'name':'" + player1 + "','history':[10,8,6,7,8],'color':-13388315,'total':39},"
                + "{'name':'" + player2 + "','history':[6,10,5,10,10],'color':-48060,'total':41}"
                + "]}";
    }

    public static void main(String[] args) throws IOException {
        PostExample example = new PostExample();
        String json = example.bowlingJson("Jesse", "Jake");
        String response = example.post("http://www.roundsapp.com/post", json);
        System.out.println(response);
    }

}

OkHttp로 동기식,비동기식 GET

비동기 방식과 동기 방식의 차이점

동기 방식의 경우 서버에 요청을 보냈을 때 응답이 돌아와야 다음 동작을 수행할 수 있지만, 비동기 방식은 그와 반대로 신호를 보냈을 때 응답 상태와 상관없이 동작을 수행할 수 있다. 비동기 방식인 Ajax의 주목적으로는 화면 전환 없이 클라이언트 측과 서버측 간의 정보를 교환하기 위해서이다. 비동기 방식을 이용하면 자료를 요청할 때 걸리는 시간에 대해 클라이언트가 기다릴 필요없이 다른 작업을 바로 수행할 수 있다는 장점이 있다.

동기식 GET

동기식 GET 요청을 보내려면 URL을 기반으로 Request 객체를 만들고 호출해야 한다.

실행 후 Response 인스턴스를 반환한다.

@SpringBootTest
public class OkHttpGetLiveTest {

    private static final String BASE_URL = "http://localhost:" + APPLICATION_PORT;

    private OkHttpClient client;

    @BeforeEach
    public void init() {
        client = new OkHttpClient();
    }

    @Test
    public void whenGetRequest_thenCorrect() throws IOException {
        final Request request = new Request.Builder().url(BASE_URL).build();

        final Call call = client.newCall(request);
        final Response response = call.execute();

        Assertions.assertThat(response.code()).isEqualTo(200);
    }

OkHttp로 비동기 GET

이제 비동기 GET을 만들려면 Call을 큐에 넣어야 한다.

콜백이 응답을 읽을 수 있을때 응답을 읽게 해준다. 이것은 응답 헤더가 준비된 후에 발생한다.

응답 본문 읽기가 여전히 차단될 수 있습니다. OkHttp는 현재 부분적으로 응답 본문을 수신하는 비동기 API를 제공하지 않는다.

 public void whenAsynchronousRequest_thenCorrect() throws Exception{
        //given
        Request request = new Request.Builder()
                .url(BASE_URL)
                .build();

        Call call = client.newCall(request);

        call.enqueue(new Callback() {
            @Override
            public void onFailure(@NotNull Call call, @NotNull IOException e) {
                fail();
            }

            @Override
            public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
                System.out.println("OK");
            }
        });
        Thread.sleep(3000);
    }
OK

쿼리 매개변수로 GET

Get 요청에 쿼리 매개변수를 추가하기 위해서 HttpUrlBuilder를 활용할 수 있다.

URL이 빌드된 후 요청 개체에 전달하면 된다.

@Test
    public void whenGetRequestWithQueryParameter_thenCorrect() throws Exception{
        //given
        HttpUrl.Builder urlBuilder = HttpUrl.parse(BASE_URL).newBuilder();
        urlBuilder.addQueryParameter("userId", "1");

        //when
        String url = urlBuilder.build().url().toString();
        Request request = new Request.Builder().url(url).build();

        Call call = client.newCall(request);
        Response response = call.execute();

        //then
        assertThat(response.code()).isEqualTo(200);
    }

OkHttp로 동기식,비동기식 POST

동기식 POST

username password 매개변수를 보내기 위해 RequestBody를 빌드하는 간단한 POST를 살펴보자.


@SpringBootTest
public class OkHttpPostingLiveTest {
    private static final String BASE_URL = "http://localhost:" + APPLICATION_PORT;
    private OkHttpClient client;

    @BeforeEach
    public void init() {
        client = new OkHttpClient();
    }

    @Test
    public void whenSendPostRequest_thenCorrect() throws Exception{
        //given
        FormBody formBody = new FormBody.Builder()
                .add("username", "test")
                .add("password", "test")
                .build();

        Request request = new Request.Builder()
                .url(BASE_URL + "/tests")
                .post(formBody)
                .build();
        //when
        Call call = client.newCall(request);
        Response response = call.execute();
        //then
        Assertions.assertThat(response.code()).isEqualTo(200);
    }

}

비동기식 POST

@Test
    public void whenSendAsynchronousPostRequest_thenCorrect() throws Exception{
        FormBody formBody = new FormBody.Builder()
                .add("username", "test")
                .add("password", "test")
                .build();

        Request request = new Request.Builder()
                .url(BASE_URL + "/tests")
                .post(formBody)
                .build();
        //비동기 처리 (enqueue 사용)
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(@NotNull Call call, @NotNull IOException e) {
                fail();
            }

            @Override
            public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
                System.out.println("Response Body is " + response.body().string());
            }
        });
    }

정리

동기처리를 사용할때는 execute, 비동기처리를 사용할때는 enqueue를 사용하면 된다.


파일 업로드

  • 이 예에서 File을 업로드하는 방법을 살펴보자.
  • MultipartBody.Builder를 사용하여
  • test.txt 파일을 업로드해보자.
@Test
    public void whenUploadFile_thenCorrect() throws Exception{
        //given
        MultipartBody multipartBody = new MultipartBody.Builder()
                .setType(MultipartBody.FORM)
                .addFormDataPart("file", "file.txt",
                        RequestBody.create(MediaType.parse("application/octet-stream")
                                , new File("src/test/resources/test.txt")))
                .build();

        Request request = new Request.Builder()
                .url(BASE_URL + "/files")
                .post(multipartBody)
                .build();

        //when

        Call call = client.newCall(request);
        Response response = call.execute();
        //then

        Assertions.assertThat(response.code()).isEqualTo(200);
    }

사용자 정의 헤더 설정

1. 요청에 헤더 설정

요청에 사용자 정의 헤더를 설정하려면 간단한 addHeader 호출을 사용할 수 있다.

@SpringBootTest
public class OkHttpHeaderLiveTest {

    private static final String SAMPLE_URL = "http://www.github.com";

    private OkHttpClient client;

    @BeforeEach
    public void init() {
        client = new OkHttpClient();
    }

    @Test
    public void whenSetHeader_thenCorrect() throws Exception{
        //given
        Request request = new Request.Builder()
                .url(SAMPLE_URL)
                .addHeader("Content-Type","application/json")
                .build();
        //when
        Call call = client.newCall(request);
        Response response = call.execute();
        //then
        response.close();
    }
}

2. 기본 헤더 설정

이 예에서는 모든 요청에 대해 기본헤더를 설정하는 대신 클라이언트 자체에서 기본헤더를 구성하는 방법을 볼 것이다.

예를 들어 모든 요청에 대해 application/json 컨텐츠 유형을 설정하려면 클라이언트에 대한 인터셉터를 설정해야 한다.

방법은 아래와 같다.

@Test
    public void whenSetDefaultHeader_thenCorrect() throws Exception{
        //given
        OkHttpClient client = new OkHttpClient.Builder()
                .addInterceptor(
                        new DefaultContentTypeInterceptor("application/json")
                ).build();

        //when
        Request request = new Request.Builder()
                .url(SAMPLE_URL)
                .build();
        //then
        Call call = client.newCall(request);
        Response response = call.execute();
        response.close();
    }

다음은 Interceptor 의 확장 버전인 DefaultContentTypeInterceptor

public class DefaultContentTypeInterceptor implements Interceptor {

    private final String contentType;

    public DefaultContentTypeInterceptor(String contentType) {
        this.contentType = contentType;
    }

    @NotNull
    @Override
    public Response intercept(@NotNull Chain chain) throws IOException {
        Request originalRequest = chain.request();
        Request requestWithUserAgent = originalRequest.newBuilder()
                .header("Content-Type", contentType)
                .build();
        return chain.proceed(requestWithUserAgent);
    }
}

인터셉터는 원래 요청에 헤더를 추가한다.


Do Not Follow Redirects

이 예에서는 다음 리다이렉션을 중지하도록 OkHttpClient를 구성한다.

기본적으로 Get요청이 HTTP 301 Moved Permanently로 응답 되면 리디렉션이 자동으로 따른다.

일부 사용 사례에서는 완벽하게 괜찮을 수 있지만 원하지 않는 경우가 발생할 것이다.

이동작을 수행하려면 클라이언트를 빌드할 때 followRedirectsfalse로 설정해야 한다.

응답은 HTTP 301 상태 코드를 반환한다.

@SpringBootTest
public class OkHttpRedirectLiveTest {
    @Test
    public void whenSetFollowRedirects_thenNotRedirected() throws Exception{
        //given
        OkHttpClient client = new OkHttpClient().newBuilder()
                .followRedirects(false).build();
        //when
        Request request = new Request.Builder().url("http://t.co/I5YYd9tddw")
                .build();

        Call call = client.newCall(request);
        Response response = call.execute();
        //then
        Assertions.assertThat(response.code()).isEqualTo(301);
    }
}

true 매개변수로 리다이렉션을 켜거나 완전히 제거하면 클라이언트가 리다이렉션을 따르고 반환코드가 200이 되므로 테스트가 실패한다.


TimeOut

  • 피어에 연결할 수 없는 경우 시간 초과를 사용하여 호출에 실패한다.

  • 네트워크 오류는 크랑이언트 연결 문제, 서버 가용성 문제 또는 그 사이의 모든 문제로 인해 발생할 수 있다

  • 따라서 OkHttp는 연결,읽기 및 쓰기 제한 시간을 지원한다.

  • 아래 예에서는 1초의 readTimeOut으로 클라이언트를 구축했고 URL은 2초의 지연으로 제공된다.


@SpringBootTest
public class OkHttpTimeoutLiveTest {

    private static final String BASE_URL = "http://localhost:" + APPLICATION_PORT;

    @Test
    public void whenSetRequestTimeout_thenFail() throws Exception{
        //given
        OkHttpClient client = new OkHttpClient.Builder()
                .readTimeout(1, TimeUnit.SECONDS)
                .build();

        Request request = new Request.Builder()
                .url(BASE_URL + "/delay")
                .build();
        //when

        Call call = client.newCall(request);
        Response response = call.execute();
        //then
        Assertions.assertThat(response.code()).isEqualTo(200);
    }
}

클라이언트 시간 초과가 리소스 응답 시간보다 낮기 때문에 테스트가 실패해야 한다.


Call cancel

진행 중인 호출을 즉시 중지하려면 CallCancel()을 사용하자.

쓰레드가 현재 요청을 작성하거나 응답을 읽는 중이면 IOException이 발생한다.

call이 더이상 필요로 하지 않는다면 이것을 사용하여 네트워크를 절약하자.(예를 들어 사용자가 애플리케이션에서 다른 곳으로 이동할 때)


@SpringBootTest
public class OkHttpMiscLiveTest {

    private static final String BASE_URL = "http://localhost:" + APPLICATION_PORT;
    private static Logger logger = LoggerFactory.getLogger(OkHttpMiscLiveTest.class);
    private OkHttpClient client;

    @BeforeEach
    public void beforeAll() {
        client = new OkHttpClient();
    }

    @Test
    public void whenCancelRequest_thenCorrect() throws Exception{
        //given
        ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);

        Request request = new Request.Builder()
                .url(BASE_URL + "/delay")
                .build();

        int seconds = 1;
        long startNanos = System.nanoTime();


        //when
        Call call = client.newCall(request);
        //then
        executor.schedule(() -> {
            logger.debug("Canceling call: "
                    + (System.nanoTime() - startNanos) / 1e9f);

            call.cancel();

            logger.debug("Canceled call: "
                    + (System.nanoTime() - startNanos) / 1e9f);

        }, seconds, TimeUnit.SECONDS);

        logger.debug("Executing call: "
                + (System.nanoTime() - startNanos) / 1e9f);

        Assertions.assertThrows(IOException.class, () -> {
            Response response = call.execute();
        });
    }
    
}

응답 캐싱

Cache를 생성하려면 읽고 쓸 수 있는 캐시 디렉토리와 캐시 크기 제한이 필요하다.

클라이언트는 이를 사용하여 응답을 캐시한다.


    @Test
    public void  whenSetResponseCache_thenCorrect() throws Exception{
        //given
        int cacheSize = 10 * 1024 * 1024;
        File cacheDirectory = new File("src/test/resources/cache");
        Cache cache = new Cache(cacheDirectory, cacheSize);

        OkHttpClient client = new OkHttpClient.Builder()
                .cache(cache)
                .build();

        //when
        Request request = new Request.Builder()
                .url("http://publicobject.com/helloworld.txt")
                .build();
        //then
        Response response1 = client.newCall(request).execute();
        logResponse(response1);

        Response response2 = client.newCall(request).execute();
        logResponse(response2);
    }

    private void logResponse(Response response) throws IOException {
        logger.info("Response response:" + response);
        logger.info("Response cache response:" + response.cacheResponse());
        logger.info("Response network response:" + response.networkResponse());
        logger.info("Response responseBody:" + response.body().string());
    }
  • 테스트를 시작한 후 첫 번째 호출의 응답은 캐시되지 않습니다. cacheResponse 메서드를 호출하면 null 을 반환 하는 반면, networkResponse 메서드를 호출 하면 네트워크에서 응답을 반환합니다.또한 캐시 폴더는 캐시 파일로 채워집니다.

  • 응답이 이미 캐시되었기 때문에 두 번째 호출 실행은 반대 효과를 생성합니다. 즉, networkResponse에 대한 호출 은 null 을 반환하고 cacheResponse에 대한 호출 은 캐시에서 응답을 반환합니다.

  • 응답이 캐시를 사용하는 것을 방지하려면 CacheControl.FORCE_NETWORK 를 사용하세요. 네트워크 사용을 방지하려면 CacheControl.FORCE_CACHE 를 사용하세요.

주의점: FORCE_CACHE 를 사용 하고 응답에 네트워크가 필요한 경우 OkHttp504 Unsatisfiable Request 응답을 반환합니다.


OkHttp 장점

  1. 인터셉터 처리하는 것이 편하다.

    • Application Interceptors : ApplicationOKHttp 사이에 Request, Response 정보를 intercept하여 추가적으로 처리. (예: Request시 추가적인 비즈니스 로직을 공통적으로 수행해야 되는경우 로그 등..)

    • Network Interceptors : NetworkOkHttp 사이에 Request, Response 정보를 intercept하여 추가적으로 처리. (예: NetworkResponse 정보를 보고 retry할지 여부 등..)

    • 구현 참고 : https://developer88.tistory.com/m/67?category=219605

  2. 기본설정값: OkHttp는 강력한 기본값들이 잘 설정되어 있다. 또한 사용자가 내용을 수정할 수도 있다.

  3. Retofit은 OkHttp위에서 동작한다.


참고

https://square.github.io/okhttp/
https://martinwork.tistory.com/2
https://digitalbourgeois.tistory.com/59
https://www.baeldung.com/guide-to-okhttp
https://github.com/eugenp/tutorials/tree/master/libraries-http/src/test/java/com/baeldung/okhttp
전체코드: https://github.com/dragonappear/TIL/tree/master/okhttp

0개의 댓글