Spring Cloud OpenFeign은 Spring Cloud 프로젝트의 일부로, 마이크로서비스 간의 HTTP 기반 통신을 단순화하는데 사용되는 선언적 REST 클라이언트입니다. Feign은 Netflix에서 개발되었으며, Spring Cloud는 이를 Spring 생태계에 통합하여 마이크로서버스 아키텍처에서 서비스 간 통신을 더 쉽게 구현할 수 있도록 지원합니다.
Spring Cloud OpenFeign 주요 특징
- 선언적 REST 클라이언트 : 인터페이스에 어노테이션을 추가함으로써 HTTP 요청을 추상화 합니다. 이를 통해 개발자는 HTTP 클라이언트 코드를 직접 작성하는 대신 비즈니스 로직에 더 집중할 수 있습니다.
- 통합된 로드밸런싱 : Eureka, Consul 같은 서비스 디스커버리 도구와 통합하여 로드 밸런싱을 자동으로 처리할 수 있습니다.
- 풍부한 옵션의 설정 : 연결 타임아웃, 리드 타임아웃, 리트라이 등의 다양한 네트워크 옵션을 설정할 수 있습니다.
- Hystrix와의 통합 : Hystrix를 사용하여 회로 차단기 패턴을 구현할 수 있으며, 이를 통해 더 견고한 시스템을 구축할 수 있습니다.
마이크로서비스 예시
다음은 user-service와 order-service라는 두 마이크로 서비스 간의 통신을 구현한 예시입니다. user-service에서 order-service에 있는 사용자의 주문 목록을 조회하는 예를 들어 만들어 보겠습니다.
의존성 추가
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign:4.0.6'
application.yml Feign 설정
feign:
client:
config:
orders-service:
connectTimeout: 5000 // connection tme out 5초
readTimeout: 5000 // read time out 5초
Feign 클라이언트 인터페이스 생성
user-service에 OrderServiceClient 인터페이스를 생성하고 @FeignClient 어노테이션을 사용하여 order-service와의 통신을 정의 합니다. 이때, @FeignClient 어노테이션에 서비스 이름 order-service를 지정함으로써 자동으로 서비스 디스커버리와 통합됩니다. 예를 들어, order-service에 접근하기 위해 @FeignClient(name = "orders-service", path = "/orders") 를 작성함으로써 Spring Cloud는 order-service의 현재 위치를 서비스 디스커리에서 자동으로 조회하여 해당 엔드 포인트로 요청을 보냅니다.
@FeignClient(name = "orders-service", path = "/orders")
public interface OrdersServiceClient {
@GetMapping("/{userId}")
List<Order> getOrdersByUserId(@PathVariable("userId") String userId);
}
Feign 클라이언트 사용
OrderServiceClient를 사용하여 user-service 내에서 order-service의 /order/{userId} 엔드 포인트를 호출할 수 있습니다.
@Service
@RequiredArgsConstructor
public class UserService {
private final OrdersServiceClient ordersServiceClient;
public List<Order> getUserOrders(String userId) {
return ordersServiceClient.getOrdersByUserId(userId);
}
}
기타 설정 ( 참고 사항)
아래 코드는 호출시 500에러가 발생하게 되면 재시도를 하는 설정 입니다.
@Slf4j
public class FeignClientExceptionErrorDecoder implements ErrorDecoder {
private ErrorDecoder errorDecoder = new Default();
@Override
public Exception decode(String methodKey, Response response) {
log.error("요청 실패 FeignClientExceptionErrorDecoder.decode() methodKey: {}, response: {}", methodKey, response);
FeignException exception = FeignException.errorStatus(methodKey, response);
HttpStatus httpStatus = HttpStatus.valueOf(response.status());
if (httpStatus.is5xxServerError()) {
return new RetryableException(
response.status(),
exception.getMessage(),
response.request().httpMethod(),
exception,
null,
response.request());
}
return errorDecoder.decode(methodKey, response);
}
}
Feign Client 사용자 설정
@EnableFeignClients(basePackages = "com.app") 은 Feign 클라이언트를 활성화 하고, 스캔할 패키지를 지정합니다. 설정된 패키지 및 하위 패키드에 Feign 클라이언트 인터페이스를 찾아 자동으로 등록 합니다. 이렇게 되면 Feign을 사용하여 다른 서비스를 호출하기 위한 클라이언트 인터페이스가 자동으로 구성됩니다.
feignLoggerLevel() 메서드는 로깅 수준을 설정하는 빈을 정의합니다. Logger.Level.FULL은 요청과 응답의 헤더, 바디, 메타데이터를 포함하여 모든 HTTP 통신에 대한 자세한 로깅을 보여 주도록 합니다. 이는 디버깅 목적으로 유용합니다.
feignRetry() 는 클라이언트의 재시도 정책을 설정하는 빈을 정의합니다. new Retryer.Default(1000, 2000, 3) 의 첫번째 값은 처 번째 재시도 사이의 지연시간을 1000밀리초(1초), 두번째 값은 재시도 사이의 최대 지연 시간을 2000밀리초(2초)로 설정하고, 마지막 값은 최대 3번까지 재시도 하도록 구성합니다. 이는 일시적인 네트워크 문제나 일시적인 서비스 불가 상황에서 요청을 다시 시도함으로써 복원력을 높이는데 유용합니다. 이 코드는 Feign 클라이어느의 고급 사용사례를 보여주며, 로깅, 에러처리, 재시도 매커니즘을 사용자가 지정하여 마이크로서비스간 통신의 안정성과 가시성을 향상 시킵니다.
@Configuration
@EnableFeignClients(basePackages = "com.app")
public class FeignConfiguration {
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
@Bean
public ErrorDecoder errorDecoder() {
return new FeignClientExceptionErrorDecoder();
}
@Bean
Retryer.Default retry() {
// 실행주기 1초, 최대 3번 재시도
return new Retryer.Default(1000,2000,3);
}
}
내부 테스트를 위한 코드
로컬:8080 환경에서 테스트를 위한 코드를 작성합니다. HelloClient 인터페이스를 만들고, 특정 url의 엔드포인트를 호출하도록 합니다.
호출 순서는 HealthFeignTestController -> HelloClient -> http://localhost:8080/api/health-check 입니다.
Feign 클라이언트 호출을 위한 인터페이스 선언
@FeignClient(url = "http://localhost:8080", name = "helloClient")
public interface HelloClient {
@GetMapping(value = "/api/health-check", consumes = "application/json")
HealthCheckResponseDto healthCheck();
}
/api/health/feign-test 호출시 feign client 인터페이스 메서드 호출
@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class HealthFeignTestController {
private final HelloClient helloClient;
@GetMapping("/health/feign-test")
public ResponseEntity<HealthCheckResponseDto> feignTest() {
HealthCheckResponseDto healthCheckResponseDto = helloClient.healthCheck();
return ResponseEntity.ok(healthCheckResponseDto);
}
}
HelloClient의 엔드 포인트 호출 ( http://localhost:8080/api/health-check)
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class HealthCheckController {
private final Environment environment;
@GetMapping("/health-check")
public ResponseEntity<HealthCheckResponseDto> healthCheck() {
log.info("activeProfiles={}",environment.getActiveProfiles());
return ResponseEntity.ok(HealthCheckResponseDto.builder()
.health("ok")
.activeProfiles(List.of(environment.getActiveProfiles()))
.build());
}
}
결과 확인
~> http get http://localhost:8080/api/health/feign-test
HTTP/1.1 200
Connection: keep-alive
Content-Type: application/json
Date: Wed, 07 Feb 2024 11:25:40 GMT
Keep-Alive: timeout=60
Transfer-Encoding: chunked
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
{
"activeProfiles": [
"test"
],
"health": "ok"
}
--- console log
[HelloClient#healthCheck] ---> GET http://localhost:8080/api/health-check HTTP/1.1
[HelloClient#healthCheck] Content-Type: application/json
[HelloClient#healthCheck] ---> END HTTP (0-byte body)
activeProfiles=test
[HelloClient#healthCheck] <--- HTTP/1.1 200 (88ms)
[HelloClient#healthCheck] connection: keep-alive
[HelloClient#healthCheck] content-type: application/json
[HelloClient#healthCheck] date: Wed, 07 Feb 2024 11:25:39 GMT
[HelloClient#healthCheck] keep-alive: timeout=60
[HelloClient#healthCheck] transfer-encoding: chunked
[HelloClient#healthCheck] vary: Access-Control-Request-Headers
[HelloClient#healthCheck] vary: Access-Control-Request-Method
[HelloClient#healthCheck] vary: Origin
[HelloClient#healthCheck]
[HelloClient#healthCheck] {"health":"ok","activeProfiles":["test"]}
[HelloClient#healthCheck] <--- END HTTP (41-byte body)
에러 발생시 확인 (500 Connection time out error )
HelloClient 의 url 을 8081 port 로 변경, 현재 어플리케이션은 8080으로 제공되므로 500 Connection time out 에러가 발생하게 됩니다. 이때 재시도를 3번 하고 예외를 반환하게 됩니다.
http get http://localhost:8080/api/health/feign-test
HTTP/1.1 500
Connection: close
Content-Type: application/json
Date: Wed, 07 Feb 2024 11:40:49 GMT
Transfer-Encoding: chunked
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
{
"errorCode": "500 INTERNAL_SERVER_ERROR",
"errorMessage": "Connection refused executing GET http://localhost:8081/api/health-check"
}
-- console log
// 아래와 같은 에러가 3번 발생하게 됩니다.
feign.RetryableException: Connection refused executing GET http://localhost:8081/api/health-check
[HelloClient#healthCheck] <--- END ERROR
[HelloClient#healthCheck] ---> RETRYING
[HelloClient#healthCheck] ---> GET http://localhost:8081/api/health-check HTTP
[HelloClient#healthCheck] Content-Type: application/json
[HelloClient#healthCheck] ---> END HTTP (0-byte body)
[HelloClient#healthCheck] <--- ERROR ConnectException: Connection refused (2ms
[HelloClient#healthCheck] java.net.ConnectException: Connection refused
'Spring, Springboot' 카테고리의 다른 글
자가 호출 (0) | 2024.07.16 |
---|---|
타입 기반 주입 (0) | 2024.07.15 |
서비스 ( Service ) (1) | 2024.07.10 |
전역 에러 처리 (0) | 2024.01.30 |
CORS 이해와 설정 (0) | 2024.01.29 |