OpenFeign...Interceptor, Logger, ErrorDecoder Part

duckbill413·2024년 6월 13일
0

Spring boot

목록 보기
12/13
post-thumbnail

OpenFeign

Feign Client 예제 Github

앞서, Feign Client를 이용하여 요청을 전송하는 방법에 대하여 알아봤습니다. 다음으로는 OpenFeign의 Interceptor, Logger, ErrorDecoder를 활용하여 OpenFeign을 좀 더 활용하는 방법에 대하여 알아보겠습니다.

Feign Interceptor

외부로 요청이 나가기 전에 만약 공통적으로 처리해야하는 부분이 있다면 Interceptor를 재정의하여 처리한다.

RequestInterceptor 작성

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);
    }
}
  • RequestInterceptorimplement하여 Interceptor를 구현할 수 있다.
  • RequestTemplate를 intercept해서 요청에 따라 다르게 처리한 것을 확인할 수 있다.
  • 이를 이용하여 DemoFeign에 대한 요청의 공통 로직 처리를 수행할 수 있다.

DemoFeignConfig

위의 RequestInterceptor를 사용하기 위해서는 FeignConfigbean등록을 해주어야 한다.

@Configuration
public class DemoFeignConfig {
    @Bean
    public DemoFeignInterceptor demoFeignInterceptor() {
        return DemoFeignInterceptor.of();
    }
}
  • DemoFeignInterceptor를 보면 @RequiredArgsConstructorstaticName="of"로 설정한 것을 볼 수 있다.

Feign Logger

Feign Logger를 Feign 요청을 통한 수행 과정을 Log로 남기는데 목적이 있다.

application.yml 확인

  • application.yml의 feign 설정에서 loggerLevel이 설정되어 있는지 확인
  • 예시 프로젝트의 경우 demo-clientloggerLevelHEADERS로 설정
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

Logger Config

/**
 * 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();
    }
}
  • Global Feign Config에서 Feign의 Log Level을 전역으로 설정할 필요가 있습니다.
  • FeignCustomLogger 클래스를 bean에 등록하여 Feign Request가 FeignCustomLogger 클래스를 탈 수 있도록 설정

Logger class 생성

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 ErrorDecoder

Feign 요청 수행 중 에러가 발생할 경우 에러에 대한 로그를 남기고 handling 하기 위해 사용된다.

ErrorDecoder 클래스 생성

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의 구현부

ErrorDecoder 클래스 등록

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 클래스는 전역적으로 사용하는 것이 아니므로 DemoFeignConfigbean 등록을 하여 사용할 수 있다.
profile
같이 공부합시다~

0개의 댓글