Application
Http client
팅리엔
2024. 9. 24. 10:45
RestTemplate
- spring 3.0 ~
- synchronous, blocking
- 1 thread per 1 request
- thread safe
- 여러 요청을 병렬로 처리하고 싶다면 connection pool을 사용한다. (수신 서버가 Keep-Alive를 지원해야 한다.)
- RestTemplate 생성 시 어떤 HttpClient, ClientHttpRequestFactory를 사용할 것인지 선택할 수 있다. (default: SimpleClientHttpRequestFactory, 그 외 Apache HttpComponents, Netty, OkHttp)
- https://docs.spring.io/spring-framework/reference/integration/rest-clients.html
@Bean
public RestTemplate restTemplate() {
HttpClient httpClient = HttpClientBuilder.create()
.setMaxConnTotal(100) // 최대 오픈되는 커넥션 수
.setMaxConnPerRoute(5) // IP,포트 1쌍에 대해 수행 할 연결 수
.build();
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
factory.setConnectTimeout(5000); // 연결시간초과, ms
factory.setReadTimeout(5000); // 읽기시간초과, ms
factory.setHttpClient(httpClient);
return new RestTemplate(factory);
}
restTemplate.getInterceptors().add(addHeaderFunction);
Todo todo = restTemplate.getForEntity("<https://jsonplaceholder.typicode.com/todos/1>", Todo.class);
- RestTemplate은 ClientHttpRequestFactory 구현체를 통해 요청을 처리한다. 구현체 중HttpComponentsClientHttpRequestFactory을 사용하면 커넥션 pool 설정을 할 수 있다. (connectTimeout, socketTimeout, connectionRequestTimeout)
WebClient
- spring 5.0 ~
- part of spring-webflux
- asynchronus, non-blocking
- event loop in single thread (별도의 워커 스레드)
- https://docs.spring.io/spring-framework/reference/web/webflux-webclient.html
@Bean
public WebClient webClient() {
return WebClient.builder().build();
}
webClient.mutate()
.defaultHeaders(addHeaderFunction)
.build();
Mono todoMono = webClient
.post()
.uri("<https://jsonplaceholder.typicode.com/todos/1>")
.bodyValue(todoRequest)
.retrieve()
.bodyToMono(Todo.class);
todoMono
.subscribe(
todo -> {
System.out.println(todo.getTitle());
},
error -> {
System.out.println(error.getMessage());
}
);
장점
- 논블로킹 방식으로 여러 동시 요청을 효율적으로 처리한다.
- 선언적 스타일(메서드 체인)로 코드를 작성할 수 있다.
단점
- spring mvc에서 API 호출을 위해 webflux 의존성을 모두 추가해줘야 한다.
- 반환값으로 Flux, Mono를 반환한다. 이를 다루기 위해 subscribe나 map을 사용해야 한다. 이런 선언적 스타일은 RestTemplate과는 달라 이질감이 든다. (block을 통해 RestTemplate처럼 원하는 결과 객체를 받을 수 있지만 이는 블로킹 방식이므로 안티 패턴이다. 하지만 Spring MVC를 쓰며 block은 불가피하다.)
RestClient
- spring 6.1 ~
- 스프링팀은 Spring MVC 프로젝트의 HTTP 클라이언트로 RestClient를 권장한다.
- https://docs.spring.io/spring-framework/reference/integration/rest-clients.html
@Bean
public RestClient restClient() {
return RestClient.builder().build();
}
restClient.mutate()
.defaultHeaders(addHeaderFunction)
.build();
Todo todo = restClient
.post()
.uri("<https://jsonplaceholder.typicode.com/todos/1>")
.body(todoRequest)
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError, (request, response) -> {
throw new MyCustomRuntimeException(response.getStatusCode(), response.getHeaders());
})
.body(Todo.class);
스레드풀 사용하기
implementation group: 'org.apache.httpcomponents.client5', name: 'httpclient5', version: '5.3'
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.stream.Collectors;
@Slf4j
@Component
public class LoggingHttpRequestInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
logRequest(request, body);
ClientHttpResponse response = execution.execute(request, body);
logResponse(response);
return response;
}
private void logRequest(HttpRequest request, byte[] body) {
log.info("===== HTTP Request =====");
log.info("URI : {}", request.getURI());
log.info("Method : {}", request.getMethod());
log.info("Headers : {}", request.getHeaders());
log.info("Request body: {}", new String(body, StandardCharsets.UTF_8));
}
private void logResponse(ClientHttpResponse response) throws IOException {
StringBuilder inputStringBuilder = new StringBuilder();
try (BufferedReader bufferedReader = new BufferedReader(
new InputStreamReader(response.getBody(), StandardCharsets.UTF_8))) {
String line = bufferedReader.lines().collect(Collectors.joining("\n"));
inputStringBuilder.append(line);
}
log.info("===== HTTP Response =====");
log.info("Status code : {}", response.getStatusCode());
log.info("Status text : {}", response.getStatusText());
log.info("Headers : {}", response.getHeaders());
log.info("Response body: {}", inputStringBuilder);
}
}
import lombok.extern.slf4j.Slf4j;
import org.apache.hc.client5.http.classic.HttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.core5.util.TimeValue;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.client.BufferingClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestClient;
@Slf4j
@Configuration
public class PostRestClientConfig {
@Value("${post.baseurl}")
private String baseUrl;
// requestFactory 미설정 시 SimpleClientHttpRequestFactory가 사용된다.
// 커넥션풀 사용을 위해 HttpComponentsClientHttpRequestFactory를 사용한다.
@Bean
RestClient postRestClient(ClientHttpRequestFactory postRequestFactory,
LoggingHttpRequestInterceptor loggingHttpRequestInterceptor) {
return RestClient.builder()
.baseUrl(baseUrl)
.requestFactory(postRequestFactory)
.requestInterceptor(loggingHttpRequestInterceptor)
.defaultHeaders(headers -> {
headers.add(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
})
// .messageConverters(converters -> converters.add(new MyCustomMessageConverter()))
// .defaultUriVariables(Map.of("variable", "foo"))
// .requestInitializer(myCustomInitializer)
.build();
}
// HttpComponentsClientHttpRequestFactory requires Apache HttpComponents 5.1 or higher, as of Spring 6.0
// gradle 추가 [implementation group: 'org.apache.httpcomponents.client5', name: 'httpclient5', version: '5.3']
@Bean
BufferingClientHttpRequestFactory postRequestFactory() {
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
connectionManager.setMaxTotal(10); // 최대 커넥션 수
HttpClient httpClient = HttpClients.custom()
.setConnectionManager(connectionManager)
.evictIdleConnections(TimeValue.ofMilliseconds(60_000)) // 해당 시간동안 사용되지 않은 커넥션 정리
.build();
HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
clientHttpRequestFactory.setConnectTimeout(5_000); // 커넥션 최대 시간
clientHttpRequestFactory.setConnectionRequestTimeout(5_000); // 커넥션풀에서 사용 가능한 연결을 가져오기 위해 대기하는 최대 시간
return new BufferingClientHttpRequestFactory(clientHttpRequestFactory); // 요청/응답 내용을 여러 번 읽을 수 있도록 설정
}
}
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;
@Component
@RequiredArgsConstructor
public class PostClient {
@Qualifier("postRestClient")
private final RestClient restClient;
public void getPost(Long id) {
Post post = restClient
.get()
.uri("/posts/" + id)
.retrieve()
.body(Post.class);
System.out.println(post);
}
}
HttpInterface
- spring 6.0 ~
- Client를 추상화했다. Feign과 비슷한데 Feign은 FeignClient를 사용하고, HttpClient는 특정 구현체에 종속되지 않는다.
- @HttpExchange 어노테이션이 적용된 메서드를 가진 인터페이스의 구현체를 자동으로 만들어준다. (마치 JpaRepository처럼)
interface TodoApi {
@GetExchange("/todos/{id}")
Todo getTodoById(@PathVariable Long id);
}
@Bean
RestClientTest.TodoApi todoApi(RestClient.Builder restClientBuilder) {
RestClient restClient = restClientBuilder
.baseUrl("<https://jsonplaceholder.typicode.com>")
.build();
HttpServiceProxyFactory httpServiceProxyFactory = HttpServiceProxyFactory
.builderFor(RestClientAdapter.create(restClient))
.build();
return httpServiceProxyFactory.createClient(RestClientTest.TodoApi.class);
}
@Autowired
TodoApi todoApi;
public void test() {
Todo todo = todoApi.getTodoById(1 L);
}
병렬 실행 사례
- 한 API 진행 중 다른 서버의 API를 여러 개 호출하는 경우가 있다. *[시작 → A 서버 호출 → B 서버 호출 → C 서버 호출 → 끝]*처럼 순차적으로 실행될 필요가 없다면 *[시작 → (A 서버 호출, B 서버 호출, C 서버 호출) → 끝]*처럼 외부 API 호출을 병렬로 실행하면 시간 절약이 될 것이다.
- WebClient
Mono<AResponse> AMono = webClient.get().retrieve().bodyToMono(AResponse.class).subscribeOn(Schedulers.boundedElastic());
Mono<BResponse> BMono = webClient.get().retrieve().bodyToMono(BResponse.class).subscribeOn(Schedulers.boundedElastic());
Mono<CResponse> CMono = webClient.get().retrieve().bodyToMono(CResponse.class).subscribeOn(Schedulers.boundedElastic());
// Mono, Flux는 Future와 다르게 실행 '가능성'을 의미하는 것이다. 아직 실행된 게 아니다!
// subscribeOn은 어느 스레드에서 subscribe 할 것인지 지정하는 메서드이고,
// elastic scheduler는 구독마다 다른 스레드에서 동작되도록 보장한다.
Tuple3<AResponse, BResponse, CResponse> responses = Mono.zip(AMono, BMono, CMono).block();
// block으로 최종적 결과를 동기로 처리하지만 각각의 Mono는 스케쥴러에 의해 비동기적으로 수행된다.
- RestClient
@Test
public void test2() {
RestClient restClient = RestClient.builder().baseUrl("<https://jsonplaceholder.typicode.com/todos/1>").build();
CompletableFuture AFuture = CompletableFuture.supplyAsync(() -> restClient
.get()
.retrieve()
.body(Todo.class));
CompletableFuture BFuture = CompletableFuture.supplyAsync(() -> restClient
.get()
.retrieve()
.body(Todo.class));
// CompletableFuture로 여러 비동기 작업을 조합할 수 있다.
CompletableFuture.allOf(AFuture, BFuture).join();
}
참고
https://incheol-jung.gitbook.io/docs/q-and-a/spring/resttemplate