티스토리 뷰
[Spring] Feign Client - (2) logger, interceptor, error decoder
5_Clock 2024. 2. 22. 22:24서론
Feign Client에서는 요청과 응답을 로깅하는 Logger를 제공합니다.
Feign Client의 Logger을 사용하면 HTTP 요청/응답의 세부사항을 추적할 수 있습니다.
Interceptor는 Request 즉 요청이 실행되기 전 후에 커스텀 로직을 삽입할 수 있게 합니다.
이를 통해 요청을 수정하거나 추가 정보를 로깅하는 등의 작업을 할 수 있습니다.
예를 들어, 모든 요청에 공통 헤더를 추가하거나 인증 토큰을 주입하는 경우 Interceptor를 사용할 수 있습니다.
마지막으로 Error Decoder는 Feign Client가 API로부터 에러 응답을 받았을때의 동작을 정의합니다.
이를 통해 요청을 수정하거나 추가 정보를 로깅하는 등의 작업을 할 수 있습니다.
예를 들어, 모든 요청에 공통 헤더를 추가하거나 인증 토큰을 주입하는 경우 Interceptor를 사용할 수 있습니다.
Feign Logger
Feign Logger는 기본적으로 Logger(feign.Logger)를 상속 받아야 합니다.
public class FeignCustomLogger extends Logger{}
Logger를 상속받게 되면 필수적으로 log()라는 메서드를 상속받아야 합니다.
protected abstract void log(String var1, String var2, Object... var3);
매개변수에 대해서 조금 더 자세하게 살펴보겠습니다.
첫번째 변수인 configKey(var1)은 현재 로깅이 발생하는 Feign 클라이언트의 구성 키를 나타냅니다.
구성 키는 일반적으로 클라이언트 인터페이스의 전체 클래스 이름과 메서드 이름을 포함합니다.
이 키를 통해 로그가 발생하는 정확한 클라이언트와 그 클라이언트의 메서드를 식별할 수 있습니다.
그리고 format(var2), args(var3)는 각각 로그 뒤에 어떤 형식으로 var3을 받아올 지를 정의하는 것입니다.
따라서 log() 메서드는 내가 출력하고 싶은 로그의 형식을 정하는 것입니다.
@Override
protected void log(String configKey, String format, Object... args) {
log.info(String.format(methodTag(configKey) + format , args));
}
그리고 logRequest()와 logAndRebufferResponse() 또한 같이 상속받아서 Logger를 완성할 수 있습니다.
logRequest는 현재 API를 보내는 주소와 타입 등의 정보를 찍는 것이고,
logAndRebufferResponse는 앞선 log()를 로그 데이터를 어떻게 출력할 지 정하는 것입니다.
@Override
protected void logRequest(String configKey, Level logLevel, Request request) {
System.out.println("[Log Request] " + request);
}
@Override
protected Response logAndRebufferResponse(String configKey, Level logLevel, Response response,
long elapsedTime) throws IOException {
String protocolVersion = resolveProtocolVersion(response.protocolVersion());
String reason = response.reason() != null && logLevel.compareTo(Logger.Level.NONE) > 0 ? " " + response.reason() : "";
int status = response.status();
this.log(configKey, "<--- %s %s%s (%sms)", protocolVersion, status, reason, elapsedTime);
if (logLevel.ordinal() >= Logger.Level.HEADERS.ordinal()) {
Iterator var9 = response.headers().keySet().iterator();
while(true) {
String field;
do {
if (!var9.hasNext()) {
int bodyLength = 0;
if (response.body() != null && status != 204 && status != 205) {
if (logLevel.ordinal() >= Logger.Level.FULL.ordinal()) {
this.log(configKey, "");
}
byte[] bodyData = Util.toByteArray(response.body().asInputStream());
bodyLength = bodyData.length;
if (logLevel.ordinal() >= Logger.Level.FULL.ordinal() && bodyLength > 0) {
this.log(configKey, "%s", Util.decodeOrDefault(bodyData, Util.UTF_8, "Binary data"));
}
if(elapsedTime > DEFAULT_SLOW_API_TIME){
log(configKey, "[%s] elapsedTime : %s", SLOW_API_NOTICE, elapsedTime);
}
this.log(configKey, "<--- END HTTP (%s-byte body)", bodyLength);
return response.toBuilder().body(bodyData).build();
}
this.log(configKey, "<--- END HTTP (%s-byte body)", bodyLength);
return response;
}
field = (String)var9.next();
} while(!this.shouldLogResponseHeader(field));
Iterator var11 = Util.valuesOrEmpty(response.headers(), field).iterator();
while(var11.hasNext()) {
String value = (String)var11.next();
this.log(configKey, "%s: %s", field, value);
}
}
} else {
return response;
}
}
logAndRebufferResponse는 기존의 Logger를 그대로 가져왔습니다.
다음과 같이 Logger를 만들고 호출을 하면 아래와 같은 결과값을 기대할 수 있습니다.
INFO 10932 --- [nio-8080-exec-1] c.s.f.feign.logger.FeignCustomLogger : [Log Request] POST http://localhost:8080/target_server/post HTTP/1.1
INFO 10932 --- [nio-8080-exec-1] c.s.f.feign.logger.FeignCustomLogger : [DemoFeignClient#callPost] <--- HTTP/1.1 200 (35ms)
INFO 10932 --- [nio-8080-exec-1] c.s.f.feign.logger.FeignCustomLogger : [DemoFeignClient#callPost] connection: keep-alive
INFO 10932 --- [nio-8080-exec-1] c.s.f.feign.logger.FeignCustomLogger : [DemoFeignClient#callPost] content-type: application/json
INFO 10932 --- [nio-8080-exec-1] c.s.f.feign.logger.FeignCustomLogger : [DemoFeignClient#callPost] date: Wed, 21 Feb 2024 16:34:36 GMT
INFO 10932 --- [nio-8080-exec-1] c.s.f.feign.logger.FeignCustomLogger : [DemoFeignClient#callPost] keep-alive: timeout=60
INFO 10932 --- [nio-8080-exec-1] c.s.f.feign.logger.FeignCustomLogger : [DemoFeignClient#callPost] transfer-encoding: chunked
INFO 10932 --- [nio-8080-exec-1] c.s.f.feign.logger.FeignCustomLogger : [DemoFeignClient#callPost]
INFO 10932 --- [nio-8080-exec-1] c.s.f.feign.logger.FeignCustomLogger : [DemoFeignClient#callPost] {"name":"CustomName","age":1,"header":"CustomHeader"}
INFO 10932 --- [nio-8080-exec-1] c.s.f.feign.logger.FeignCustomLogger : [DemoFeignClient#callPost] <--- END HTTP (53-byte body)
마지막으로 config에 bean으로 등록해주는 작업을 해야합니다.
@Configuration
public class FeignConfig {
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
@Bean
public Logger feignLogger(){
return new FeignCustomLogger();
}
}
여기서 주의할 점은 Logger 레벨을 설정해 줘야하는데 아래를 참고 하면 됩니다.
NONE
로깅을 수행하지 않습니다.
로깅이 필요 없을 때 사용합니다. 예를 들어, 성능이 중요한 운영 환경에서 불필요한 로깅 오버헤드를 줄이고 싶을 때 NONE을 선택할 수 있습니다.
BASIC
기본적인 정보만 로깅합니다. 요청 메서드, URL, 응답 상태 코드, 요청 실행 시간을 포함합니다.
서비스 간의 통신이 성공적으로 이루어지는지 확인하고 싶을 때 유용합니다. 이 레벨은 네트워크 요청이 성공적으로 완료되었는지, 얼마나 시간이 걸렸는지 등의 기본적인 정보를 제공합니다.
HEADERS
BASIC 레벨의 정보에 더해, 요청과 응답 헤더를 로깅합니다.
HTTP 헤더에 중요한 정보나 인증 정보가 포함되어 있을 때, 이러한 헤더 정보를 추적하고 싶을 때 HEADERS 레벨을 사용합니다. 예를 들어, 요청이나 응답의 특정 헤더 값에 문제가 있어서 통신이 실패하는 경우에 문제를 진단하는 데 도움이 될 수 있습니다.
FULL
요청과 응답의 모든 세부 정보를 포함하여 로깅합니다. 이는 헤더, 바디, 메타데이터 등 요청과 응답에 관한 모든 정보를 포함합니다.
디버깅이나 문제 해결 시, HTTP 요청과 응답의 완전한 내용을 파악해야 할 때 FULL 레벨을 사용합니다. 예를 들어, 원격 서비스로부터 예상치 못한 응답을 받았을 때, 요청의 바디나 응답의 바디 내용을 정확히 확인하고 싶을 때 매우 유용합니다.
Interceptor
Interceptor는 Feign 요청이 실제로 실행되기 전이나 후에 사용자 정의 로직을 실행할 수 있도록 해줍니다.
이는 AOP(Aspect-Oriented Programming)의 개념과 유사하고 Feign 클라이언트를 사용하여
외부 서비스를 호출할 때 요청이나 응답을 가로채어 추가적인 처리를 할 수 있게 해줍니다.
사용 목적
- 인증 토큰 추가: API 요청을 보낼 때 인증 토큰을 자동으로 헤더에 추가하는 것과 같이 공통적인 요청 정보를 주입할 때 유용합니다.
- 로깅: 요청과 응답에 대한 세부 정보를 로깅하는 용도로 사용할 수 있습니다.
- 요청 수정: 요청을 서버로 보내기 전에 요청의 헤더, 바디 등을 수정하거나 추가 정보를 주입할 수 있습니다.
- 메트릭 및 모니터링: 요청 처리 시간, 실패율 등의 메트릭을 수집하여 모니터링할 때 사용할 수 있습니다.
아래 예시 코드에서는 GET/POST 요청을 보내는 것의 처리 로직을 다르게 했습니다.
@Slf4j
@RequiredArgsConstructor(staticName = "of")
public class DemoFeignInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
// get 요청일 경우
if(requestTemplate.method().equals(HttpMethod.GET.name())){
log.info("[GET] [DemoFeignInterceptor] queries: " + requestTemplate.queries());
return;
}
// post 요청일 경우
String encodedRequestBody
= StringUtils.toEncodedString(requestTemplate.body(), StandardCharsets.UTF_8);
log.info("[POST] [DemoFeignInterceptor] requestBody: " + encodedRequestBody);
// 추가적으로 본인이 필요한 로직 추가
String convertRequestBody = encodedRequestBody;
requestTemplate.body(convertRequestBody);
}
}
위처럼 RequestInterceptor를 상속받는 Class를 만들어주고 apply() 메서드를 필수로 오버라이드 해야합니다.
apply()에서는 RequestTemplate를 이용해서 요청에 대한 정보를 받을 수 있습니다.
또한 위 소스코드에서 추가적으로 본인이 필요한 로직 추가의 주석 부분에 앞선 사용 사례를 개발하면 됩니다.
마지막으로 Interceptor또한 Bean 객체로 등록하여 자동 주입 받도록 해야합니다.
하지만 Logger와는 조금 다르게 API마다 다른 Interceptor가 필요한 경우가 많습니다.
예를 들어 Naver API와 Kakao API를 호출시 헤더에 들어가는 인증 부분을 Interceptor에 넣어주면 좋습니다.
하지만 Naver와 Kakao는 인증 토큰이 다르기 때문에 Interceptor 또한 다릅니다.
@Configuration
public class DemoFeignConfig {
@Bean
public DemoFeignInterceptor demoFeignInterceptor(){
return DemoFeignInterceptor.of();
}
}
따라서 위처럼 추상적인 인터페이스가 아닌 정확한 DemoFeignInterceptor처럼 정확하게 bean을 만들어 주었습니다.
또한, DemoFeignConfig로 해당 Feign Client에만 쓰일 수 있도록 config도 분리해줬다.
아래 코드처럼 @FeignClient의 configuration에 들어가는 설정 파일이다.
@FeignClient(
name = "demo-client",
url = "${feign.url.prefix}",
configuration = DemoFeignConfig.class
)
public interface DemoFeignClient {
// ...
}
Error Decoder
Error Decoder는 Feign 클라이언트를 통해 외부 서비스를 호출할 때 발생하는 예외 상황을 사용자 정의 방식으로 처리할 수 있게 해줍니다. 이를 통해 서버로부터 받은 에러 응답을 기반으로 적절한 예외를 발생시키거나, 특정 에러 코드에 대해 특별한 로직을 실행할 수 있습니다.
사용 목적
- 에러 응답 처리: 서버로부터 받은 에러 응답 코드(예: 404, 500 등)에 따라 적절한 예외를 발생시키거나, 사용자 정의 예외 처리 로직을 실행할 수 있습니다.
- 응답 기반 로직 실행: 특정 에러 응답에 대해 복구 작업을 시도하거나, 로깅, 알림 전송 등 추가적인 처리를 할 수 있습니다.
- 응답의 세부 정보 활용: 에러 응답 본문에 포함된 세부 정보를 분석하여, 문제의 원인을 파악하거나, 사용자에게 보다 구체적인 피드백을 제공할 수 있습니다.
ErrorDecoder를 사용하려면 ErrorDecoder를 상속받는 객체를 만들어주고 decode() 메서드를 오버라이드 해줍니다.
@Slf4j
public class DemoFeignErrorDecoder implements ErrorDecoder {
// Default error decoder
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){
log.info("[DemoFeignErrorDecoder] Http Status = " + httpStatus);
throw new RuntimeException(String.format("[RuntimeException] Http Status is %s", httpStatus));
}
return errorDecoder.decode(methodKey, response);
}
}
decoder()의 매개변수는 다음과 같은 의미를 갖습니다.
- methodKey: 이 파라미터는 Feign 클라이언트에서 에러가 발생한 특정 메서드를 식별하는 데 사용되는 문자열입니다. methodKey는 주로 Feign 인터페이스 내에 선언된 메서드의 이름과 관련된 정보를 포함하며, 에러 로깅이나 처리 시 어떤 요청에서 문제가 발생했는지 식별하는 데 도움을 줍니다.
- response: response는 외부 서비스로부터 받은 HTTP 응답을 나타내는 feign.Response 객체입니다. 이 객체에는 상태 코드(status), 응답 헤더(headers), 응답 본문(body) 등 호출 결과에 대한 세부 정보가 포함됩니다. ErrorDecoder에서는 이 response 객체를 분석하여 에러의 성격을 파악하고, 적절한 예외를 생성하여 던지는 데 사용됩니다.
그리고 return 부분의 errorDecoder.decode()는 다음과 같은 처리 과정을 거칩니다.
- 응답 상태 코드 분석: Default 디코더는 응답의 HTTP 상태 코드를 검사하여, 클라이언트 에러(4xx)나 서버 에러(5xx)인 경우에 대해 다르게 처리합니다.
- 예외 생성 및 던지기:
- 클라이언트 에러(4xx): 일반적으로 FeignException의 서브클래스인 FeignClientException을 생성하고 던집니다. 이 예외는 HTTP 상태 코드, 요청에 대한 메타데이터, 그리고 응답 본문을 포함할 수 있습니다.
- 서버 에러(5xx): 이 경우에도 FeignException의 서브클래스인 FeignServerException이 생성되고 던져집니다. 이 예외 역시 상태 코드, 메타데이터, 응답 본문을 담을 수 있습니다.
- 기타 상황: HTTP 상태 코드가 명시적으로 처리되지 않는 경우, 일반적으로 FeignException이나 그 서브클래스 중 하나를 던지되, 상황에 맞게 다양한 정보를 포함시킬 수 있습니다.
위처럼 Error Decoder를 만들게 되면 내가 보낸 요청에서 어떤 부분에서 에러가 있는지 파싱이 가능하고, 목적에 맞게 처리가 가능합니다.
'Back End > Spring' 카테고리의 다른 글
[Spring Data] Spring Data MongoDB - insert() vs save() (0) | 2024.04.06 |
---|---|
[Spring] Logback으로 Spring 애플리케이션 Logging 하기 (2) | 2024.03.05 |
[Spring] Feign Client - (1) Naver API 호출하기 (0) | 2024.02.18 |
[Spring] prototype scope 사용 예시 (0) | 2024.02.15 |
[Spring] Spring에서 Proxy 객체를 사용하는 경우 (0) | 2024.02.15 |
- Total
- Today
- Yesterday
- apache kafka
- apache
- K8S
- logback
- Linux
- React
- zookeeper
- feign client
- docker
- NextJS
- API
- KAFKA
- JPA
- Front
- Container
- spring
- Firebase
- Data Engineering
- spring boot
- cs
- OS
- Java
- consumer
- broker
- 리액트
- Producer
- 프론트엔드
- frontend
- rhel
- centos
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |