
SpringBoot를 사용해 HTTP API를 호출할 때 선택할 수 있는 방법을 알아보고, 어떤 상황에 어떤 클라이언트를 사용하는 것이 좋을지 정리해보겠다.
HTTP 요청을 하기 위해 간단한 API를 제공하는 템플릿
Build.gradle
implementation 'org.springframework.boot:spring-boot-starter-web'
Service
public BaseResponse<SongResponseDto> getChat(SongRequestDto songRequestDto) {
String url = "http://localhost:8000/chat";
try {
ResponseEntity<SongResponseDto> response = restTemplate.postForEntity(url, songRequestDto, SongResponseDto.class);
if (response.getStatusCode() == HttpStatus.OK) {
return new BaseResponse<>(response.getBody());
} else {
throw new CustomException(BaseResponseStatus.INTERNAL_SERVER_ERROR);
}
} catch (Exception e) {
throw new CustomException(BaseResponseStatus.INTERNAL_CLIENT_ERROR);
}
}
기존 RestTemplate 기능을 제공하되, 비동기 논블로킹 방식을 사용하여 병렬 실행이 가능하게 하는 API CALL 방식
Build.gradle
starter-web 의존성 대신 web flux 의존성을 주입함. 동기 방식인 starter-web과 비동기 방식인 web-flux 동시에 주입하면 의존성 충돌이 발생하기 때문임
implementation 'org.springframework.boot:spring-boot-starter-webflux'
Service
public BaseResponse<SongResponseDto> getChat(SongRequestDto songRequestDto){
SongResponseDto songResponseDto = webClient.post()
.uri("http://locathost:8000/chat")
.bodyValue(songRequestDto)
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError,
clientResponse -> Mono.error(new CustomException(BaseResponseStatus.INTERNAL_CLIENT_ERROR)))
.onStatus(HttpStatusCode::is5xxServerError,
clientResponse -> Mono.error(new CustomException(BaseResponseStatus.INTERNAL_SERVER_ERROR)))
.bodyToMono(SongResponseDto.class)
.block();
return new BaseResponse<>(songResponseDto);
}
RestTemplate + WebClient의 장점을 합친 API CALL 방식
Build.gradle
web flux 관련 의존성을 제거함. RestTemplate을 재사용하여 동기식으로 처리하는게 RestTemplate의 의의이기 때문.
implementation 'org.springframework.boot:spring-boot-starter-web'
RestClientConfig
프록시 패턴을 사용하여 RestClient를 구현함.
프록시 패턴을 사용한 이유
@Configuration
public class RestClientConfig {
private static final int READ_TIMEOUT = 1500;
private static final int CONNECT_TIMEOUT = 3000;
@Bean
public RestTemplate restTemplate() {
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
factory.setHttpClient(createHttpClient());
return new RestTemplate(factory);
}
// HttpClient 생성 메서드
private CloseableHttpClient createHttpClient() {
// 요청 설정에 대한 타임아웃 값 설정
RequestConfig config = RequestConfig.custom()
.setConnectionRequestTimeout(Timeout.ofMilliseconds(CONNECT_TIMEOUT))
.setResponseTimeout(Timeout.ofMilliseconds(READ_TIMEOUT))
.build();
// HttpClient 생성 및 설정
return HttpClients.custom()
.setDefaultRequestConfig(config)
.setConnectionManager(new PoolingHttpClientConnectionManager())
.build();
}
@Bean
public RestClient restClient(RestTemplate restTemplate) {
return RestClient.create(restTemplate);
}
}
Service
public BaseResponse<SongResponseDto> getChat(SongRequestDto songRequestDto) {
String url = "http://localhost:8000/chat";
ResponseEntity<SongResponseDto> response = restClient.post()
.uri(url)
.body(songRequestDto)
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError, clientResponse -> {
throw new CustomException(BaseResponseStatus.INTERNAL_CLIENT_ERROR);
})
.onStatus(HttpStatusCode::is5xxServerError, clientResponse -> {
throw new CustomException(BaseResponseStatus.INTERNAL_SERVER_ERROR);
})
.toEntity(SongResponseDto.class);
return new BaseResponse<>(response.getBody());
}
| 특징 | RestTemplate | WebClient | RestClient |
|---|---|---|---|
| 도입 시기 | Spring 3.0 (구형) | Spring 5.0 (리액티브 지원) | Spring 6.1 (최신) |
| 동기/비동기 | 동기식(블로킹) | 동기/비동기 (비동기 및 논블로킹 지원) | 동기식(블로킹) |
| API 스타일 | 템플릿 기반 | Fluent API (메서드 체이닝) | Fluent API (메서드 체이닝) |
| 리액티브 지원 | 지원하지 않음 | 리액티브 및 논블로킹 지원 | 지원하지 않음 |
| 적합한 상황 | 단순한 동기식 요청 처리 | 복잡한 리액티브 또는 고성능 애플리케이션 | 최신 동기식 HTTP 호출 |
| 설정 재사용 | 재사용 불가 | 완전한 커스터마이징 가능 | RestTemplate 설정을 재사용 가능 |
| 오류 처리 | 기본적인 오류 처리 | 고급 오류 처리 및 재시도 기능 지원 | 고급 오류 처리 및 상태 코드 기반 처리 |