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이고
파라미터들은 요청하기 위한 값들입니다.
요청을 하게 되면 다음과 같이 결괏값이 잘 나오는 것을 확인할 수 있습니다.


'Spring Boot > Feign Client' 카테고리의 다른 글
Feign Client - Error Decoder 및 logger (0) | 2023.01.15 |
---|---|
Feign Client - Interceptor (0) | 2023.01.15 |
댓글