From e7a682bfe60d0426bb5762f82256d965407d00f7 Mon Sep 17 00:00:00 2001 From: duswo5310 <63777714+duswo5310@users.noreply.github.com> Date: Mon, 7 Feb 2022 16:38:09 +0900 Subject: [PATCH] =?UTF-8?q?[#67]=20CircuitBreaker=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + .../user/service/UserAddressService.java | 6 +- .../doorrush/global/api/KakaoAddressApi.java | 38 +++--- .../global/dto/response/kakao/Address.java | 2 + .../dto/response/kakao/GetAddressInfo.java | 2 + .../kakao/KakaoApiGetAddressResponse.java | 11 ++ .../global/dto/response/kakao/Meta.java | 13 ++- .../dto/response/kakao/RoadAddress.java | 2 + src/main/resources/application.yml | 12 +- .../global/api/KakaoAddressApiTest.java | 109 +++++++++++++++--- .../global/api/mock/KakaoMockObject.java | 60 ++++++++++ src/test/resources/application-test.yml | 6 + 12 files changed, 220 insertions(+), 42 deletions(-) create mode 100644 src/test/java/com/flab/doorrush/global/api/mock/KakaoMockObject.java create mode 100644 src/test/resources/application-test.yml diff --git a/build.gradle b/build.gradle index 0135507..fad3266 100644 --- a/build.gradle +++ b/build.gradle @@ -57,6 +57,7 @@ dependencies { annotationProcessor 'org.projectlombok:lombok' /* Junit, Hamcrest, Mockito 포함하는 스프링 어플리케이션을 테스트 가능하도록 합니다. */ testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation("org.springframework.cloud:spring-cloud-starter-contract-stub-runner") } dependencyManagement { diff --git a/src/main/java/com/flab/doorrush/domain/user/service/UserAddressService.java b/src/main/java/com/flab/doorrush/domain/user/service/UserAddressService.java index 4edda9c..4fc6fa6 100644 --- a/src/main/java/com/flab/doorrush/domain/user/service/UserAddressService.java +++ b/src/main/java/com/flab/doorrush/domain/user/service/UserAddressService.java @@ -14,7 +14,6 @@ import com.flab.doorrush.global.exception.KakaoApiResponseException; import java.util.List; import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -73,12 +72,11 @@ public AddressInfoDTO getOriginAddress(Long addressSeq) { .x(address.getSpotX().toString()) .y(address.getSpotY().toString()) .build(); - return parseKakaoApiGetAddressResponse(kakaoAddressApi.getAddressBySpot(request)); + return parseKakaoApiGetAddressResponse(kakaoAddressApi.getAddressBySpot(request).getBody()); } private AddressInfoDTO parseKakaoApiGetAddressResponse( - ResponseEntity kakaoApiGetAddressResponse) { - KakaoApiGetAddressResponse response = kakaoApiGetAddressResponse.getBody(); + KakaoApiGetAddressResponse response) { if (!response.getMeta().isExist()) { throw new KakaoApiResponseException("API 응답결과가 없습니다."); diff --git a/src/main/java/com/flab/doorrush/global/api/KakaoAddressApi.java b/src/main/java/com/flab/doorrush/global/api/KakaoAddressApi.java index 79221ac..7deb3a3 100644 --- a/src/main/java/com/flab/doorrush/global/api/KakaoAddressApi.java +++ b/src/main/java/com/flab/doorrush/global/api/KakaoAddressApi.java @@ -26,22 +26,26 @@ @Slf4j public class KakaoAddressApi { + public static final String KAKAO_HEADER = "KakaoAK "; + public static final String CUSTOM_CIRCUIT_BREAKER = "kakaoAddressApiCircuitBreaker"; + private final RestTemplate restTemplate; - @Value("${api.authorization}") + @Value("${api.kakao.authorization}") private String AUTHORIZATION; - public static final String KAKAO_HEADER = "KakaoAK "; - public static final String KAKAO_HOST = "https://dapi.kakao.com"; - public static final String KAKAO_URL = "/v2/local/geo/coord2address.json"; - public static final String CUSTOM_CIRCUIT_BREAKER = "customCircuitBreaker"; + @Value("${api.kakao.host}") + public String KAKAO_HOST; + + @Value("${api.kakao.url}") + public String KAKAO_URL; + /** - * @CircuitBreaker - * resilience4j Spring Boot2 스타터에서 제공되는 어노테이션으로 AOP 측면을 제공하는 역할입니다. + * @CircuitBreaker resilience4j Spring Boot2 스타터에서 제공되는 어노테이션으로 AOP 측면을 제공하는 역할입니다. * CircuitBreaker라는 것을 명시하고 이름과 콜백 메소드를 지정할 수 있습니다. - */ - @CircuitBreaker(name = CUSTOM_CIRCUIT_BREAKER, fallbackMethod = "fallbackMethod") + */ + @CircuitBreaker(name = CUSTOM_CIRCUIT_BREAKER, fallbackMethod = "fallback") public ResponseEntity getAddressBySpot( KakaoApiGetAddressRequest getAddressRequest) { @@ -62,16 +66,16 @@ public ResponseEntity getAddressBySpot( return restTemplate.exchange(url, HttpMethod.GET, entity, KakaoApiGetAddressResponse.class); } - private ResponseEntity fallbackMethod( + private ResponseEntity fallback( KakaoApiGetAddressRequest getAddressRequest, Throwable t) { - log.info("KakaoAddressApi fallback Method running, Exception ={}", t.getMessage()); + + log.info("KakaoAddressApi fallback Method running, Exception = {}", t.getMessage()); + List documents = new ArrayList<>(); - new Meta("0"); - KakaoApiGetAddressResponse response = KakaoApiGetAddressResponse.builder() - .meta(new Meta("0")) - .documents(documents) - .build(); - return ResponseEntity.ok().body(response); + return ResponseEntity.ok().body(KakaoApiGetAddressResponse.builder() + .meta(Meta.builder().totalCount(0).build()) + .documents(documents) + .build()); } } diff --git a/src/main/java/com/flab/doorrush/global/dto/response/kakao/Address.java b/src/main/java/com/flab/doorrush/global/dto/response/kakao/Address.java index d697d99..940a534 100644 --- a/src/main/java/com/flab/doorrush/global/dto/response/kakao/Address.java +++ b/src/main/java/com/flab/doorrush/global/dto/response/kakao/Address.java @@ -1,9 +1,11 @@ package com.flab.doorrush.global.dto.response.kakao; import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; import lombok.Getter; @Getter +@Builder public class Address { @JsonProperty("address_name") diff --git a/src/main/java/com/flab/doorrush/global/dto/response/kakao/GetAddressInfo.java b/src/main/java/com/flab/doorrush/global/dto/response/kakao/GetAddressInfo.java index 644ac4a..4f22c5d 100644 --- a/src/main/java/com/flab/doorrush/global/dto/response/kakao/GetAddressInfo.java +++ b/src/main/java/com/flab/doorrush/global/dto/response/kakao/GetAddressInfo.java @@ -1,9 +1,11 @@ package com.flab.doorrush.global.dto.response.kakao; import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; import lombok.Getter; @Getter +@Builder public class GetAddressInfo { @JsonProperty("road_address") diff --git a/src/main/java/com/flab/doorrush/global/dto/response/kakao/KakaoApiGetAddressResponse.java b/src/main/java/com/flab/doorrush/global/dto/response/kakao/KakaoApiGetAddressResponse.java index 1848908..81046e4 100644 --- a/src/main/java/com/flab/doorrush/global/dto/response/kakao/KakaoApiGetAddressResponse.java +++ b/src/main/java/com/flab/doorrush/global/dto/response/kakao/KakaoApiGetAddressResponse.java @@ -1,17 +1,28 @@ package com.flab.doorrush.global.dto.response.kakao; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.flab.doorrush.global.exception.KakaoApiResponseException; import java.util.List; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; @Getter @Builder +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@JsonIgnoreProperties({"mainAddress"}) public class KakaoApiGetAddressResponse { private Meta meta; private List documents; public GetAddressInfo getMainAddress() { + if (documents.isEmpty()) { + throw new KakaoApiResponseException("응답값이 없습니다."); + } return documents.get(0); } diff --git a/src/main/java/com/flab/doorrush/global/dto/response/kakao/Meta.java b/src/main/java/com/flab/doorrush/global/dto/response/kakao/Meta.java index 3c7d319..88e8b27 100644 --- a/src/main/java/com/flab/doorrush/global/dto/response/kakao/Meta.java +++ b/src/main/java/com/flab/doorrush/global/dto/response/kakao/Meta.java @@ -1,15 +1,22 @@ package com.flab.doorrush.global.dto.response.kakao; import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AccessLevel; import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; -@AllArgsConstructor +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PRIVATE) public class Meta { @JsonProperty("total_count") - private String totalCount; + private int totalCount; public boolean isExist() { - return !totalCount.equals("0"); + return !(totalCount == 0); } } diff --git a/src/main/java/com/flab/doorrush/global/dto/response/kakao/RoadAddress.java b/src/main/java/com/flab/doorrush/global/dto/response/kakao/RoadAddress.java index fb5bd5c..6fe36e9 100644 --- a/src/main/java/com/flab/doorrush/global/dto/response/kakao/RoadAddress.java +++ b/src/main/java/com/flab/doorrush/global/dto/response/kakao/RoadAddress.java @@ -1,9 +1,11 @@ package com.flab.doorrush.global.dto.response.kakao; import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; import lombok.Getter; @Getter +@Builder public class RoadAddress { @JsonProperty("address_name") diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index a01341c..3605330 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -18,8 +18,12 @@ spring: mybatis: mapper-locations: classpath:mybatis/mapper/*Mapper.xml +# api api: - authorization: ${AUTHORIZATION} + kakao: + authorization: ${AUTHORIZATION} + host: https://dapi.kakao.com + url: /v2/local/geo/coord2address.json # resilience4j resilience4j.circuitbreaker: @@ -32,8 +36,8 @@ resilience4j.circuitbreaker: maxWaitDurationInHalfOpenState: 1000 slidingWindowType: COUNT_BASED slidingWindowSize: 10 - minimumNumberOfCalls: 10 + minimumNumberOfCalls: 5 waitDurationInOpenState: 10000 instances: - customCircuitBreaker: - baseConfig: default \ No newline at end of file + kakaoAddressApiCircuitBreaker: + baseConfig: default diff --git a/src/test/java/com/flab/doorrush/global/api/KakaoAddressApiTest.java b/src/test/java/com/flab/doorrush/global/api/KakaoAddressApiTest.java index 1be3c6d..c9bba1b 100644 --- a/src/test/java/com/flab/doorrush/global/api/KakaoAddressApiTest.java +++ b/src/test/java/com/flab/doorrush/global/api/KakaoAddressApiTest.java @@ -1,35 +1,116 @@ package com.flab.doorrush.global.api; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static com.flab.doorrush.global.api.mock.KakaoMockObject.createKakaoApiFailResponse; +import static com.flab.doorrush.global.api.mock.KakaoMockObject.createKakaoApiRequest; +import static com.flab.doorrush.global.api.mock.KakaoMockObject.createKakaoApiSuccessResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static org.junit.jupiter.api.Assertions.assertEquals; -import com.flab.doorrush.global.dto.request.KakaoApiGetAddressRequest; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import com.flab.doorrush.global.dto.response.kakao.KakaoApiGetAddressResponse; +import io.github.resilience4j.circuitbreaker.CircuitBreaker; +import io.github.resilience4j.circuitbreaker.CircuitBreaker.State; +import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry; +import java.util.Optional; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.cloud.contract.spec.internal.HttpStatus; +import org.springframework.cloud.contract.wiremock.AutoConfigureWireMock; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ActiveProfiles; -@SpringBootTest +@ActiveProfiles("test") +@SpringBootTest(webEnvironment = WebEnvironment.NONE) +@AutoConfigureWireMock(port = 0) class KakaoAddressApiTest { + @Autowired KakaoAddressApi kakaoAddressApi; + @Autowired + ObjectMapper objectMapper; + + @Autowired + CircuitBreakerRegistry CircuitBreakerRegistry; + @Test @DisplayName("좌표로 주소 받아오는 API 성공 테스트") - public void getAddressSuccessTest() { - ResponseEntity result = kakaoAddressApi.getAddressBySpot(KakaoApiGetAddressRequest.builder() - .x("127.423084873712") - .y("37.0789561558879") - .build()); - - KakaoApiGetAddressResponse response = result.getBody(); - assertThat(response.getMainAddress().getRoadAddress().getAddressName()).isEqualTo("경기도 안성시 죽산면 죽산초교길 69-4"); - assertThat(response.getMainAddress().getAddress().getAddressName()).isEqualTo("경기 안성시 죽산면 죽산리 343-1"); - assertTrue(response.getMeta().isExist()); + public void getAddressSuccessTest() throws JsonProcessingException { + + // Given + stubFor(get(urlPathEqualTo("/v2/local/geo/coord2address.json")) + .willReturn(aResponse() + .withStatus(200) + .withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .withBody(objectMapper.writeValueAsString(createKakaoApiSuccessResponse())))); + + // When + ResponseEntity response = kakaoAddressApi.getAddressBySpot( + createKakaoApiRequest()); + + // Then + assertEquals(objectMapper.writeValueAsString(response.getBody()), + objectMapper.writeValueAsString(createKakaoApiSuccessResponse())); + assertEquals(HttpStatus.OK, response.getStatusCode().value()); + } + + @Test + @DisplayName("좌표로 주소 받아오는 API 실패 테스트 - fallback 메소드 작동") + public void getAddressRunningFallbackTest() throws JsonProcessingException { + + // Given + stubFor(get(urlPathEqualTo("/v2/local/geo/coord2address.json")) + .willReturn(aResponse() + .withStatus(500) + .withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .withBody(objectMapper.writeValueAsString(createKakaoApiFailResponse())))); + + // When + ResponseEntity response = kakaoAddressApi.getAddressBySpot( + createKakaoApiRequest()); + + // Then + assertEquals(objectMapper.writeValueAsString(response.getBody()), + objectMapper.writeValueAsString(createKakaoApiFailResponse())); + assertEquals(HttpStatus.OK, response.getStatusCode().value()); } + @Test + @DisplayName("minimumNumberOfCalls에 도달 시 CircuitBreaker OPEN 상태로 변경") + public void shouldCircuitBreakerOpenStatus() throws JsonProcessingException { + // Given + Optional circuitBreaker = CircuitBreakerRegistry.find( + "kakaoAddressApiCircuitBreaker"); + int minimumNumberOfCalls = circuitBreaker.get().getCircuitBreakerConfig() + .getMinimumNumberOfCalls(); + + stubFor(get(urlPathEqualTo("/v2/local/geo/coord2address.json")) + .willReturn(aResponse() + .withStatus(500) + .withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .withBody(objectMapper.writeValueAsString(createKakaoApiFailResponse())))); + + circuitBreaker.get().reset(); + // When + for (int i = 1; i < minimumNumberOfCalls + 10; i++) { + kakaoAddressApi.getAddressBySpot(createKakaoApiRequest()); + // Then + if (i < minimumNumberOfCalls) { + assertEquals(State.CLOSED, circuitBreaker.get().getState()); + } else { + assertEquals(State.OPEN, circuitBreaker.get().getState()); + } + } + } } diff --git a/src/test/java/com/flab/doorrush/global/api/mock/KakaoMockObject.java b/src/test/java/com/flab/doorrush/global/api/mock/KakaoMockObject.java new file mode 100644 index 0000000..fd933ee --- /dev/null +++ b/src/test/java/com/flab/doorrush/global/api/mock/KakaoMockObject.java @@ -0,0 +1,60 @@ +package com.flab.doorrush.global.api.mock; + +import com.flab.doorrush.global.dto.request.KakaoApiGetAddressRequest; +import com.flab.doorrush.global.dto.response.kakao.Address; +import com.flab.doorrush.global.dto.response.kakao.GetAddressInfo; +import com.flab.doorrush.global.dto.response.kakao.KakaoApiGetAddressResponse; +import com.flab.doorrush.global.dto.response.kakao.Meta; +import com.flab.doorrush.global.dto.response.kakao.RoadAddress; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class KakaoMockObject { + + private static final int TOTAL_COUNT = 0; + private static final int SUCCESS_TOTAL_COUNT = 1; + private static final String ROAD_ADDRESS_NAME = "경기도 안성시 죽산면 죽산초교길 69-4"; + private static final String ROAD_NAME = "죽산초교길"; + private static final String ROAD_BUILDING_NAME = "무지개아파트"; + private static final String ADDRESS_NAME = "경기 안성시 죽산면 죽산리 343-1"; + + public static KakaoApiGetAddressRequest createKakaoApiRequest() { + return KakaoApiGetAddressRequest.builder() + .x("127.423084873712") + .y("37.0789561558879") + .build(); + } + + public static KakaoApiGetAddressResponse createKakaoApiSuccessResponse() { + GetAddressInfo getAddressInfo = GetAddressInfo.builder() + .address(Address.builder() + .addressName(ADDRESS_NAME) + .build()) + .roadAddress(RoadAddress.builder() + .addressName(ROAD_ADDRESS_NAME) + .roadName(ROAD_NAME) + .buildingName(ROAD_BUILDING_NAME) + .build()) + .build(); + + List documents = Arrays.asList(getAddressInfo); + + return KakaoApiGetAddressResponse.builder() + .meta(Meta.builder() + .totalCount(SUCCESS_TOTAL_COUNT) + .build()) + .documents(documents) + .build(); + } + public static KakaoApiGetAddressResponse createKakaoApiFailResponse() { + List documents = new ArrayList<>(); + + return KakaoApiGetAddressResponse.builder() + .meta(Meta.builder() + .totalCount(TOTAL_COUNT) + .build()) + .documents(documents) + .build(); + } +} diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml new file mode 100644 index 0000000..34ac241 --- /dev/null +++ b/src/test/resources/application-test.yml @@ -0,0 +1,6 @@ +# api +api: + kakao: + authorization: ${AUTHORIZATION} + host: http://localhost:${wiremock.server.port} + url: /v2/local/geo/coord2address.json