[Spring Cloud] OpenFeign 메뉴얼 정리 with Spring REST Clients

이동엽·2024년 4월 20일
6

spring

목록 보기
15/21

Spring Application을 사용하다보면 다른 API Server를 호출하는 경우가 자주 발생합니다.

이때 사용할 수 있는 Spring Cloud에서 제공하는 OpenFeign 메뉴얼을 정리해보고, 이외에 사용할 수 있는 여러 REST Client에 대해 정리하는 글입니다. (RestTemplate, WebClient, RestClient, HttpInterface)

아래 나오는 모든 예제 코드는 Github에서 확인하실 수 있습니다.


아래 두 가지에 해당되는 분들은 RestClient & HttpInterface 챕터를 보시길 추천드립니다.

  • RestTemplate, WebClient에 대해 학습을 했고, 이미 사용 중이신 분들
  • Spring Cloud 혹은 MSA에 익숙하며 OpenFeign을 이미 사용 중이신 분들

RestClient와 HttpInterface에 대해서는 최근 기술이다보니, 도큐먼트 외에는 자료가 많이 없습니다. 정리한 내용에 오류가 있다면 알려주시면 감사하겠습니다.

  • RestClient : Spring Boot 3.2부터 지원된 기능
    • RestTemplate + WebClient의 장점만을 합친 기능
  • HttpInterface : Spring 6에 추가된 선언형 Http Client 기능.
    • OpenFeign과 같이 애너테이션 & 인터페이스 기반의 선언형으로 작성.

1. OpenFeign

1.1 OpenFeign 소개

1.1.1 OpenFeign이란?

Netflix에 의해 처음 만들어진 Declarative(선언적) HTTP Client 도구로써, 외부 API 호출을 쉽게 할 수 있도록 도와준다.

여기서 선언적이란 애너테이션 사용을 의미한다.


1.1.2 OpenFeign 장점

  • 인터페이스와 애너테이션 기반으로 작성할 코드가 줄어듬
  • 익숙한 Spring MVC 애너테이션으로 개발이 가능
  • 다른 Spring Cloud 기술들 (Eureka, Circuit Breaker, LoadBalancer) 과의 통합이 쉬움

1.1.3 OpenFeign 단점 및 한계

  • 공식적인 Reactive 모델을 지원하지 않는다.
    • 따라서 CompletableFuture 등과 함께 처리해야 함.
    • 다만, 구현체를 AsyncFeign으로 구현한다면 처리가 가능함으로 보임.
  • 테스트 도구를 제공하지 않는다.
    • RestTemplate의 경우 TestRestTemplate 등이 존재.
    • WireMock을 이용한 외부 API를 목킹할 수 있다고 함.

1.2 기능

  • 타임아웃, 재시도(Retry) 지원
  • 각각의 Feign Client별로 로그 레벨을 지정 가능
  • 구성 속성을 활용한 설정 가능
  • Fallback 설정 가능
    • Fallback : 실행을 실패(Exception)하는 경우에 대신 실행되게하는 프로세스
  • Error Handling (Error Decoder) 지원
  • RequestInterceptor 지원

1.3 활용코드

1.3.1 의존성 추가

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    <version>4.1.0</version>
</dependency>

1.3.2 Fegin Client 활성화 및 정의

  • @EnableFeignClients : Feign Client 활성화
@EnableFeignClients
@SpringBootApplication
public class RequestServerApplication { ... }

Feign Client 정의

  • @FeignClient : FeignClient 임을 명시
    • value : 이름 지정
    • url : 요청 주소 지정
@FeignClient(value = "openFeignClient", url = "${url.server.response.endpoint")
public interface OpenFeignClient {

    @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
    CommonResponse send(@RequestBody CommonRequest request);
}

사용 방식

@Slf4j
@Service
@RequiredArgsConstructor
public class OpenFeignService {

    private final OpenFeignClient feignClient;

    /**
     * FeignClient로 외부 API를 호출하는 예시
     */
    public CommonResponse send(final CommonRequest request) {
        log.info("request[{}]", request);

        return feignClient.send(request);
    }
}

실행 결과


1.3.3 타임아웃, 로그 설정

타임아웃, 로그 레벨 설정

  • connect-timeout : 클라이언트가 서버에 연결하기 위해 대기하는 시간을 설정
  • read-timeout : 클라이언트가 서버로부터 데이터를 읽는 데 걸리는 시간을 설정
  • logger-level : 로그 레벨 설정(NONE, BASIC, HEADERS, FULL)
spring:
  cloud:
    openfeign:
      client:
        config:
          default:                   # 공통 설정
            connect-timeout: 5000    # default : 1000
            read-timeout: 30000       # default : 60000
            logger-level: BASIC
          openFeignClient:           # 특정 Feign Client 설정
            connect-timeout: 1000
            read-timeout: 20000
            logger-level: FULL

FULL 로그 레벨 설정 예시


1.3.4 재시도(Retry) 설정

설정 클래스 작성

/**
 * @author 이동엽(Lee Dongyeop)
 * @date 2024. 03. 24
 * @description FeignClient 전용 Retry 설정 클래스
 */
public class FeignClientRetryConfig {

    /**
     * openFeignClient 전용 Retryer
     * {period} 의 간격으로 시작해 최대 {duration}의 간격으로 증가
     * 최대 {maxAttempt}번 재시도한다.
     */
    @Bean
    Retryer.Default openFeinClientRetryer() {
        // 0.1초의 간격으로 시작해 최대 3초의 간격으로 점점 증가하며, 최대5번 재시도한다.
        return new Retryer.Default(
                period,                               // default : 100
                TimeUnit.SECONDS.toMillis(duration),  // default : 1L
                maxAttempt                            // default : 5
        );
    }
}

FeignClient에만 적용되도록 설정 클래스 지정

@FeignClient(
    value = "openFeignClient", 
    url = "${url.server.response.endpoint}", 
    configuration = FeignClientRetryConfig.class)
public interface OpenFeignClient {
		...
}

1.3.5 RequestInterceptor 설정

요청을 수행하기 전, 인터셉터에서 헤더 정보를 수정할 수도 있다.

@Slf4j
public class UrlDecodeRequestInterceptor implements RequestInterceptor {

    /**
     * 요청이 수행되기 전, Authorization 헤더를 설정
     */
    @Override
    public void apply(final RequestTemplate requestTemplate) {
        requestTemplate.header(CommonConst.AUTHORIZATION, CommonConst.BEARER);
    }
}

마찬가지로, 구성 속성을 이용해 원하는 Feign Client에 지정할 수 있다.

spring:
  cloud:
    openfeign:
      client:
        config:
          openFeignClient:
            request-interceptors:
              - {경로}.AuthorizationRequestInterceptor

1.3.6 Error Handling 설정

Feign은 오류 발생시 항상 FeignException을 발생 시킨다.

따라서 응답의 상태 코드에 따라 사용자 정의에 적합하게 동작하도록 ErrorDecoder를 정의할 수 있다.

@Slf4j
public class CustomErrorDecoder implements ErrorDecoder {

    @Override
    public Exception decode(final String methodKey, final Response response) {
        log.warn("statusCode[{}], methodKey[{}]", response.status(), methodKey);

        return switch (response.status()) {
            case 400 -> new CustomException("bad request", 400);
            case 404 -> new CustomException("not found", 404);
            case 500 -> new CustomException("internal server error", 500);
            default -> new CustomException("feign client call error");
        };
    }
}

마찬가지로, 구성 속성을 이용해 원하는 Feign Client에 지정할 수 있다.

spring:
  cloud:
    openfeign:
      client:
        config:
          openFeignClient:
            error-decoder: io.dongvelop.requestserver.common.CustomErrorDecoder

1.4 장점 및 기능 정리

장점

  • 인터페이스와 애너테이션 기반으로 작성할 코드가 줄어듬
  • 익숙한 Spring MVC 애너테이션으로 개발이 가능
  • 다른 Spring Cloud 기술들 (Eureka, Circuit Breaker, LoadBalancer) 과의 통합이 쉬움

기능

  • 타임아웃, 재시도(Retry) 지원
  • 각각의 Feign Client별로 로그 레벨을 지정 가능
  • Error Handling (Error Decoder) 지원
  • RequestInterceptor 지원
  • 아래와 같이 구성 속성을 활용한 다양한 설정 가능
spring:
	cloud:
		openfeign:
			client:
				config:
					feignName:
                        url: http://remote-service.com
						connectTimeout: 5000
						readTimeout: 5000
						loggerLevel: full
						errorDecoder: com.example.SimpleErrorDecoder
						retryer: com.example.SimpleRetryer
						defaultQueryParameters:
							query: queryValue
						defaultRequestHeaders:
							header: headerValue
						requestInterceptors:
							- com.example.FooRequestInterceptor
							- com.example.BarRequestInterceptor
						responseInterceptor: com.example.BazResponseInterceptor
						dismiss404: false
						encoder: com.example.SimpleEncoder
						decoder: com.example.SimpleDecoder
						contract: com.example.SimpleContract
						capabilities:
							- com.example.FooCapability
							- com.example.BarCapability
						queryMapEncoder: com.example.SimpleQueryMapEncoder
						micrometer.enabled: false

2. RestTemplate

2.1 RestTemplate 소개

스프링 프레임워크에서 제공하는 RESTful API 통신을 위한 도구이다.

  • 다양한 HTTP Method를 사용하며, 외부 서버와 동기식 방식으로 통신한다.
  • 동기식 으로 요청을 보내고 응답을 받을 때까지 블로킹된다. (응답을 기다린다.)

번외

Spring 공식 문서에서 RestTemplate을 확인하면 아래와 같이 내용이 있다.

  • 동기식 HTTP 접근에 사용하고 있을 경우 → Spring 6.1에 나온 RestClient 사용을 권장
  • 비동기 및 스트리밍 시나리오의 경우 → WebClient 사용을 권장


2.2 RestTemplate 기능 및 특징

2.2.1 HTTP 요청 및 응답에 대해 동기식 요청으로 블로킹하여 데이터를 주고받는다.

2.2.2 다양한 HTTP Method를 사용

메서드명설명
delete(…)지정된 URL의 리소스에 HTTP DELETE 요청을 수행한다.
exchange(…)지정된 HTTP 메서드를 URL에 실행, ResponseEntity를 반환
execute(…)지정된 HTTP 메서드를 URL에 실행, 응답 Body와 연결되는 객체를 반환
getForEntity(…)HTTP GET 요청을 전송, ResponseEntity를 반환
getForObject(…)HTTP GET 요청을 전송, 응답 Body와 연결되는 객체를 반환
headForHeaders(…)HTTP HEAD 요청을 전송, 지정된 리소스 URL의 헤더를 반환
optionsForAllow(…)HTTP OPTIONS 요청을 전송, 지정된 리소스 URL의 Allow 헤더를 반환
patchForObject(…)HTTP PATCH 요청을 전송, 응답 Body와 연결되는 객체를 반환
postForEntity(…)URL에 데이터를 POST, ResponseEntity를 반환
postForLocation(…)URL에 데이터를 POST, 새로 생성된 리소스의 URL을 반환
postForObject(…)URL에 데이터를 POST, 응답 Body와 연결되는 객체를 반환
put(…)리소스 데이터에 지정된 URL에 PUT

2.3 RestTemplate 활용 코드

2.3.1 의존성 추가

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
<dependencies>

2.3.2 로그 레벨 수정

RestTemplate은 DEBUG 레벨로 API 통신 정보를 보여준다.

logging:
  level:
    web: debug

2.3.3 재시도(ReTry) 구현

  1. 의존성 추가
<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
    <version>2.0.5</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
  1. 애플리케이션 메인 클래스에 애너테이션 추가
@EnableRetry
@SpringBootApplication
public class RequestServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(RequestServerApplication.class, args);
    }
}
  1. Retry를 수행할 메서드에 애너테이션 추가
@Retryable(maxAttempts = 3)
public CommonResponse sendRetry(final CommonRequest request) { ... }

2.3.4 RestTemplate 객체 생성

  1. 클래스 내의 객체로 생성하기
RestTemplate restTemplate = new RestTemplate();
  1. Bean으로 등록 후, 의존성 주입하여 사용하기 + 타임아웃 설정 추가
@Configuration
public class RestTemplateConfig {

    @Value("${restTemplate.connectTimeOut}")
    private Long connectTimeOut;
    @Value("${restTemplate.readTimeOut}")
    private Long readTimeOut;

    @Bean
    @Qualifier("restTemplate")
    public RestTemplate restTemplate(final RestTemplateBuilder restTemplateBuilder) {
        return restTemplateBuilder
                .setConnectTimeout(Duration.ofSeconds(connectTimeOut)) // 외부 API 서버에 연결 요청 시간
                .setReadTimeout(Duration.ofSeconds(readTimeOut))       // 외부 API 서버로부터 데이터를 읽어오는 시간
                .build();
    }
}

2.3.5 실제 사용 코드

@Slf4j
@Service
@RequiredArgsConstructor
public class RestTemplateService {

    @Value("${url.server.response.endpoint}")
    private String responseServerUrl;
    @Value("${url.server.response.retry}")
    private String retryRequestPath;

    private final RestTemplate restTemplate;

    /**
     * RestTemplate 으로 응답 서버에 요청보내기 <br/>
     * - Retry 및 TimeOut을 제공하지 않음. <br/>
     * - 비즈니스 코드에 지저분한 설정 코드들이 섞임. <br/>
     * - OpenFeign에 비해 제공되는 로그가 불친절함
     *
     * @param request : 공통 요청 형태
     * @return : 공통 응답 형태
     */
    public CommonResponse send(final CommonRequest request) {
        log.info("request[{}]", request);
        settingHeader();
        return restTemplate.postForObject(responseServerUrl, request, CommonResponse.class);
    }

    /**
     * RestTemplate은 Retry를 지원하지 않아, 아래처럼 Spring-Retry를 이용하여 구현
     */
    @Retryable(maxAttempts = 3)
    public CommonResponse sendRetry(final CommonRequest request) {
        log.info("request[{}]", request);
        settingHeader();
        return restTemplate.postForObject(responseServerUrl + retryRequestPath, request, CommonResponse.class);
    }

    private void settingHeader() {
        /* Header에 필요한 정보 삽입 */
        final ArrayList<ClientHttpRequestInterceptor> interceptors = new ArrayList<>();
        interceptors.add(new RequestHeaderInterceptor(CommonConst.AUTHORIZATION, CommonConst.BEARER + "restTemplateExample"));
        interceptors.add(new RequestHeaderInterceptor(CommonConst.CONTENT_TYPE_KEY, MediaType.APPLICATION_JSON_VALUE));
        restTemplate.setInterceptors(interceptors);
    }
}

2.3.6 RestTemplate 단점

  • 비즈니스 코드에 지저분한 설정들이 섞인다.
  • Retry를 구현할 경우, 추가 의존성(spring-retry, spring-boot-starter-aop)을 필요로 한다.
  • 제공되는 로그가 자세하지 않다.


3. WebClient

기본적으로 외부 API를 호출하기 위해 내부적으로 HttpClient를 이용하는 점은 RestTemplate과 동일하다.

3.1 RestTemplate와 차이점

  • 기존의 동기 API를 제공할 뿐만 아니라, 논블로킹 및 비동기 접근 방식을 지원해서 효율적인 통신이 가능
  • 외부 API로 요청할 때 리액티브 타입의 전송과 수신을 한다. (Mono, Flux)

3.2 WebClient의 특징

  • 싱글 스레드 방식으로 동작
  • Non-Blocking 방식을 사용

3.3 WebClient 활용 코드

3.3.1 의존성 추가

<dependency>
    <groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

3.3.2 로그 레벨 수정

logging:
  level:
    web: debug
    reactor: debug

3.3.3 설정 클래스 작성

  • 타임아웃 설정
  • 요청 BaseUrl 설정
  • 헤더 설정
@Configuration
public class WebClientConfig {
    @Value("${url.server.response.endpoint}")
    private String responseServerUrl;
    @Value("${webClient.timeout.connect}")
    private Integer connectTimeout;
    @Value("${webClient.timeout.response}")
    private Long responseTimeout;
    @Value("${webClient.timeout.read}")
    private Long readTimeout;
    @Value("${webClient.timeout.write}")
    private Long writeTimeout;
    @Value("${webClient.maxMemorySize}")
    private int maxMemorySize;

    /**
     * 공통으로 쓰이는 WebClient
     */
    @Bean
    public WebClient webClient() {

        // 타임아웃 설정을 위한 HttpClient
        final HttpClient httpClient = HttpClient.create()
                // 연결 타임아웃
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectTimeout) 
                // 응답 타임아웃
                .responseTimeout(Duration.ofSeconds(responseTimeout))     
                .doOnConnected(conn -> conn
                        // 읽기 작업 타임아웃
                        .addHandlerLast(new ReadTimeoutHandler(readTimeout, TimeUnit.SECONDS))
                        // 쓰기 작업 타임아웃
                        .addHandlerLast(new WriteTimeoutHandler(writeTimeout, TimeUnit.SECONDS))
                );

        /*
         * Spring WebFlux 에서는 애플리케이션 메모리 문제를 피하기 위해 in-memory buffer 값이 256KB로 기본설정 되어 있음.
         * 따라서 256KB 보다 큰 HTTP 메시지를 처리 시도 → DataBufferLimitException 에러 발생
         * in-memory buffer 값을 늘려주기 위한 설정 추가
         */
        return WebClient.builder()
                .baseUrl(responseServerUrl)
                .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(maxMemorySize))
                .clientConnector(new ReactorClientHttpConnector(httpClient))
                .defaultHeaders(httpHeaders -> {
                    httpHeaders.add(CommonConst.AUTHORIZATION, CommonConst.BEARER);
                    httpHeaders.add(CommonConst.CONTENT_TYPE_KEY, MediaType.APPLICATION_JSON_VALUE);
                })
                .build();
    }
}

3.3.4 실제 사용 코드

    /**
     * WebClient를 이용한 재시도 처리 <br/>
     *
     * @see <a href=https://www.baeldung.com/spring-webflux-retry>WebClient Retry</a>
     */
    public CommonResponse sendRetry(final CommonRequest request) {
        log.info("request[{}]", request);

        return webClient.post()
                .uri(retryRequestPath)
                .body(Mono.just(request), CommonRequest.class)
                .retrieve()
                .bodyToMono(CommonResponse.class)
                .retryWhen(Retry.fixedDelay(retryMaxAttempt, Duration.ofSeconds(retryDelay))) // 1초로 고정된 간격으로 재시도
//                .retryWhen(Retry.max(3))                    // 매우 짧은 간격으로 재시도 - 복구할 기회를 주지 않아 위험함.
//                .retryWhen(Retry.backoff(3, Duration.ofSeconds(2)))                // 2초. 4초. 8초의 백오프 전략으로 제시도
                .block();
    }

    /**
     * Response Server로부터 받은 HTTP 상태코드별로 에러 처리
     */
    public CommonResponse get500status(final CommonRequest request) {
        log.info("request[{}]", request);

        return webClient.post()
                .uri(retryRequestPath)
                .body(Mono.just(request), CommonRequest.class)
                .retrieve()
                .onStatus(HttpStatusCode::is5xxServerError, response -> Mono.error(new WebClientResponseException(
                                response.statusCode().value(),
                                String.format("5xx error. %s", response.bodyToMono(String.class)),
                                response.headers().asHttpHeaders(), null, null
                        )
                ))
                .bodyToMono(CommonResponse.class)
                .block();
    }

5xx 에러 핸들링 시 남는 로그 예시


3.3.5 WebClient 단점

  • WebFlux에 대한 학습이 필요하다.
    • retrieve() vs exchangeToXXX
    • Mono, Flux
  • 로그를 DEBUG 레벨로 남길 수는 있지만, 가독성이 떨어진다.


4. RestClient

4.1 RestClient 소개 및 특징

  • Spring Boot 3.2에 도입
  • RestTemplate과 WebClient의 문제점을 보완
    • RestTemplate의 모든 HTTP 기능을 노출하는 문제점
    • fluent API를 지원하는 리액티브 WebClient의 spring-webflux 의존성을 추가해야만 한다는 아쉬운 점.
    • 위 두개를 대안으로 삶기 위해 출시된 RestClient
  • RestTemplate과 동일한 동기식 HTTP 호출 기능을 가지되, fluent한 API를 제공

4.2 RestClient 활용 코드

4.2.1 의존성 추가

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
<dependencies>

4.2.2 로그 레벨 수정

RestClient는 DEBUG 레벨로 API 통신 정보를 보여준다.

logging:
  level:
    web: debug

4.2.3 재시도(ReTry) 구현

RestTemplate와 동일하게 Retry(재시도)를 지원하지않아, Spring-Retry를 이용해야 함

  1. 의존성 추가
<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
    <version>2.0.5</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
  1. 애플리케이션 메인 클래스에 애너테이션 추가
@EnableRetry
@SpringBootApplication
public class RequestServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(RequestServerApplication.class, args);
    }
}
  1. Retry를 수행할 메서드에 애너테이션 추가
@Retryable(maxAttempts = 3)
public CommonResponse sendRetry(final CommonRequest request) { ... }

4.2.4 설정 클래스 작성

  • 타임아웃 설정(RestTemplate와 동일)
  • 공통 헤더/Url 설정(WebClient와 동일)
@Configuration
public class RestClientConfig {
    @Value("${url.server.response.endpoint}")
    private String responseServerUrl;
    @Value("${restTemplate.connectTimeOut}")
    private Long connectTimeOut;
    @Value("${restTemplate.readTimeOut}")
    private Long readTimeOut;

    @Bean
    public RestClient restClient() {
        // 아래와 같이 Timeout 설정하는 부분은 RestTemplate 와 동일
        final ClientHttpRequestFactory requestFactory = ClientHttpRequestFactories.get(ClientHttpRequestFactorySettings.DEFAULTS
                .withReadTimeout(Duration.ofSeconds(readTimeOut))
                .withConnectTimeout(Duration.ofSeconds(connectTimeOut)));

        // 기본적인 공통 헤더/Url 설정 방식은 WebClient 와 동일
        return RestClient.builder()
                .baseUrl(responseServerUrl)
                .defaultHeaders(httpHeaders -> {
                    httpHeaders.add(CommonConst.AUTHORIZATION, CommonConst.BEARER);
                    httpHeaders.add(CommonConst.CONTENT_TYPE_KEY, MediaType.APPLICATION_JSON_VALUE);
                })
                .requestFactory(requestFactory)
                .build();
    }
}

4.2.5 실제 사용 코드

    /**
     * RestClient를 이용한 재시도 처리 <br/>
     * RestTemplate과 마찬가지로 Retry를 지원하지 않아, Spring-Retry를 이용해야 함. <br/>
     * 재시도 전략은 백오프 전략
     */
    @Retryable(maxAttempts = 3, backoff = @Backoff(delay = 1000L))
    public CommonResponse sendRetry(final CommonRequest request) {
        log.info("request[{}]", request);

        return restClient.post()
                .uri(retryRequestPath)
                .body(request)
                .retrieve()
                .body(CommonResponse.class);
    }

    /**
     * Response Server로부터 받은 HTTP 상태코드별로 에러 처리
     */
    public CommonResponse get500status(final CommonRequest request) {
        log.info("request[{}]", request);

        return restClient.post()
                .uri(retryRequestPath)
                .body(request)
                .retrieve()
                .onStatus(HttpStatusCode::is4xxClientError, (httpRequest, httpResponse) -> {
                    log.error("errorStatus[{}], errorText[{}]", httpResponse.getStatusCode(), httpResponse.getStatusText());
                    throw new RuntimeException();
                })
                .onStatus(HttpStatusCode::is5xxServerError, (httpRequest, httpResponse) -> {
                    log.error("errorStatus[{}], errorText[{}]", httpResponse.getStatusCode(), httpResponse.getStatusText());
                    throw new RuntimeException();
                })
                .body(CommonResponse.class);
    }

4.2.6 RestClient 단점

  • Spring Boot 3.2 이상을 요구한다.
  • 로그를 DEBUG 레벨로 남길 수는 있지만, 정보가 자세하지 않다.


5. HttpInterface

5.1 HttpInterface 소개 및 특징

  • Spring 6에 새롭게 추가된 선언형 Http Client
  • 내부 동작은 WebClient를 이용하므로, 기본적으로는 spring-webflux 의존성을 필요로 한다.
    • 다만, Spring Boot 3.2 이상에서는 RestClient를 이용하므로, webflux 의존성을 사용하지 않아도 된다.

5.2 HttpInterface 활용 코드

5.2.1 의존성 추가

Spring Boot 3.2 이하 버전일 경우에만 추가

<dependency>
    <groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

5.2.2 로그 설정

logging:
  level:
    web: debug       # Spring Boot 3.2 이후(RestClient 사용시에만)
    reactor: debug   # Spring Boot 3.1 이하(WebClient 사용시에만)

5.2.3 HttpInterface 정의

  • OpenFeign과 달리, @PostExchange() 의 value 속성에 환경변수를 지정할 수 없어, 하드코딩이 요구되는 단점이 있다.
public interface ExampleHttpInterface {

    @PostExchange(contentType = MediaType.APPLICATION_JSON_VALUE, accept = MediaType.APPLICATION_JSON_VALUE)
    CommonResponse send(@RequestBody CommonRequest request);

    @PostExchange(value = "/retry", contentType = MediaType.APPLICATION_JSON_VALUE, accept = MediaType.APPLICATION_JSON_VALUE)
    CommonResponse sendRetry(@RequestBody CommonRequest request);
}

5.2.4 HttpInterface Builder 작성 (커스텀)

  • HttpInterface를 이용하기 위해선, RestClient(혹은 WebClient)를 이용해 만드는 코드가 매번 필요하다.
  • 따라서 이를 유틸성 클래스로 생성
@UtilityClass
public class HttpInterfaceBuilder {

    public <T> T builder(final RestClient restClient, Class<T> httpInterfaceType) {
        final RestClientAdapter adapter = RestClientAdapter.create(restClient);
        final HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();

        return factory.createClient(httpInterfaceType);
    }
}

5.2.6 실제 사용 코드

    /**
     * HttpInterface를 이용한 재시도 처리 <br/>
     * Retry를 지원하지 않아, Spring-Retry를 이용해야 함. <br/>
     */
    @Retryable(maxAttempts = 3, backoff = @Backoff(delay = 1000L))
    public CommonResponse sendRetry(final CommonRequest request) {
        log.info("request[{}]", request);

        return HttpInterfaceBuilder
                .builder(restClient, ExampleHttpInterface.class)
                .sendRetry(request);
    }

    /**
     * Response Server로부터 받은 HTTP 상태코드별로 에러 처리
     */
    public CommonResponse get500status(final CommonRequest request) {
        log.info("request[{}]", request);

        final RestClient reBuildRestClient = restClient.mutate()
                .defaultStatusHandler(HttpStatusCode::isError, (httpRequest, httpResponse) -> {
                    log.error("errorStatus[{}], errorText[{}]", httpResponse.getStatusCode(), httpResponse.getStatusText());
                    throw new RuntimeException();
                })
                .build();

        return HttpInterfaceBuilder
                .builder(reBuildRestClient, ExampleHttpInterface.class)
                .get500Error(request);
    }

5.2.7 HttpInterface 단점

  • HttpInterface 정의 시, 환경 변수를 읽어올 수 없어 Url 값에 하드코딩이 요구된다.
  • RestClient/WebClient 를 이용하다보니 따라오는 단점이 있다.
    • RestClient/WebClient를 이용해 HttpInterface 구현체를 생성하는 코드가 매번 요구된다.
    • RestClient/WebClient와 마찬가지로 로그가 자세히 남지 않는다.
    • RestClient를 이용할 경우 Spring Boot 3.2 이상을 요구한다.
    • WebClient를 이용할 경우 spring-webflux 의존성을 추가해야 한다.

6. 개인적인 의견

Spring 6, Spring Boot 3.1에 들어서면서 많은 HTTP 도구들이 나왔고, 이제는 선택지가 많은 것 같다.

  • OpenFeign
  • RestTemplate
  • WebClient
  • RestClient
  • HttpInterface

개인적으로는 위에서 설명한 다섯가지 선택지 중에서는 OpenFegin이나 HttpInteface이 좋아 보인다.

  1. 애너테이션 기반의 선언형 코드 작성 방식
  2. 인터페이스를 사용하므로, 유연하게 변화에 대응할 수 있다.

이 두가지 도구 중에서 고르라면 OpenFeign이 가장 좋은 것 같다.

  1. HttpInterface는 Spring Boot 3.2 아래 버전을 사용할 경우에는 webflux 의존성을 필요로 한다.
  2. 5.2.3에서 언급했듯이, HttpInterface 정의 시에 환경 변수를 사용할 수 없다.

7. 참고자료

profile
백엔드 개발자로 등 따숩고 배 부르게 되는 그 날까지

0개의 댓글