개발새발
WebClient 본문
우리가 개발하는 어플리케이션들을 크게 2개로 나눠보면 요청자와 제공자라고 할 수 있다
요청자를 consumer 또는 subscriber // 제공자를 producer 또는 provider
요청자가 제공자에게 무언가를 요청할 때 제공자가 공개한 API를 이용하게 된다
요청 시 프로그램에서 우리가 가장 흔하게 사용하는 것이 Http Client이다
Spring WebClient는 웹으로 API를 호출하기 위해 사용되는 Http Client 모듈 중 하나이다
Java에서 가장 많이 사용하는 Http Client는 RestTemplate이다
공통점은 둘다 HttpClient모듈이다,
차이점은 통신방법이 RestTemplate은 Blocking방식이고, WebClient는 Non-Blocking방식이다
Non-blocking방식이 필요한 이유는 네트워킹의 병목현상을 줄이고 성능을 향상시키기 위해서다
WebClient는 요청을 나타내고 전송하게 해주는 빌더 방식의 인터페이스를 사용하며,
외부 API로 요청을 할 때 리액티브 타입의 전송과 수신을 한다 (Mono, Flux)
WebClient 특징
- - 싱글 스레드 방식을 사용
- - Non-Blocking 방식을 사용
- - JSON, XML을 쉽게 응답
WebClient를 사용하는 방법
Dependencies
먼저, Webclient 의존성을 추가해줍니다.
gradle
dependencies {
compile 'org.springframework.boot:spring-boot-starter-webflux'
}
Create
WebClient를 생성하는 방법은 2가지가 있다
아주 단순하게 create()를 하는 방법과, option을 추가할 수 있는 build()를 사용가능 하다
create()
정말 단순하게 WebClient의 디폴트 세팅으로 아래와 같이 생성할 수 있으며, 요청할 uri와 함께 생성할 수도 있다
WebClient.create();
// or
WebClient client = WebClient
.create("http://api.data.go.kr/openapi/tn_pubr_public_electr_whlchairhgh_spdchrgr_api");
build()
Options
- uriBuilderFactory : base url을 커스텀한 UriBuilderFactory
- defaultHeader : 모든 요청에 사용할 헤더
- defaultCookie : 모든 요청에 사용할 쿠키
- defaultRequest : 모든 요청을 커스텀할 Consumer
- filter : 모든 요청에 사용할 클라이언트 필터
- exchangeStrategies : HTTP 메시지 reader & writer 커스터마이징
- clientConnector : HTTP 클라이언트 라이브러리 세팅
WebClient client = WebClient.builder()
.baseUrl("http://localhost:8080")
.defaultCookie("cookieKey", "cookieValue")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultUriVariables(Collections.singletonMap("url", "http://localhost:8080"))
.build();
Request
GET
일반적으로 GET API를 사용하여 리소스 모음 또는 단일 리소스를 가져온다
1. Flux
GET /employees
Request: collection of employees as Flux
@Autowired
WebClient webClient;
public Flux<Employee> findAll() {
return webClient.get()
.uri("/employees")
.retrieve()
.bodyToFlux(Employee.class);
}
2. Mono
GET /employees/{id}
Request: single employee by id as Mono
@Autowired
WebClient webClient;
public Mono<Employee> findById(Integer id) {
return webClient.get()
.uri("/employees/" + id)
.retrieve()
.bodyToMono(Employee.class);
}
POST
계속해서 body를 포함하는 POST의 예시
POST /employees
Request : creates a new employee from request body
Reponse : returns the created employee
@Autowired
WebClient webClient;
public Mono<Employee> create(Employee empl) {
return webClient.post()
.uri("/employees")
.body(Mono.just(empl), Employee.class)
.retrieve()
.bodyToMono(Employee.class);
}
여기서 body는 아래와 같이 정의되어다
body를 보면 Mono.just(empl)이 있으며 empl의 타입이 Employee이라는 클래스 정의
여기서 입력값이 Mono<Employee> 타입이라는 것을 확인할 수 있다
만약, POST를 하고 받을 body가 없다면 Void.class를 입력하면 된다
// Empty body
public Mono<Void> create(Employee empl) {
return webClient.post()
.uri("/employees")
.body(Mono.just(empl), Employee.class)
.retrieve()
.bodyToMono(Void.class);
}
bodyToMono()는 아래 Response에서 확인하실 수 있다
body(Object producer, Class<?> elementClass);
body(Object producer, ParameterizedTypeReference<?> elementTypeRef);
body(BodyInserter<?, ? super ClientHttpRequest> inserter);
요청할 데이터(producer)와 그 타입(elementClass / elementTypeRef)을 명시해주면 됩니다
Response
요청을 한 후에는 응답을 받아 처리
응답을 받을 때에는 아래의 두 가지 메소드 중 적절한 것을 선택해서 사용하면 된다
- retrieve() : body를 받아 디코딩하는 간단한 메소드
- exchange() : ClientResponse를 상태값 그리고 헤더와 함께 가져오는 메소드
exchange()를 통해 세세한 컨트롤이 가능하지만, Response 컨텐츠에 대한 모든 처리를 직접 하면서 메모리 누수 가능성 때문에 retrieve()를 권고
bodyToFlux, bodyToMono 를 위에서 계속해서 봤다
bodyToFlux, bodyToMono 는 가져온 body를 각각 Reactor의 Flux와 Mono 객체로 바꿔준다
Mono 객체는 0-1개의 결과를 처리하는 객체이고, Flux는 0-N개의 결과를 처리하는 객체입니다.
retrieve()
retrieve를 사용한 후의 데이터는 크게 두 가지 형태로 받을 수 있다
✔️ toEntity()
status, headers, body를 포함하는 ResponseEntity 타입으로 받을 수 있습니다.
Mono<ResponseEntity<Person>> entityMono = client.get()
.uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.toEntity(Person.class);
✔️ toMono() , toFlux()
Mono<Person> entityMono = client.get()
.uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(Person.class);
📌 exchangeToXXXX()
Mono<Person> entityMono = client.get()
.uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchangeToMono(response -> {
if (response.statusCode().equals(HttpStatus.OK)) {
return response.bodyToMono(Person.class);
}
else {
return response.createException().flatMap(Mono::error);
}
});
Mono<>, Flux<> 사용
아래에서도 확인할 수 있는데, Mono<> Flux<>는 어떻게 사용할 수 있을까요?
map이나 filter와 같은 메서드를 사용하는 방법이 있으며,
필자는 아래와 같이 사용했습니다.
Mono<Employee> employeeMono = webClient.get(). ...
employeeMono.block() // or blockOptional()
Flux<Employee> employeeFlux = webClient.get(). ...
employeeFlux.subscribe(employee -> { ... });
ErrorHandling
에러 핸들링은 결과 값을 반환받을 때의 상황에 따라 적절히 처리할 수 있다
retrieve()
retrieve는 1xx, 2xx, 3xx, ... StatusCode 별로 아래와 같이 처리할 수 있다
4xx, 5xx 의 에러코드일 때에만 new RuntimeException으로 처리해주도록 제작
Mono<Person> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(HttpStatus::is4xxClientError, response -> ...)
.onStatus(HttpStatus::is5xxServerError, response -> ...)
.bodyToMono(Person.class);
exchangeToXXXX()
exchange를 통해 받은 응답에 대한 statusCode를 이용해 분기처리하여 핸들링할 수 있다
Mono<Object> entityMono = client.get()
.uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchangeToMono(response -> {
if (response.statusCode().equals(HttpStatus.OK)) {
return response.bodyToMono(Person.class);
}
else if (response.statusCode().is4xxClientError()) {
return response.bodyToMono(ErrorContainer.class);
}
else {
return Mono.error(response.createException());
}
});
참고
출처: https://gngsn.tistory.com/154 [ENFJ.dev:티스토리]
출처: https://gngsn.tistory.com/154 [ENFJ.dev:티스토리]
https://my-gaebalsaebal.tistory.com/44(예전에 간략하게 정리한 RestTemplate다)
동기 비동기 방식에 대해서는
https://www.youtube.com/watch?v=oEIoqGd-Sns&t=1s
'CS' 카테고리의 다른 글
OAuth 와 JWT의 흐름 (0) | 2022.10.27 |
---|---|
CI / CD (0) | 2022.10.25 |
동기 비동기 (0) | 2022.09.11 |
네트워크 (0) | 2022.08.03 |
컴퓨터와 프로그래밍의 이해 - 웹 어플리케이션과 웹 서버 (0) | 2022.07.27 |