회사에서 코드를 분석하다가 외부 API 통신 시 사용하고 있는 WebClient 를 발견할 수 있었다. Spring 5 부터 제공하는 기능이였고, 이전에는 RestTemplate 를 사용하고 있었어서 WebClient 에 대해 알아보고 정리해보고자 한다.
1. WebClient 란?
- Spring 5 부터 제공하는 RestTemplate 를 대체하는 웹 클라이언트(HTTP Client).
- 웹으로 API를 호출하기 위해 사용되는 Http Client 모듈 중 하나.
- 기존의 동기(Sync) API 를 제공할 뿐만 아니라, 논블로킹(Non-Blocking) 및 비동기(Async) 방식을 지원해서 효율적인 통신이 가능.
- 요청을 나타내고 전송하게 해주는 빌더(Builder) 방식의 인터페이스를 사용.
- API 요청시에는 리액티브(Reactive) 타입의 요청과 응답을 합니다.(Mono, Flux)
1-1. 특징
- WebClient 의 특징은 다음과 같다.
- Reactor 기반의 Functional, Fulent API.
- 싱글 스레드 방식을 사용.(쓰레드, 동시성 문제들을 다룰 필요 없이 비동기 로직의 선언적 형태로 사용 가능)
- Non-Blocking 방식을 사용.
- 서버 사이드의 Request / Response 를 인코딩, 디코딩하는 코덱들을 지원.
- HTTP/1.1 프로토콜을 통해 작동하는 반으형 비차단 솔루션.
1-2. WebClient VS RestTemplate
- RestTemplate 은 Deprecated 되어 스프링에서도 WebClient 사용을 권장하고 있다.
- 공통점
- 웹 통신을 위한 HttpClient 모듈.
- 차이점
- RestTemplate 은 Blocking 기반의 Synchronous API
- WebClient 는 Non-Blocking 기반의 Asynchronous API
- 아래 그림은 성능을 비교한 그림입니다.(빨강 : WebClient, 초록 : RestTemplate)
- 사용자가 늘어날수록 WebClient 와 RestTemplate 성능차이가 심해진다.(Thread Pool Size를 초과할 경우 Queue에서 대기하는 작업이 늘어나기 때문)
- 동시사용자가 1000명까지는 비슷하고, 서버의 성능이 좋아지면 비슷한 구간이 더 늘어난다.
- 어플리케이션 사용자가 비슷한 구간에 해당한다면 오히려 RestTemplate 사용하는것이 생산성 측면에서 유리할 수 있다.(동기 방식이 코드작성, 이해, 디버깅하기가 쉬움)
1-3. 동작 방식
- WebClient 의 동작 방식을 이해하기 전에 RestTemplate 의 방식도 이해해야 한다.
- RestTemplate
- Thread Pool 은 어플리케이션 구동 시에 미리 만들어둔다.
- Request(요청) 는 먼저 Queue 에 쌓아두고, 가용 가능한 쓰레드(Thread)가 있다면 해당 쓰레드에 할당하여 처리한다.
(즉, 1 Request 당 1 Thread 가 할당)
- 각 쓰레드는 Blocking 방식으로 처리되어 응답이 올 때까지 해당 스레드는 다른 요청에 할당 될 수 없다.
- 요청을 처리할 쓰레드가 있으면 문제가 없지만, 모든 쓰레드를 사용하고 있는 경우 이후의 요청은 Queue 에서 대기하게 된다.
- 대부분의 속도 문제는 네트워크나 DB와의 통신에서 생기는데, 이런 문제가 쓰레드에서 발생하면 가용가능한 쓰레드 수가 줄어들면서, 전체 서비스가 느려지는 현상이 발생한다.
- WebClient
- WebClient 는 Single Thread 와 Non-Blocking 방식을 사용한다.(1 Core 당 1 개의 Thread 를 이용)
- 각 요청은 Event Loop 내에 Job 으로 등록된다.
- Event Loop 는 각 Job 을 제공자(Wokres)에게 요청한 후, 응답을 기다리지 않고 다른 Job 을 처리한다.
2. WebClient 사용해보기
- 간단하게 Spring Boot 프로젝트를 생성합니다.
2-1. Dependency 설정
- WebClient 를 사용하기 위해서는 spring-boot-starter-webflux Dependency만 있으면 사용이 가능합니다.
- Maven
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
- Gradle
dependencies {
compile 'org.springframework.boot:spring-boot-starter-webflux'
}
2-2. API Class 작성
- 간단하게 테스트를 진행하기 위해 Controller 를 생성하고 그 안에 구현해보겠습니다.
- 기능별로 나누어서 작성이 필요하지만, 너무 길어질 수 있어 최대한 간단하게 구현하였습니다.
참고!!!
테스트와 블로그 상에서 불필요 코드가 많아질수 있어 cotroller에 구현하였지만, 보통 공통 모듈로 만들고, service 단계에서 구현하는게 일반적일 거라 생각합니다.
- Response Class
- 만약 공통 객체로 사용할 때는 rsp_msg 도 String 형태로 받고, 이후 알맞게 파싱하여 사용할 수 있습니다.
@Data
public class ApiResponse {
@JsonProperty("rsp_code")
private String rsp_code;
@JsonProperty("rsp_msg")
private List<Todo> rsp_msg;
}
- Todo Class
@Getter
@Setter
@ToString(callSuper = true)
public class Todo {
private Long id;
private String item;
private String date;
private boolean completed;
private String time;
private LocalDateTime writeDate;
}
- Controller
@RestController
@Slf4j
@RequiredArgsConstructor
public class WebclientTestContorller {
private HttpClient httpClient;
private ExchangeStrategies exchangeStrategies;
// (1)
@PostConstruct
public void initSetting(){
// (2)
// HttpClient 타임아웃 설정
httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
.responseTimeout(Duration.ofMillis(5000))
.doOnConnected(conn ->
conn.addHandlerLast(new ReadTimeoutHandler(5000, TimeUnit.MILLISECONDS))
.addHandlerLast(new WriteTimeoutHandler(5000, TimeUnit.MILLISECONDS)));
// (3)
// 메모리 설정
exchangeStrategies = ExchangeStrategies.builder()
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2*1024*1024))
.build();
}
@RequestMapping("/webclient")
public void webclientTest(){
try{
// (4)
// WebClient 통신
ApiResponse resp = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.baseUrl("https://test-jpa-api.herokuapp.com/")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.exchangeStrategies(exchangeStrategies)
.build()
.get()
.uri("api/v1/todo")
// (5)
.exchangeToMono(response -> {
if (response.statusCode().equals(HttpStatus.OK)){
return response.bodyToMono(ApiResponse.class)
.doOnError(e -> {
// 에러 구현
log.error("통신 후 에러 : " + e.getMessage());
throw new RuntimeException();
})
.map(t -> {
log.info(t.toString());
if(!t.getRsp_code().equals("200")){
throw new RuntimeException();
}
return t;
});
} else {
log.error("통신 실패 에러");
return response.createException().flatMap(Mono::error);
}
})
.onErrorResume(throwable -> {
log.error("err : {}", throwable.getMessage());
return Mono.empty();
})
.block();
// Null 체크
if(!ObjectUtils.isEmpty(resp)){
log.info("Result : " + resp.getRsp_msg());
// 이후 필요 로직 수행
}
} catch (Exception e){
log.error("Error : {} ", e.getMessage());
}
}
}
3. 소스 설명
- 간략하게나마 소스에 대해서 설명드리겠습니다.
3-1. @PostConstruct
- 종속성 주입이 완료된 후 실행되어야 하는 메서드(간단하게 의존성 주입 이후 아래 메소드를 실행해준다고 생각하시면 됩니다)
- 좀 더 자세한 설명은 추후 블로그에 포스팅 하도록 하겠습니다.
3-2. HttpClient
- WebClient 는 netty 기반의 httpClient 를 사용합니다.
- HttpClient 를 커스텀하여 사용할 수 있고, 해당 글에서는 TimeOut 설정을 위해 사용하였습니다.
3-3. ExchangeStrategies
- Http 메시지의 Reader & Writer 커스텀을 제공합니다.
- 해당 글에서는 메모리 설정을 위해 사용하였습니다.
3-4. WebClient
- WebClient 를 생성하는 방법은 2가지가 있습니다.
- create()
- 가장 기본적인 WebClient 세팅으로 생성하고, 요청할 uri 와 함께 생성합니다.
WebClient.create();
// or
WebClient.create("http://localhost:8080");
- build()
- WebClient 의 설정을 모두 커스텀마이징 하여 사용할 수 있도록 제공합니다.
- Builder 를 통해 생성 시 다음과 같은 이점이 있습니다.- 모든 호출에 대한 기본 Header / Cookie 설정 가능
- filter 를 통한 Request / Response 처리
- Http 메시지 Reader / Writer 조작(커스텀마이징)
- Http Client Library 설정
- 해당 글에서는 build() 를 통해서 생성하였고, 기본적으로 적용 가능한 옵션들은 아래와 같습니다.
- clientConnector
- HTTP Client 세팅
- 위(3-2)에서 생성한 HttpClient 를 세팅합니다. - baseUrl
- 기본적인 호스트의 URL을 작성합니다. - defualtHeader
- 요청시 사용하고자 하는 헤더.
- 헤더를 생성하여 필요한 정보를 담아 세팅할 수 있습니다. - exchangeStrategies
- HTTP 메시지의 Reader & Writer 를 세팅.
- 위(3-3)에서 생성한 ExchangeStrategies 를 세팅합니다. - defulatCookie
- 요청시 사용하고자 하는 쿠키 - defaultRequest
- 요청 시 커스텀할 Consumer - filter
- 모든 요청에 사용할 Client filter
- get() : get 방식으로 통신하겠다를 의미합니다.(post 라면 post(), delete 라면 delete() ...)
- uri() : baseUrl 뒤에 붙을 uri 를 설정합니다.
3-5. exchangeToMono
- 요청한 값에 대해 응답을 받아 처리를 합니다.
- 응답을 받을 때에도 2 가지의 메소드가 있습니다.
- retrive()
- body 를 받아 디코딩하는 간단한 메소드.
- 응답값을 어떻게 추출할 것인지 명시하는데 사용.
WebClient.builder()
.exchangesStrategies(...)
...
.retrieve()
.toEntity(response.class)
.block();
- exchangeToXXXX()
- 응답값의 Response Status 에 따라 결과를 커스텀하여 사용.
- ClientResponse 를 상태값과 헤더와 함께 가져오는 메소드
- 기존에는 exchange() 였지만, 메모리 누수 가능성으로 인해 Deprecated가 되었고, exchangeToMono/exchangeToFlux 로 대체되었습니다.
- 해당 글에서는 exchnageToMono 를 사용합니다.
.exchangeToMono(response -> {
if (response.statusCode().equals(HttpStatus.OK)){
...
} else {
log.error("통신 실패 에러");
...
return response.createException().flatMap(Mono::error);
}
})
- bodyToMono & bodyToFlux
- 응답값 중 Body 에 대해 Mono, Flux 형식으로 받아 처리.
- 1개의 값을 리턴할 때는 bodyToMono 를 사용하고, 복수의 값을 리턴할 때는 bodyToFlux 를 사용. - doOnError
- 응답 과정에서 오류가 발생하면 해당 영역 실행.
- 통신은 정상적으로 된 것이고, 응답 이후 로직에서 오류가 발생하는 것.
- 해당 영역에서 exception 을 Throw 하면 onErrorResume 이 실행되어 최종적으로 Exception 을 처리. - map
- 응답값 중 필요한 부분에 대해서만 전달하기 위해 응답값을 새롭게 생성.
- 아래 그림은 Mono(좌), Flux(우) 에서 Map 동작 순서이다.
- onErrorResume
- 통신과정에서 발생한 Exception 에 대한 처리 및 doOnError 에서 Throw 된 Exception 최종 처리.
- 통신 이후 400,500 에러에 대해서는 무조건 적으로 해당 영역이 실행되는 것이 아니라, 응답에 대해 실패한 경우에 실행. - block
- WebClient 는 기본적으로 비동기로 동작.
- 동기 방식으로 동작하게 하기 위해서는 block() 메소드를 추가하여 사용.
4. 응답 확인
- 간단하게 호출하여 확인해보면 정상적으로 통신되는 것을 확인할 수 있습니다.
5. Reactive Programming(Mono 와 Flux)
- Reactive Programming(리액티브 프로그래밍)
- 일련의 작업 단계를 기술하는 것이 아니라 데이터가 전달될 파이프라인을 구성.
- 어플리케이션을 논블로킹 프로세스로 동작하기 위해 지원하는 프로그래밍. - Reactor
- 리엑티브 스트림을 구현하는 라이브러리.
- Mono, Flux 두 가지 타입으로 스트림 정의. - Mono
- 0 또는 1 개의 데이터 항목과 에러를 갖는다. - Flux
- 0 또는 1 개 이상의 데이터 항목과 에러를 갖는다.
- 보통 여러 스트림을 하나의 결과로 모아줄 때 Mono 를 사용하고, 각각의 Mono 를 합쳐 여러개의 값을 처리할 때 Flux 를 사용합니다.
참고
- https://gngsn.tistory.com/154
- https://bravenamme.github.io/2021/01/07/web_client/
- https://www.baeldung.com/spring-5-webclient
- https://binux.tistory.com/56
'BackEnd > Spring' 카테고리의 다른 글
[SpringBoot] @Valid 를 이용한 유효성(Validation) 검사(Feat. @RequestBody, @Validated) (0) | 2022.10.29 |
---|---|
[Spring] @Scheduled 어노테이션에서 cron 사용 및 정리 (0) | 2022.08.11 |
[Spring] JPA - 영속성 컨텍스트(Persistence Context) 정리 (0) | 2022.07.25 |
[Spring] JPA 정리 (0) | 2022.07.22 |
[Spring] 의존성 주입(DI) 시 생성자 주입(Constructor Injection)을 사용해야하는 이유 (2) | 2022.07.17 |