From 3e8b54dbba59efbd02ad5700bacce60a7d6faa36 Mon Sep 17 00:00:00 2001 From: hseong3243 Date: Sun, 19 May 2024 18:36:35 +0900 Subject: [PATCH 1/2] =?UTF-8?q?chore:=20okhttp3=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index 171f5c5..4132316 100644 --- a/build.gradle +++ b/build.gradle @@ -50,6 +50,7 @@ dependencies { runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'com.squareup.okhttp3:mockwebserver:4.12.0' testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor' From c10c5f3a7c9f9ce8b3ca6dd02966bf02d4f15253 Mon Sep 17 00:00:00 2001 From: hseong3243 Date: Sun, 19 May 2024 18:59:57 +0900 Subject: [PATCH 2/2] =?UTF-8?q?refactor:=20API=20=ED=86=B5=EC=8B=A0=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/client/api/ApiException.java | 8 ++ .../global/client/api/RestApiClient.java | 33 +++-- .../global/client/api/RestApiClientTest.java | 115 ++++++++++++++++++ 3 files changed, 148 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/seong/shoutlink/global/client/api/ApiException.java create mode 100644 src/test/java/com/seong/shoutlink/global/client/api/RestApiClientTest.java diff --git a/src/main/java/com/seong/shoutlink/global/client/api/ApiException.java b/src/main/java/com/seong/shoutlink/global/client/api/ApiException.java new file mode 100644 index 0000000..e58fa71 --- /dev/null +++ b/src/main/java/com/seong/shoutlink/global/client/api/ApiException.java @@ -0,0 +1,8 @@ +package com.seong.shoutlink.global.client.api; + +public class ApiException extends RuntimeException{ + + public ApiException(String message) { + super(message); + } +} diff --git a/src/main/java/com/seong/shoutlink/global/client/api/RestApiClient.java b/src/main/java/com/seong/shoutlink/global/client/api/RestApiClient.java index 1151ddb..d9ebafb 100644 --- a/src/main/java/com/seong/shoutlink/global/client/api/RestApiClient.java +++ b/src/main/java/com/seong/shoutlink/global/client/api/RestApiClient.java @@ -4,20 +4,28 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.seong.shoutlink.domain.common.ApiClient; -import com.seong.shoutlink.domain.exception.ErrorCode; -import com.seong.shoutlink.domain.exception.ShoutLinkException; +import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Map; -import org.springframework.http.ResponseEntity; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatusCode; import org.springframework.web.client.RestClient; +@Slf4j public class RestApiClient implements ApiClient { private final RestClient restClient; private final ObjectMapper objectMapper; public RestApiClient(ObjectMapper objectMapper) { - restClient = RestClient.create(); + restClient = RestClient.builder() + .defaultStatusHandler(HttpStatusCode::is5xxServerError, (request, response) -> { + log.error("[API] API 서버 에러가 발생하였습니다. [response - status={}, body={}]", + response.getStatusCode(), + new String(response.getBody().readAllBytes(), StandardCharsets.UTF_8)); + throw new ApiException("API 서버 에러가 발생하였습니다."); + }) + .build(); this.objectMapper = objectMapper; } @@ -27,17 +35,26 @@ public Map post( Map> uriVariables, Map> headers, String requestBody) { - ResponseEntity entity = restClient.post() + String responseBody = restClient.post() .uri(url, uriVariables) .headers(httpHeaders -> httpHeaders.putAll(headers)) .body(requestBody) .retrieve() - .toEntity(String.class); + .onStatus(HttpStatusCode::is4xxClientError, (request, response) -> { + log.error("[API] 요청 형식이 잘못되었습니다. " + + "[request - url={}, uriVariables={}, headers={}, body={}] " + + "[response - body={}", + url, uriVariables, headers, requestBody, + new String(response.getBody().readAllBytes(), StandardCharsets.UTF_8)); + throw new ApiException("요청 형식이 잘못되었습니다."); + }) + .body(String.class); try { - return objectMapper.readValue(entity.getBody(), new TypeReference<>() {}); + return objectMapper.readValue(responseBody, new TypeReference<>() {}); } catch (JsonProcessingException e) { - throw new ShoutLinkException("API 응답을 읽는데 실패하였습니다.", ErrorCode.ILLEGAL_ARGUMENT); + log.error("[API] API 응답을 읽는데 실패하였습니다. [response - body={}]", responseBody); + throw new ApiException("API 응답을 읽는데 실패하였습니다."); } } } diff --git a/src/test/java/com/seong/shoutlink/global/client/api/RestApiClientTest.java b/src/test/java/com/seong/shoutlink/global/client/api/RestApiClientTest.java new file mode 100644 index 0000000..9d738a5 --- /dev/null +++ b/src/test/java/com/seong/shoutlink/global/client/api/RestApiClientTest.java @@ -0,0 +1,115 @@ +package com.seong.shoutlink.global.client.api; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchException; + +import com.seong.shoutlink.base.BaseIntegrationTest; +import java.io.IOException; +import java.util.Map; +import okhttp3.HttpUrl; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +class RestApiClientTest extends BaseIntegrationTest { + + @Autowired + private RestApiClient restApiClient; + + MockWebServer mockServer; + + @BeforeEach + void setUp() throws IOException { + mockServer = new MockWebServer(); + mockServer.start(); + } + + @AfterEach + void tearDown() throws IOException { + mockServer.shutdown(); + } + + @Nested + @DisplayName("생성하면") + class CreateTest { + + @Test + @DisplayName("예외(apiException): 5xx 에러일 때") + void whenStatusCode_5xx() { + //given + MockResponse mockResponse = new MockResponse() + .setResponseCode(500); + mockServer.enqueue(mockResponse); + HttpUrl baseUrl = mockServer.url("/server-error"); + + //when + Exception exception = catchException( + () -> restApiClient.post(baseUrl.toString(), Map.of(), Map.of(), "")); + + //then + assertThat(exception).isInstanceOf(ApiException.class); + } + } + + @Nested + @DisplayName("post 호출 시") + class WhenPost { + + @Test + @DisplayName("성공") + void post() { + //given + MockResponse mockResponse = new MockResponse() + .addHeader("Content-Type", "application/json") + .setBody("{\"content\": \"hello\"}"); + mockServer.enqueue(mockResponse); + HttpUrl baseUrl = mockServer.url("/success"); + + //when + Map response = restApiClient.post(baseUrl.toString(), Map.of(), + Map.of(), ""); + + //then + assertThat(response.get("content")).isEqualTo("hello"); + } + + @Test + @DisplayName("예외(apiException): 4xx 에러일 때") + void whenStatusCode_4xx() { + //given + MockResponse mockResponse = new MockResponse() + .setResponseCode(400); + mockServer.enqueue(mockResponse); + HttpUrl baseUrl = mockServer.url("/client-error"); + + //when + Exception exception = catchException( + () -> restApiClient.post(baseUrl.toString(), Map.of(), Map.of(), "")); + + //then + assertThat(exception).isInstanceOf(ApiException.class); + } + + @Test + @DisplayName("예외(apiException): 응답 형식이 json이 아닐 때") + void whenResponse_isNotJson() { + //given + MockResponse mockResponse = new MockResponse() + .setBody("not json"); + mockServer.enqueue(mockResponse); + HttpUrl baseUrl = mockServer.url("/not-json"); + + //when + Exception exception = catchException( + () -> restApiClient.post(baseUrl.toString(), Map.of(), Map.of(), "")); + + //then + assertThat(exception).isInstanceOf(ApiException.class); + } + } +}