WebClient는 Spring Boot 서버에서 HTTP 요청을 보낼때 활용하게 되는 인터페이스이며, 본래 사용되어 오던 RestTemplate 을 대체합니다. Spring Reactive의 일부로, 리엑티브 프로그래밍을 하기 위한 모듈이지만, 지금은 일단 단순한 HTTP 요청을 위한 용도로 활용합니다. 후에 리엑티브 프로그래밍에 대한 관심이 생기신다면, 지금 소개드리는 것보다 WebClient를 좀더 잘 활용하실 수 있을 겁니다.
HTTP 다시보기
강의 초기에 HTTP에 대한 내용을 공부한적 있습니다.
OSI 통신 모델에서 Layer 7에 정의된 통신 규약으로서, 어느 특정한 형식의 데이터를 주고받는 것이 HTTP입니다. WebClient는 spring-boot-starter-webflux 에 정의된 인터페이스로서, WebClient 를 구현한 객체는 외부로 HTTP 요청을 보낼 수 있습니다.
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
Java
복사
WebClient 를 사용해보기 위한 의존성입니다. 추가해서 새로운 프로젝트에서 시작해봅시다.
또한 HTTP 응답을 보낼 예시를 위해 여태까지 만들었던 서버 중 하나에 spring-boot-starter-actuator 를 추가하고, 아래의 설정을 application.yml 에 추가합시다.
Actuator 설정
또한 실습중 사용할 요청들이 정의된 Postman Collection 또한 Postman에 Import 해둡시다.
WebClient 구현체 선언하기
가장 기본적인 WebClient를 사용하려면, WebClient 인터페이스에 정의된 create() 함수를 호출하면 됩니다.
WebClient.create();
Java
복사
아니면 WebClient에서 사용할 몇몇 설정들을 정의하기 위해 builder() 함수를 호출하여 WebClient.Builder 를 이용할 수 있습니다. 같은 HOST 에서 PATH 만 변경하여 호출하는 API 등을 위한 Base Url이나, 해당 API에서 필요로 하는 인증정보가 담긴 Header 등을 정의할 수 있습니다.
WebClient.builder()
.baseUrl("http://localhost:8081/actuator")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.build();
Java
복사
좀더 복잡하게 만들게 되면, 요청 / 응답 데이터에서, JSON의 key가 snake_case 일때 camelCase 로 변경하도록 하는 codec 등을 정의할 수도 있습니다.
// Spring Boot에 정의된 ObjectMapper를 가져와서
ObjectMapper newMapper = baseConfig.copy();
// JSON Key 값을 변경하는 (ObjectMapper의) 전략을 선언하고
newMapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
// 데이터를 주고받는데 활용하는 (WebClient의) 전략을 만듭니다.
ExchangeStrategies exchangeStrategies = ExchangeStrategies.builder()
.codecs(configurer ->
configurer.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(newMapper)))
.build();
WebClient.builder()
.baseUrl("https://random-data-api.com")
.exchangeStrategies(exchangeStrategies) // WebClient 정의시에 전달합니다.
.build();
Java
복사
이렇게 만들면, 응답 과정에서 돌아오는 snake_case 의 JSON Key를 camelCase 로 변경할 수 있습니다.
이렇게 정의한 WebClient는 @Configuration 에서 @Bean 으로 등록하여 Spring Application 전반에서 가져와 사용할 수 있습니다.
@Configuration
public class WebClientConfig {
...
@Bean
public WebClient ncpWebClient(){
return WebClient.builder()
.defaultHeader("x-ncp-iam-access-key", accessKey)
.build();
}
...
}
Java
복사
WebClientConfig.java
@Service
public class NcpApiService {
...
public NcpApiService(WebClient ncpWebClient) {
this.ncpWebClient = ncpWebClient;
}
...
}
Java
복사
NcpApiService.java
WebClient 구현체 사용하기
@Configuration 에서 WebClient를 정의했으면, 이제 Service에서 활용해 봅시다.
요청 전 추가 설정
우선 ncpWebClient 를 살펴보면,
return WebClient.builder()
.defaultHeader("x-ncp-iam-access-key", accessKey)
.build();
Java
복사
Naver Cloud Platform에서 요구하는 accessKey 를 Header에 설정해 두었습니다. NCP의 많은 API들은 이 특정 Header들을 모든 API 요청에서 요구합니다.
한편 x-ncp-iam-access-key 외에 두가지 Header는 현재시각을 필요로 하기 때문에, 요청시에 결정되어 추가되어야 합니다. 아래의 예시를 봅시다.
// 현재 시각을 문자열로
String epochString = String.valueOf(System.currentTimeMillis());
String uriBase = "https://geolocation.apigw.ntruss.com";
String uriPath = String.format("/geolocation/v2/geoLocation?ip=%s&ext=t&responseFormatType=json", ip);
// signature를 생성하기 위한 클래스 내부 함수
String ncpSignature = this.makeSignature(
"GET", uriPath, epochString
);
NcpGeolocationDto result = this.ncpWebClient
.get()
.uri(uriBase + uriPath)
.header(ncpHeaderNameTimestamp, epochString)
.header(ncpHeaderNameSignature, ncpSignature)
... // 생략
Java
복사
구현체 WebClient의 함수들을 사용하면 실행 이전에 추가적인 설정을 진행할 수 있습니다.
•
.get() : Method를 정의합니다. get() , post(), put() 등이 존재합니다.
•
.uri() : 요청 경로를 설정합니다. WebClient의 baseUrl 이 정의되어 있으면, 둘이 합쳐집니다.
•
.header() : Request Header를 설정합니다.
Header를 설정할때, .header() 대신 .headers() 함수를 활용할 수도 있습니다.
NcpGeolocationDto result = this.ncpWebClient
.get()
.uri(uriBase + uriPath)
.headers(httpHeaders -> {
httpHeaders.add(ncpHeaderNameTimestamp, epochString);
httpHeaders.add(ncpHeaderNameSignature, ncpSignature);
})
... // 생략
Java
복사
.headers() 함수를 이용하면 일급함수를 사용하듯이 Header를 추가할 수 있습니다.
이번엔 Body 가 필요한 actuator/loggers 요청을 살펴봅시다.
ResponseEntity<?> bodiless = this.actuatorClient
.post()
.uri(uri)
.bodyValue(new ActuatorLoggerDto(logLevel))
... // 생략
Java
복사
.bodyValue() 함수를 사용하면 DTO 객체 등이 JSON 등의 형식으로 변환되어 HTTP 요청의 Body로 등록됩니다.
응답 다루기
앞서 언급하였듯, WebClient는 리엑티브 프로그래밍을 위한 인터페이스이기 때문에 응답을 처리하는 방식도 다양합니다. 다만 구체적인 내용은 Webflux에 대한 공부가 필요하기 때문에, 지금은 가장 간단한 .retrieve() 함수와 .exchangeToMono() 함수를 사용하여 확인하도록 합니다.
ResponseEntity<?> bodiless = this.actuatorClient
.post()
.uri(uri)
.bodyValue(new ActuatorLoggerDto(logLevel))
.retrieve()
.toBodilessEntity()
.block();
Java
복사
retrieve() 함수를 사용하면, 그 시점부터 응답을 어떻게 처리할지에 대한 정의를 진행합니다. 현재 보이는 예시의 경우, Body를 무시하는 .toBodilessEntity() 함수를 이용하여 Body 를 무시합니다. 마지막에 호출되어 응답을 반환하는 .block() 함수를 호출하면, 응답이 돌아올때까지 대기합니다.
참고
.toBodilessEntity().block() 으로 진행하게 되면, 응답이 ResponseEntity<?> 의 형식으로 반환됩니다. HTTP Body는 무시하지만, 그 외의 Status나 Header 등은 확인할 수 있습니다.
if (bodiless != null) {
logger.info(String.valueOf(bodiless.getStatusCode()));
if (!bodiless.getStatusCode().is2xxSuccessful())
throw new ResponseStatusException(bodiless.getStatusCode());
}
Java
복사
다만
HTTP Body는 정의된 Java Class로 데이터를 매핑할 수 있습니다. 아래의 예시를 확인합시다.
CarDto result = randomDataClient
.get()
.uri("/api/vehicle/random_vehicle")
.retrieve()
.bodyToMono(CarDto.class)
.block();
Java
복사
.bodyToMono() 함수를 사용하면, 특정 class의 형태로 Body 를 받을 수 있습니다. 만일 Header, StatusCode 를 전부 확인할 수 있는 ResponseEntity 의 형태로 사용하고 싶다면,
ResponseEntity<CarDto> result = randomDataClient
.get()
.uri("/api/vehicle/random_vehicle")
.retrieve()
.toEntity(CarDto.class)
.block();
Java
복사
와 같이 .toEntity() 를 사용할 수 있습니다.
.retrieve() 와 .bodyToMono() 또는 .toEntity() 대신 .exchangeToMono() 함수를 사용할 수도 있습니다.
NcpGeolocationDto result = this.ncpWebClient
.get()
.uri(uriBase + uriPath)
.headers(httpHeaders -> {
httpHeaders.add(ncpHeaderNameTimestamp, epochString);
httpHeaders.add(ncpHeaderNameSignature, ncpSignature);
})
.exchangeToMono(clientResponse -> {
logger.trace(clientResponse.headers().toString());
return clientResponse.bodyToMono(NcpGeolocationDto.class);
})
.block();
Java
복사
응답 예외 처리
앞서 언급하였듯, 비정상적 응답(4xx, 5xx)에 대해서는 WebClientResponseException 이 먼저 발생합니다. 해당 상황에 대한 처리를 위해서 .onStatus() 함수를 사용할 수 있습니다.
ResponseEntity<?> bodiless = this.actuatorClient
.post()
.uri(uri)
.bodyValue(new ActuatorLoggerDto(logLevel))
.retrieve()
.onStatus(HttpStatus::is4xxClientError, clientResponse -> {
logger.error(clientResponse.statusCode().toString());
return Mono.empty();
})
.onStatus(HttpStatus::is5xxServerError, clientResponse ->
Mono.error(new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR)))
.toBodilessEntity()
.block();
Java
복사
.onStatus() 함수는 HTTP status code를 판별할 수 있는 (참 거짓을 반환하는) 함수와 돌아온 실제 응답을 처리하는 함수를 인자로 받아 처리할 수 있습니다. 오류 응답의 내용을 좀더 상세히 처리하고 싶을때 사용하면 됩니다.
만일 오류 상황(Status Code 기반)을 무시하고 응답 처리를 속행하고 싶다면 내부 함수에서 Mono.empty() 를 반환하면 됩니다. 특정 예외를 발생시키고자 한다면, Mono.error(Throwable) 함수의 결과를 반환하면 됩니다.