
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과 같이 애너테이션 & 인터페이스 기반의 선언형으로 작성.
Netflix에 의해 처음 만들어진 Declarative(선언적) HTTP Client 도구로써, 외부 API 호출을 쉽게 할 수 있도록 도와준다.
여기서 선언적이란 애너테이션 사용을 의미한다.
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>4.1.0</version>
</dependency>
@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);
}
}
실행 결과

타임아웃, 로그 레벨 설정
- 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 로그 레벨 설정 예시

설정 클래스 작성
/**
* @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 {
...
}
요청을 수행하기 전, 인터셉터에서 헤더 정보를 수정할 수도 있다.
@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
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
장점
기능
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
스프링 프레임워크에서 제공하는 RESTful API 통신을 위한 도구이다.
동기식 방식으로 통신한다.동기식 으로 요청을 보내고 응답을 받을 때까지 블로킹된다. (응답을 기다린다.)번외
Spring 공식 문서에서 RestTemplate을 확인하면 아래와 같이 내용이 있다.


| 메서드명 | 설명 |
|---|---|
| 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 |
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependencies>
RestTemplate은 DEBUG 레벨로 API 통신 정보를 보여준다.
logging:
level:
web: debug
<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>
@EnableRetry
@SpringBootApplication
public class RequestServerApplication {
public static void main(String[] args) {
SpringApplication.run(RequestServerApplication.class, args);
}
}
@Retryable(maxAttempts = 3)
public CommonResponse sendRetry(final CommonRequest request) { ... }
- 클래스 내의 객체로 생성하기
RestTemplate restTemplate = new RestTemplate();
- 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();
}
}
@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);
}
}

기본적으로 외부 API를 호출하기 위해 내부적으로 HttpClient를 이용하는 점은 RestTemplate과 동일하다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
logging:
level:
web: debug
reactor: debug
@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();
}
}
/**
* 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 에러 핸들링 시 남는 로그 예시


<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependencies>
RestClient는 DEBUG 레벨로 API 통신 정보를 보여준다.
logging:
level:
web: debug
RestTemplate와 동일하게 Retry(재시도)를 지원하지않아, Spring-Retry를 이용해야 함
<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>
@EnableRetry
@SpringBootApplication
public class RequestServerApplication {
public static void main(String[] args) {
SpringApplication.run(RequestServerApplication.class, args);
}
}
@Retryable(maxAttempts = 3)
public CommonResponse sendRetry(final CommonRequest request) { ... }
@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();
}
}
/**
* 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);
}

Spring Boot 3.2 이하 버전일 경우에만 추가
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
logging:
level:
web: debug # Spring Boot 3.2 이후(RestClient 사용시에만)
reactor: debug # Spring Boot 3.1 이하(WebClient 사용시에만)
@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);
}
@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);
}
}
/**
* 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);
}
Spring 6, Spring Boot 3.1에 들어서면서 많은 HTTP 도구들이 나왔고, 이제는 선택지가 많은 것 같다.
개인적으로는 위에서 설명한 다섯가지 선택지 중에서는 OpenFegin이나 HttpInteface이 좋아 보인다.
이 두가지 도구 중에서 고르라면 OpenFeign이 가장 좋은 것 같다.