Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor: 외부 API 통신 예외를 추가한다. #125

Merged
merged 2 commits into from
May 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.seong.shoutlink.global.client.api;

public class ApiException extends RuntimeException{

public ApiException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -27,17 +35,26 @@ public Map<String, Object> post(
Map<String, List<String>> uriVariables,
Map<String, List<String>> headers,
String requestBody) {
ResponseEntity<String> 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 응답을 읽는데 실패하였습니다.");
}
}
}
Original file line number Diff line number Diff line change
@@ -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<String, Object> 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);
}
}
}
Loading