spring cloud의 중요 요소중의 하나인 feign client
에 대해서 알아보자
가장 중요한 것은 역시 설정 파일인데요
feign 에서 제공하는 @EnableFeignClients
를 활용하면 정말 좋습니다
기본적으로
- class 을 가져오는 방식과
- basepackage 를 기반으로 scan 하는 방식
이 있는데요
basePackage를 기반으로 가져온다면.. 더 많이 편리하게 사용할 수 있겠죠?
feignClient가 추가될 때마다 환경 설정들을 추가적으로 할 필요가 없으니까요
또한, 보통 Feign 레벨의 client를 한번 더 감싸서 어떻게 처리할 것인지 결정하게 되는데요
이 경우에도 같이 @ComponentScan
을 같이 해준다면,
통합 테스트를 실시할 때 감싸진 Bean 들도 같이 가져올 수 있게 됩니다
@Configuration
@EnableFeignClients(basePackageClasses = [BaseFeignClientPackage::class])
@ComponentScan(basePackageClasses = [BaseFeignClientPackage::class])
class FeignClientConfiguration
인터페이스 기반이므로 사용법이란 참 간단합니다
@FeignClient(value = "placeholder", url = "\${external.api.placeHolder}")
interface PlaceHolderFeignClient {
@GetMapping("/posts")
fun posts(): Response
@GetMapping("/posts/{id}")
fun post(@PathVariable("id") id: Long): Response
}
FeignClient
를 활용할 때는 2가지 방식이 있는데요.
- Feign 에서 제공하는
AsyncResponseHandler
를 활용해서 response를 파싱하는 방법 - 직접 구현해서 파싱하는 방법
기본적으로 feign은 HttpStatus 200
번대는 정상 케이스임을 인지하고
ResponseBody에 대한 파싱이 이루어지게 됩니다
그 외의 상황이라면 FeignException
으로 감싸서 예외로 던져지게 되죠
else if (response.status() >= 200 && response.status() < 300) {
if (isVoidType(returnType)) {
resultFuture.complete(null);
} else {
final Object result = decode(response, returnType);
shouldClose = closeAfterDecode;
resultFuture.complete(result);
}
다만 404의 경우에는 조금 예외인데요
404
상황에 에러를 던질 것인지, 정상으로 인지하고 Body를 파싱할 것인지
옵션을 줄 수 있습니다
/**
* @return whether 404s should be decoded instead of throwing FeignExceptions
*/
boolean decode404() default false;
Feign에서 ResponseBody에 대한 정보는 ByteArrayBody
에 담겨져 있는데요
해당 클래스를 생성하는 시점에 response에 대한 byteArray 정보를 같이 저장하게 됩니다.!
@Override
public String toString() {
return decodeOrDefault(data, UTF_8, "Binary data");
}
해당 기능을 적절하게 이용하면 아래와 같이 직접파싱할 수도 있죠
다만, 이 경우에는 Feign.Response
객체를 적절히 활용해야 됩니다
val responseBody = objectMapper.readValue<RestPlaceHolderPost>(response.body().toString())
MSA
상황 속에서는 가장 힘든게 어디서 문제가 터졌는지입니다
기본적으로 Request, Response 에 대한 로깅이 필수적인 요소로 작용하는데요
신기하게도 위 Response 객체에는 Request 에 대한 정보도 담겨 있습니다
/**
* the request that generated this response
*/
public Request request() {
return request;
}
그러니 적절히 활용하면, 직접 로깅도 가능하겠죠..? ㅎㅎ
뭐 여러 방법들이 있겠지만..!
- Feign에서 제공하는
Retry
객체를 활용하는 방식 - Spring에서 제공하는
@Retryable
을 활용하는 방식
@Bean
fun retryer() = Retryer.Default(1000, 2000, 3)
다만 feign에서 제공하는 Retryer 는 Global 설정이므로
저는 선호하지는 않습니다
특정 상황에서만 재시도하고 싶은 경우와 재시도에 대한 정책은 달라질 수 있으니까요
@Retryable(backoff = Backoff(delay = 500L), maxAttempts = 3)
@GetMapping("/posts/{id}")
fun post(@PathVariable("id") id: Long): Response
그래서 가급적 @Retryable
을 활용하는 편입니다! ㅎㅎ
테스트는 2가지 방식을 소개해드릴께요
- 직접 API에 대한 규약을 테스트 하는 방식
- mock server로 다양한 httpStatus를 테스트하는 방식
API 간의 규약테스트가 바로 ContractTest 입니다
즉, 개발환경으로 떠 있는 API를 직접 찌르는 것이죠
@ImportAutoConfiguration(value = [FeignAutoConfiguration::class])
@AutoConfigureJson
annotation class AutoConfigureTestFeign {}
@AutoConfigureTestFeign
@SpringBootTest(classes = [FeignClientConfiguration::class])
abstract class FeignContractTest {
@Autowired
protected lateinit var objectMapper: ObjectMapper
}
설정은 위와 같이 가져갑니다
실제 host를 찔러서 응답을 가져오는 방식의 테스트죠
개발 환경에서 제공하는 데이터들을 확인하고 싶을 때, 진행하게 됩니다
GET https://jsonplaceholder.typicode.com/posts/1 HTTP/1.1
Binary data, response : {
"userId": 1,
"id": 1,
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
"body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
}
WireMock이라는 Mock Server로 통해서 테스트를 진행하는 방식인데요
위 테스트의 장점은 실제 서버로부터 받지 못하는 상황들을 테스트할 수 있습니다
대표적으로 503
혹은 404
의 실패상황을 실제로 연출할 수 있게 되죠.!
testImplementation("org.springframework.cloud:spring-cloud-contract-wiremock")
spring cloud의 wireMock 의존성을 사용할 것이구요..
@ActiveProfiles("wiremock")
@AutoConfigureWireMock(port = 0)
@AutoConfigureTestFeign
@SpringBootTest(
classes = [FeignClientConfiguration::class]
)
abstract class FeignWireMockTest {
@Autowired
protected lateinit var objectMapper: ObjectMapper
}
@AutoConfigureWireMock
이라는 어노테이션을 통해서 가짜 서버가 띄워지게 되는 방식이죠.!
그러면 아래처럼 테스트를 작성해서..!
@Test
fun `503이면 IllegalStateException을 던진다`() {
// given
stubFor(
get(anyUrl())
.willReturn(serviceUnavailable())
)
// when
val exception = catchThrowable { client.posts() }
// then
verify(
getRequestedFor(urlEqualTo("/posts"))
)
assertThat(exception).isInstanceOf(IllegalStateException::class.java)
}
503 이라는 응답을 mocking해서 받게되고,
해당 상황에 대한 커스터마이징한 Exception 이 제대로 발생하는지 검증하는 테스트입니다
127.0.0.1 - GET /posts
Accept: [*/*]
User-Agent: [Java/15.0.1]
Connection: [keep-alive]
Host: [localhost:10123]
Matched response definition:
{
"status" : 503
}
Response:
HTTP/1.1 503
Matched-Stub-Id: [415ccf84-5202-458e-8bd3-7aaaf2f47b34]
실제로 요청도 위와 같이 가짜 Mocking Server로 날라간 것을 확인할 수 있게 되죠.!
관련 소스자료는 여기서 확인할 수 있습니다
feign 모듈을 위주로 보면 좋을 것 같네요.!