HTTP는 최신 애플리케이션 네트워크 방식이다. 우리는 현재 주로 HTTP를 통해 데이터와 미디어를 교환한다 Http를 효율적으로 사용하면 리소스를 더 빠르게 로드하고 대역폭을 절약할 수 있다.
OkHttp는 기본적으로 효율적인 HTTP 클라이언트이다.
라이브러리를 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'
아래 예제는 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);
}
}
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);
}
}
동기 방식의 경우 서버에 요청을 보냈을 때 응답이 돌아와야 다음 동작을 수행할 수 있지만, 비동기 방식은 그와 반대로 신호를 보냈을 때 응답 상태와 상관없이 동작을 수행할 수 있다. 비동기 방식인 Ajax의 주목적으로는 화면 전환 없이 클라이언트 측과 서버측 간의 정보를 교환하기 위해서이다. 비동기 방식을 이용하면 자료를 요청할 때 걸리는 시간에 대해 클라이언트가 기다릴 필요없이 다른 작업을 바로 수행할 수 있다는 장점이 있다.
동기식 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);
}
이제 비동기 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 요청에 쿼리 매개변수를 추가하기 위해서 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);
}
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);
}
}
@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
를 사용하면 된다.
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);
}
요청에 사용자 정의 헤더를 설정하려면 간단한 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();
}
}
이 예에서는 모든 요청에 대해 기본헤더를 설정하는 대신 클라이언트 자체에서 기본헤더를 구성하는 방법을 볼 것이다.
예를 들어 모든 요청에 대해 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);
}
}
인터셉터는 원래 요청에 헤더를 추가한다.
이 예에서는 다음 리다이렉션을 중지하도록 OkHttpClient를 구성한다.
기본적으로 Get요청이 HTTP 301 Moved Permanently로 응답 되면 리디렉션이 자동으로 따른다.
일부 사용 사례에서는 완벽하게 괜찮을 수 있지만 원하지 않는 경우가 발생할 것이다.
이동작을 수행하려면 클라이언트를 빌드할 때 followRedirects
를 false
로 설정해야 한다.
응답은 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이 되므로 테스트가 실패한다.
피어에 연결할 수 없는 경우 시간 초과를 사용하여 호출에 실패한다.
네트워크 오류는 크랑이언트 연결 문제, 서버 가용성 문제 또는 그 사이의 모든 문제로 인해 발생할 수 있다
따라서 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);
}
}
클라이언트 시간 초과가 리소스 응답 시간보다 낮기 때문에 테스트가 실패해야 한다.
진행 중인 호출을 즉시 중지하려면 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
를 사용 하고 응답에 네트워크가 필요한 경우OkHttp
는504 Unsatisfiable Request
응답을 반환합니다.
인터셉터 처리하는 것이 편하다.
Application Interceptors
: Application
과 OKHttp
사이에 Request
, Response
정보를 intercept
하여 추가적으로 처리. (예: Request
시 추가적인 비즈니스 로직을 공통적으로 수행해야 되는경우 로그 등..)
Network Interceptors
: Network
와 OkHttp
사이에 Request
, Response
정보를 intercept
하여 추가적으로 처리. (예: Network
의 Response
정보를 보고 retry
할지 여부 등..)
구현 참고 : https://developer88.tistory.com/m/67?category=219605
기본설정값: OkHttp는 강력한 기본값들이 잘 설정되어 있다. 또한 사용자가 내용을 수정할 수도 있다.
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