Spring Boot/Feign Client

Feign Client 기본 구성 및 흐름

수수한개발자 2023. 1. 14.
728x90

Feign 이란 ?

선언적으로 사용할 수 있는 Clent이다.
(= Feign is a declarative web service client. )
 ref : https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html
이 글을 작성하게 된 이유는 백엔드 프로젝트를 하다 보면 외부 API와 통신을 할 일이 생기게 됩니다.
그럴 때 자주 사용하는 것이 HttpURLConnection이나 RestTemplate입니다.
두 개다 사용해봤지만 HttpURLConnection 은 HTTP 한번 호출하기 위해서 많은 코드를 작성해야 생산성이 떨어지는  단점이 있고 
RestTemplate는 spring5부터는 Deprecated 되어 있습니다.
그래서 다른 대안을 찾다가 발견한것이 Feign Client입니다. 
 

Spring Cloud OpenFeign

Feign is a declarative web service client. It makes writing web service clients easier. To use Feign create an interface and annotate it. It has pluggable annotation support including Feign annotations and JAX-RS annotations. Feign also supports pluggable

docs.spring.io

 

 

 
 

Feign Feature

Feign의 4가지에 대해 글을 작성해 보도록 하겠습니다.

  • -  Connection/Read Timeout
  • -  Feign Interceptor
  • -  Feign Logger
  • -  Feign ErrorDecoder

 
 

Connection/Read Timeout

- 외부서버와통신시

  • -  Connection / Read Timeout
  • -  설정이 가능하다.
feign:
  url:
    prefix: http://localhost:8080/target_server #DemoFeignClient에서 사용할 url prefix 값
  client:
    config:
      default:
        connect-timeout: 1000
        read-timeout: 3000
        logger-level: NONE
      demo-client: #DemoFeignClient에서 사용할 Client 설정 값
       connect-timeout: 1000
       read-timeout: 10000
       logger-level: HEADERS # 여기서 설정한 값은 FeignCustomLogger -> Logger.Level logLevel 변수에 할당됨

 
loggerLevel 옵션
NONE : No logging
BASIC : Log only the request method and URL and the response status code and execution time.
HEADERS : Log the basic information along with request and response headers
FULL : Log the headers, body, and metadata for both requests and response
 
 

Feign Interceptor

  • -  외부로 요청이 나가기 전에
  • -  만약 공통적으로
  • -  처리해야하는 부분이 있다면
  • -  Interceptor를 재정의하여
  • -  처리가 가능하다.
@RequiredArgsConstructor(staticName = "of")
public class DemoFeignInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate template) {

        //get
        if (template.method() == Request.HttpMethod.GET.name()) {
            System.out.println("[GET] [DemoFeignInterceptor] queries  : " + template.queries());
            return;
        }

        //post
        String encodedRequestBody = StringUtils.toEncodedString(template.body(), StandardCharsets.UTF_8);
        System.out.println("[POST] [DemoFeignInterceptor] requestBody : " + encodedRequestBody);

        //추가적으로 본인이 필욯나 로직을 추기

        String convertRequestBody = encodedRequestBody;
        template.body(convertRequestBody);
    }
}

 
 

Feign CustomLogger

  • -  Request / Response
  • -  운영을 하기 위한
  • -  적절한 Log를 남길 수 있다.
@RequiredArgsConstructor
public class FeignCustomLogger extends Logger {
    private static final int DEFAULT_SLOW_API_TIME = 3_000;
    private static final String SLOW_API_NOTICE = "Slow API";
    @Override
    protected void log(String configKey, String format, Object... args) {
        //로그를 어떤 형식으로 남길지 정해준다.
        System.out.println(String.format(methodTag(configKey) + format, args));
    }

    @Override
    protected void logRequest(String configKey, Level logLevel, Request request) {
        System.out.println("[logRequest] : " + 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(Level.NONE) > 0 ? " " + response.reason() : "";
        int status = response.status();
        //elapsedTime 요청과 응답의 걸린 시간
        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;
    }
}

 
 
 

Feign ErrorDecoder

요청에 대해
- 정상응답이아닌경우 - 핸들링이 가능하다.

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] Http Status = " + httpStatus);
            throw new RuntimeException(String.format("[RuntimeException] Http Status is %s", httpStatus));
        }

        return errorDecoder.decode(methodKey, response);
    }
}

 
이번 글은 기본적인 세팅을 통해 외부 API를 호출하는 내용만 작성하도록 하겠습니다.
 

프로젝트 구조

 
기본적인 설정부터 알아보겠습니다.

build.gradle

ext {
    /**
     * Spring Boot and springCloudVersion must be compatible.
     * 2.6.x, 2.7.x (Starting with 2021.0.3) = 2021.0.x
     * ref : https://spring.io/projects/spring-cloud
     */
    set('springCloudVersion', '2021.0.4')

    set('commonsLangVersion', '3.12.0')
}

dependencyManagement {
    imports {
        mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
    }
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'

    //Feign
    implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
}

dependencies에 feign을 추가해 준 뒤 
spring-cloud의 버전을 ext set을 통해 동적으로 받아옵니다.
스프링 클라우드의 버전은 엄청 예민하기 때문에

* ref : https://spring.io/projects/spring-cloud

위의 사이트에서 지원되는 버전 설정을 맞춰해주어야 합니다.
그리고 @EnableFeignClients 어노테이션을 붙여주어야 합니다.
 

DemoController

외부 API와 통신을 할 컨트롤러입니다.

@RestController
@RequiredArgsConstructor
public class DemoController {

    private final DemoService demoService;

    @GetMapping("/")
    public String getController() {
        return demoService.get();
    }
}

 

DemoService

feign 클라이언트를 통해 외부 API를 호출하는 서비스입니다.
 

@Service
@RequiredArgsConstructor
public class DemoService {

    private final DemoFeignClient demoFeignClient;

    public String get() {
        ResponseEntity<BaseResponseInfo> response =
                demoFeignClient.callGet("CustomHeader", "CustomName", 1L);

        System.out.println("Name = " + response.getBody().getName());
        System.out.println("Age : " + response.getBody().getAge());
        System.out.println("Header : " + response.getBody().getHeader());
        return "get";
    }
}

여기서 파라미터들은 외부 API에 요청을 하기 위한 값들입니다.
response는 외부 API로부터 받아온 값들입니다.
그리고 feign이라는 패키지를 만들어 줍니다.
 

DemoFeignClient

@FeignClient(
        name = "demo-client",
        url = "${feign.url.prefix}",
        configuration = DemoFeignConfig.class
)
public interface DemoFeignClient {

    @GetMapping("/get") // -> http://localhost:8080/target_server  get 으로 요청이 감
    ResponseEntity<BaseResponseInfo> callGet(@RequestHeader("CustomHeaderName") String customHeader,
                                             @RequestParam("name") String name,
                                             @RequestParam("age") Long age);
}

 

@FeignClient

선언적으로 사용할 수 있는 만큼 정의를 다 해주어야 합니다.
 
name : 이 feignClient를 구분할 수 있는 pk 같은 속성입니다. yml 파일에 각 클라이언트마다 설정을 다르게 할 수 있는 만큼 그 클라이언트들을 구분할 수 있는 값을 주는 것입니다.
 
url : target_url입니다 yml에 정의해 둔 값을 통한 ${}를 통해 주입받을 수 있고 외부 API의 URL 값을 넣어줍니다. 
 
configuration : FeignClient의 구성을 정의하는 클래스를 적어줍니다.
 
인터페이스로 만든 뒤 메서드를 정의해줍니다.
여기서 @GetMapping("/get") 이 의미하는 바는 @FeignClient의 정의된 url/get으로 요청을 한다는 의미입니다.

ResponseEntity<BaseResponseInfo> callGet(@RequestHeader("CustomHeaderName") String customHeader,
                                         @RequestParam("name") String name,
                                         @RequestParam("age") Long age);

위 코드의 의미는 통신 후 response를 받기 위한 포맷을 정의해둔 BaseResponseInfo이고
파라미터들은 요청하기 위한 값들입니다.
 
 
요청을 하게 되면 다음과 같이 결괏값이 잘 나오는 것을 확인할 수 있습니다.

 
 
 
 
 
 

728x90

'Spring Boot > Feign Client' 카테고리의 다른 글

Feign Client - Error Decoder 및 logger  (0) 2023.01.15
Feign Client - Interceptor  (0) 2023.01.15

댓글