앞서, Feign Client를 이용하여 요청을 전송하는 방법에 대하여 알아봤습니다. 다음으로는 OpenFeign의 Interceptor
, Logger
, ErrorDecoder
를 활용하여 OpenFeign을 좀 더 활용하는 방법에 대하여 알아보겠습니다.
외부로 요청이 나가기 전에 만약 공통적으로 처리해야하는 부분이 있다면 Interceptor를 재정의하여 처리한다.
package wh.duckbill.openfeign.feign.interceptor;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpMethod;
import java.nio.charset.StandardCharsets;
@RequiredArgsConstructor(staticName = "of")
public class DemoFeignInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
// GET 요청일 경우
if (requestTemplate.method().equals(HttpMethod.GET.name())) {
System.out.println("[GET] [DemoFeignInterceptor] queries: " + requestTemplate.queries());
return;
}
// POST 요청일 경우
String encodedRequestBody = StringUtils.toEncodedString(requestTemplate.body(), StandardCharsets.UTF_8);
System.out.println("[POST] [DemoFeignInterceptor] requestBody: " + encodedRequestBody);
// 추가적으로 필요한 로직을 수행
String covertRequestBody = encodedRequestBody;
requestTemplate.body(covertRequestBody);
}
}
RequestInterceptor
를 implement
하여 Interceptor를 구현할 수 있다.RequestTemplate
를 intercept해서 요청에 따라 다르게 처리한 것을 확인할 수 있다.DemoFeign
에 대한 요청의 공통 로직 처리를 수행할 수 있다.위의 RequestInterceptor
를 사용하기 위해서는 FeignConfig
에 bean
등록을 해주어야 한다.
@Configuration
public class DemoFeignConfig {
@Bean
public DemoFeignInterceptor demoFeignInterceptor() {
return DemoFeignInterceptor.of();
}
}
DemoFeignInterceptor
를 보면 @RequiredArgsConstructor
의 staticName="of"
로 설정한 것을 볼 수 있다.Feign Logger를 Feign 요청을 통한 수행 과정을 Log로 남기는데 목적이 있다.
application.yml
확인application.yml
의 feign 설정에서 loggerLevel
이 설정되어 있는지 확인demo-client
의 loggerLevel
을 HEADERS
로 설정feign:
url:
prefix: http://localhost:8080/target_server # DemoFeignClient ?? ??? url prefix
client:
config:
default:
connectTimeout: 1000
readTimeout: 3000
loggerLevel: NONE
demo-client: # DemoFeignClient ?? ??? Client ?? ?
connectTimeout: 1000
readTimeout: 10000
loggerLevel: HEADERS
/**
* Feign 과 관련된 설정을 위한 클래스
*/
@Configuration
public class FeignConfig {
/**
* Feign 에서 logging할 level 지정
*/
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.HEADERS;
}
/**
* Feign의 Global Logger 클래스 등록
*/
@Bean
public Logger feignLogger() {
return FeignCustomLogger.of();
}
}
FeignCustomLogger
클래스를 bean
에 등록하여 Feign Request가 FeignCustomLogger 클래스를 탈 수 있도록 설정Logger class에서는 요청에 대한 Log를 남기는 클래스로 주로 log
, logRequest
, logAndRebufferResponse
메서드를 사용하며, 이중에서도 logAndRebufferResponse
를 많이 사용하게 됩니다.
Logger
class는 feign.logger.Logger
abstract class
를 상속해야 합니다.Logger
abstract class
에서 로깅을 위해 주로 사용되는 method 입니다.log
: log 메서드는 실제 로그 출력하는 부분으로 log에 대한 format
을 함께 설정하게 됩니다.logRequest
: logRequest
는 log를 출력하기전 어떤 FeignRequest
에 대한 log를 남기는 지 출력합니다.logAndRebufferResponse
: 응답을 로그로 기록하고, 필요시 해당 응답을 다시 버퍼링하는 역할을 수행configKey
: Feign Client의 특정 메서드에 대한 식별자logLevel
: 로깅 레벨. Feign Logger의 Level
enum을 사용하여 설정 (NONE, BASIC, HEADERS, FULL)response
: 원본 Feign response
객체elapsedTime
: 요청을 보내고 응답을 받기까지 걸린 시간(밀리초 단위)Response
개체. 이 객체는 원본 응답과 동일하지만, 응답 본문이 다시 읽을 수 있도록 버퍼에 저장package wh.duckbill.openfeign.feign.logger;
import feign.Logger;
import feign.Request;
import feign.Response;
import feign.Util;
import lombok.RequiredArgsConstructor;
import java.io.IOException;
import static feign.Util.*;
@RequiredArgsConstructor(staticName = "of")
public class FeignCustomLogger extends Logger { // Feign의 Logger인지 확인
private static final int DEFAULT_SLOW_API_TIME = 3_000;
private static final String SLOW_API_NOTICE = "SLOW API";
/*
log를 남길 때 어떤 형식으로 남길지 정한다.
*/
@Override
protected void log(String configKey, String format, Object... args) {
System.out.printf(methodTag(configKey) + format + "%n", args);
}
@Override
protected void logRequest(String configKey, Level logLevel, Request request) {
System.out.println("[logRequest]: " + request);
}
/**
*
* @param configKey Feign Client의 특정 메서드에 대한 식별자
* @param logLevel 로깅 레벨. Feign Logger의 Level enum을 사용하여 설정 (NONE, BASIC, HEADERS, FULL)
* @param response 원본 Feign response 객체
* @param elapsedTime 응답 시간
* @return 재 버퍼링된 Response 객체
* @throws IOException
*/
@Override
protected Response logAndRebufferResponse(String configKey, Level logLevel, Response response, long elapsedTime) throws IOException {
// request와 response 모두 handling
String protocolVersion = resolveProtocolVersion(response.protocolVersion());
String reason =
response.reason() != null && logLevel.compareTo(Level.NONE) > 0 ? " " + response.reason()
: "";
int status = response.status();
log(configKey, "<--- %s %s%s (%sms)", protocolVersion, status, reason, elapsedTime);
if (logLevel.ordinal() >= Level.HEADERS.ordinal()) {
for (String field : response.headers().keySet()) {
if (shouldLogResponseHeader(field)) {
for (String value : valuesOrEmpty(response.headers(), field)) {
log(configKey, "%s: %s", field, value);
}
}
}
int bodyLength = 0;
if (response.body() != null && !(status == 204 || status == 205)) {
// HTTP 204 No Content "...response MUST NOT include a message-body"
// HTTP 205 Reset Content "...response MUST NOT include an entity"
if (logLevel.ordinal() >= Level.FULL.ordinal()) {
log(configKey, ""); // CRLF
}
byte[] bodyData = Util.toByteArray(response.body().asInputStream());
bodyLength = bodyData.length;
if (logLevel.ordinal() >= Level.FULL.ordinal() && bodyLength > 0) {
log(configKey, "%s", decodeOrDefault(bodyData, UTF_8, "Binary data"));
}
// 응답 시간이 긴 요청에 대한 로깅
if (elapsedTime > DEFAULT_SLOW_API_TIME) {
log(configKey, "[%s] elapsedTime: %s", SLOW_API_NOTICE, elapsedTime);
}
log(configKey, "<--- END HTTP (%s-byte body)", bodyLength);
return response.toBuilder().body(bodyData).build();
} else {
log(configKey, "<--- END HTTP (%s-byte body)", bodyLength);
}
}
return response;
}
}
[logRequest]: GET http://localhost:8080/target_server/get?name=CustomName&age=1 HTTP/1.1
CustomHeaderName: CustomHeader
Binary data
[DemoFeignClient#callGet] <--- HTTP/1.1 200 (10ms)
[DemoFeignClient#callGet] connection: keep-alive
[DemoFeignClient#callGet] content-type: application/json
[DemoFeignClient#callGet] date: Thu, 13 Jun 2024 15:01:28 GMT
[DemoFeignClient#callGet] keep-alive: timeout=60
[DemoFeignClient#callGet] transfer-encoding: chunked
[DemoFeignClient#callGet] <--- END HTTP (53-byte body)
FROM TARGET SERVER
log
: [DemoFeignClient#callGet]
로그 부분. @Log4j2
등의 logback
을 사용하여 로깅할 수 있다.logAndRebufferResponse
를 보면 elapsedTime
을 이용하여 응답 시간이 3초 이상인 API를 체크하고 로깅하는 것이 가능하다.[logRequest]: GET http://localhost:8080/target_server/get?name=CustomName&age=1 HTTP/1.1
CustomHeaderName: CustomHeader
Binary data
[DemoFeignClient#callGet] <--- HTTP/1.1 200 (3578ms)
[DemoFeignClient#callGet] connection: keep-alive
[DemoFeignClient#callGet] content-type: application/json
[DemoFeignClient#callGet] date: Thu, 13 Jun 2024 15:06:20 GMT
[DemoFeignClient#callGet] keep-alive: timeout=60
[DemoFeignClient#callGet] transfer-encoding: chunked
[DemoFeignClient#callGet] [SLOW API] elapsedTime: 3578 <-- SLOW API LOG
[DemoFeignClient#callGet] <--- END HTTP (53-byte body)
Feign 요청 수행 중 에러가 발생할 경우 에러에 대한 로그를 남기고 handling 하기 위해 사용된다.
package wh.duckbill.openfeign.feign.decoder;
import feign.Response;
import feign.codec.ErrorDecoder;
import org.springframework.http.HttpStatus;
public class DemoFeignErrorDecoder implements ErrorDecoder {
private final ErrorDecoder errorDecoder = new Default();
@Override
public Exception decode(String methodKey, Response response) {
HttpStatus httpStatus = HttpStatus.resolve(response.status());
if (httpStatus == HttpStatus.NOT_FOUND) {
System.out.println("[DemoFeignErrorDecoder] HttpStatus = " + httpStatus);
throw new RuntimeException(
String.format("[RuntimeException] HttpStatus is %s", httpStatus)
); // NOT FOUND 에러에 대하여
}
return errorDecoder.decode(methodKey, response); // 정의 하지 않은 에러에 대한 처리
}
}
HttpStatus.NOT_FOUND
에러에 대하여 별도의 에러 로깅과 에러를 설정하는 것을 확인할 수 있다.new Default()
는 ErrorDecoder
를 implement한 클래스로 정의하지 않은 에러에 대한 공통된 처리를 위해서 사용한다.HttpStatus.NOT_FOUND
이외의 에러는 errorDecoder.decode
를 이용하여 처리되게 된다.@Override
public Exception decode(String methodKey, Response response) {
FeignException exception = errorStatus(methodKey, response, maxBodyBytesLength,
maxBodyCharsLength);
Long retryAfter = retryAfterDecoder.apply(firstOrNull(response.headers(), RETRY_AFTER));
if (retryAfter != null) {
return new RetryableException(
response.status(),
exception.getMessage(),
response.request().httpMethod(),
exception,
retryAfter,
response.request());
}
return exception;
}
errorDecorder.decode
의 구현부package wh.duckbill.openfeign.feign.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import wh.duckbill.openfeign.feign.decoder.DemoFeignErrorDecoder;
import wh.duckbill.openfeign.feign.interceptor.DemoFeignInterceptor;
@Configuration
public class DemoFeignConfig {
@Bean
public DemoFeignInterceptor demoFeignInterceptor() {
return DemoFeignInterceptor.of();
}
@Bean
public DemoFeignErrorDecoder demoFeignErrorDecoder() {
return new DemoFeignErrorDecoder();
}
}
DemoFeignErrorDecoder
클래스는 전역적으로 사용하는 것이 아니므로 DemoFeignConfig
에 bean
등록을 하여 사용할 수 있다.