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입니다.
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 |
댓글