ClientResponse에 대해서 알아보자
ClientResponse (Spring Framework 6.2.5 API)
bodyToFlux reactor.core.publisher.Flux bodyToFlux(Class elementClass) Extract the body to a Flux. Type Parameters: T - the element type Parameters: elementClass - the class of elements in the Flux Returns: a flux containing the body of the given type
docs.spring.io
Represents an HTTP response, as returned by WebClient and also ExchangeFunction.
Provides access to the response status and headers, and also methods to consume the response body.
WebClient와 ExchangeFunction을 리턴하며 HTTP response를 구현한다.
http header와 http status에 접근을 제공하고 http body을 처리하는 메소드도 제공한다.
bodyToMono
<T> reactor.core.publisher.Mono<T> bodyToMono(ParameterizedTypeReference<T> elementTypeRef)
Extract the body to a Mono.
Type Parameters:T - the element type
Parameters:elementTypeRef - the type reference of element in the Mono
Returns:
a mono containing the body of the given type T
Http body를 Mono로 추출한다
그렇다면 Mono는 무엇인가
위 포스팅을 번역했다.
어플리케이션에 확장성과 응답성을 요구함에 따라, 반응형 프로그래밍은 자주 사용되는 패러다임이 되었습니다.
스프링 부트는 Project Reactor와 결합되어 non-blocking 비동기 응용 프로그램을 생성하는 강력한 도구를 제공합니다.
이 접근 방식의 핵심에는 Mono 클래스가 있습니다.
이 글에서는 Mono가 무엇인지, 스프링 부트에 어떻게 통합되는지, 그리고 반응형 프로그래밍을 위한 실용적인 사용 사례를 탐구할 것입니다.
Reactive 프로그래밍에서 모노는 무엇인가?
Mono는 Project Reator(자바에서 reactive 앱을 만들기 위한 라이브러리)의 일부입니다. 이는 비동기적으로 단 한 개의 요소(또는 요소가 아예 없는) 것을 나타냅니다.
모노의 핵심 요소
- Single Value or Empty
: 1개의 요소를 산출하거나 아무것도 산출하지 않고 완료됩니다. - Non-Blocking
: 더 나은 자원 활용을 위해 스레드를 방해하지 않습니다. - Chaining Operations
: 데이터를 가공하거나 변형시키기 위해 map, flatmap, filter같은 연산자를 제공합니다.
모노를 Spring Boot에서 쓰는 이유
스프링부트의 WebFlux는 Reator에 기반한 모듈입니다. 이는 개발자들에게 완전히 reative한 앱의 개발을 가능하게 해줍니다.
- 비동기식 Workloads 처리
무거운 I/O 요청( DB 요청이나 API request 같은) 연산을 대응하기에 이상적입니다. - 확장성 향상
: 스레드 블로킹을 피함으로서 당신의 앱플리케이션은 request 경합을 대응할 수 있게 됩니다. - 마이크로 서비스 아키텍처에 적용가능
: 서버간 통신에 효율적입니다
Spring boot mono 예제
1. 의존성 추가
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
2. Mono 리턴하는 Rest api 예제
@RestController
public class MonoController {
@GetMapping("/hello")
public Mono<String> sayHello() {
return Mono.just("Hello, Reative World!");
}
}
이 response는 non-blocking 비동기 일 것입니다.
3. 체이닝 연산
@GetMapping("/uppercase")
public Mono<String> uppercase() {
return Mono.just("reactive programming)
.map(String::toUpperCase); // 클래스명::class 형태로 클래스의 멤버 참조가 가능
}
4. R2DBC(reactive relational database connectivity)일 경우 DB 데이터를 reactilvely 조회할 수 있다.
@Autowired
private UserRepository userRepository; // reactive repository interface.
@GetMapping("/user/{id}")
public Mono<User> getUser(@PathVariable Long id) {
return userRepository.findById(id);
}
5. 모노로 에러처리하기
@GetMapping("/error")
public Mono<String> handleError() {
return Mono.error(new RuntimeException("Something went wrong"))
.onErrorReturn("Fallback response");
}
6. Combining Multiple Async Results with zip
Often, you need to combine data from multiple asynchronous sources. Mono.zip is perfect for this.
@GetMapping("/combined")
public Mono<String> getCombinedData() {
Mono<String> user = Mono.just("John Doe");
Mono<Integer> age = Mono.just(30);
return Mono.zip(user, age, (u, a) -> String.format("User: %s, Age: %d", u, a));
// {"result": "User: John Doe, Age: 30"}
}
7. Caching with Mono
You can cache the result of a Mono to optimize resource usage for repeated calls:
Mono<String> cachedMono = Mono.just("Cached Data").cache();
@GetMapping("/cached")
public Mono<String> getCachedData() {
return cachedMono;
}
The first call computes the value, but subsequent calls return the cached result, improving performance.
8. Retrying on Failure
Mono provides built-in retry mechanisms for transient errors:
@GetMapping("/retry")
public Mono<String> retryExample() {
return Mono.error(new RuntimeException("Temporary error"))
.retry(3) // Retry up to 3 times
.onErrorResume(e -> Mono.just("Fallback after retries"));
}
If an error occurs, it retries the operation three times before falling back.
9. Using Mono for Streaming Large Files
Mono can be used to stream files asynchronously:
@GetMapping("/download")
public Mono<Void> downloadFile(ServerHttpResponse response) {
response.getHeaders().setContentType(MediaType.APPLICATION_OCTET_STREAM);
response.getHeaders().setContentDisposition(ContentDisposition.attachment().filename("example.txt").build());
return Mono.fromRunnable(() -> {
try (OutputStream os = response.getBody()) {
os.write("This is an example file.".getBytes());
} catch (IOException e) {
throw new RuntimeException("Error writing file", e);
}
});
}
This enables efficient handling of large file downloads without blocking threads.
10. Handling WebClient Responses
HTTP 요청을 처리하기 위해 WebClient와 함께 모노를 사용할 수 있다.
@Autowired
private WebClient.Builder webClientBuilder;
@GetMapping("/external")
public Mono<String> callExternalService() {
WebClient webClient = webClientBuilder.build();
return webClient.get()
.uri("https://jsonplaceholder.typicode.com/posts/1")
.retrieve()
.bodyToMono(String.class)
}
웹클라이언트는 외부 API로부터 데이터를 조회한다. 그리고 Mono를 리턴한다.
11. Validate Data Using Mono
You can validate incoming data reactively before proceeding:
PostMapping("/validate")
public Mono<String> validateInput(@RequestBody Mono<String> input) {
return input.filter(data -> data.length() > 5)
.switchIfEmpty(Mono.error(new IllegalArgumentException("Input too short")))
.map(data -> "Valid Input: " + data);
}
This endpoint checks if the input is valid and processes it accordingly.
Mono VS Flux
Mono는 단일 값 스트림을 나타내는 반면, Flux는 0개 이상의 요소 스트림을 나타냅니다.
단일 결과가 예상되는 경우(예: ID로 사용자 가져오기) Mono를 사용하고,
여러 결과가 예상되는 경우(예: 모든 사용자 나열) Flux를 사용합니다.
Spring Boot의 WebFlux 프레임워크에서 Mono 클래스는 반응형 논블로킹 애플리케이션을 구축하는 데 강력한 도구입니다. 간단한 REST 엔드포인트를 처리하든 반응형 데이터베이스와 통합하든 상관없이 Mono는 효율성과 확장성을 보장합니다. 모범 사례를 채택하고 풍부한 API를 활용함으로써 최신 요구 사항을 충족하는 견고하고 반응형 애플리케이션을 구축할 수 있습니다.
반응형 프로그래밍에는 학습 곡선이 있을 수 있지만, 일단 숙달되면 고성능 소프트웨어 개발의 판도를 바꿀 수 있습니다.
Mono가 Spring에서 논-블로킹 비동기를 구현하기 위해 사용하는 클래스인 것 같다.
근데 그럼 왜 http body에 .bodyToMono가 붙는 것인가
http body는 서버가 아닌데ㅠ?
이에 대해서 또 알아보자...
Mono는 무엇인가 (2)
Mono<T>는 "비동기적으로 타입 T모양의 값을 받게 될 것이라는 약속"이다.
쉽게 말하면 "내가 너한테 User 타입 줄게... 근데 지금말고, 나중에, 데이터가 준비되면."
이건 non-blocking코드에서 유용하다. 결과를 기다리는 대신에 우리는 결국 주어질 result를 handle(==Mono!)할 수 있기 때문이다.
좀 더 쉽게 예를 들어보자.
Mono가 봉인된 택배 박스라고 해보자.
- 이 택배 박스는 결국엔 무언가를 담게 될 것이다 (User 같은 거)
- 하지만 택배 박스를 지금 열어볼 수는 없다. 택배가 도착할 때까지 기다려야 한다.
- 택배 박스가 도착한 다음에 무엇을 할지를 미리 정할 수 있다. (map, flatMap, etc...)
그래서 bodyToMono가 뭘까? 이런 코드가 있다고 해보자
val userMono: Mono<User> = webClient
.get()
.uri("/user/1")
.retrieve()
.bodyToMono(User::class.java)
이 코드가 하는 일은 아래와 같다.
- 비동기 HTTP Get 요청을 보낸다.
- response body(byte겠죠)는 미래에 결국엔 도착할 것이다.
- bodyToMono(User::class.java)는 스프링한테 이렇게 지시한다...
- body가 도착하면, 이걸 User 객체로 전환해줘.
- 그리고 Mono<User>로 돌려줘.
즉 bodyToMono()는...
내가 받게 될 http body를 Mono안의 객체로 만들달라. so you cna get it later!!
는 것이다.
미래의 언젠가에 값을 받았을 때 처리를 하기 위해서는, Mono의 기능이 필요하다! 그래서 Mono안에(Wrapper), User같은 객체를 넣어서 돌려달라고 하는 것이다.
왜 그렇게 비동기에 집착하는건가...
왜 그렇게까지 비동기 비동기하는 것일까
비동기 등장 전의 웹의 세계는 도대체 어땠길래..
- 예시1)
Ajax같은 비동기 기술이 나오기 전까지 웹 페이지에서는 실시간 데이터를 표현하는 것이 매우 어려웠다.
예를 들어 실시간으로 방문자 수를 보여줄 수가 없다. 이를 위해서는 반드시 새로고침을 통해 데이터를 다시 받아와야 했다.
비동기 기술이 등장한 이후로는 이야기가 달라진다. 백그라운드에서 데이터 fetch를 수행하고 이 데이터를 토대로 페이지의 정보를 갱신해줄 수 있기 때문.
사용자와 웹사이트 모두 새로고침을 하지 않아도 실시간으로 데이터를 전달받고 표현이 가능해졌다.
웹사이트에 댓글을 작성하면 새로고침을 하지 않아도 내가 작성한 댓글이 바로 표시된다. 비동기 통신을 통해 덧글 작성 API를 보내고 데이터를 갱신하여 표현해주기 때문.
- 예시2)
아주 예전에는 데이터가 변경된거 보여주려면 페이지 전체를 새로고침하는 방식으로 개발했어요. 근데 페이지가 깜빡거리는것도 별로이고 바뀌길 원하는 부분만 바뀌도록 하는게 대세가 되면서 ajax를 이용하기 시작했어요.
AJAX라는 개념이 등장하기 전에는 플러그인을 사용하지 않는 이상 새로고침을 해서 서버에서 다시 페이지를 렌더링하고 이를 받아오는 방법밖에 없었으나(플래시의 시대죠)
- 예시3)
하지만 새로고침 되면 현재 페이지에 대한 정보들이 초기화 돼버리는 문제가 있고 그걸 다시 세팅해 주는 것도 손이 가는 작업이므로 새로고침을 막고 비동기 처리 시키는 것이죠
사용자 입장에서도 페이지가 한번 새로고침 된 후 다시 로드되는 것보다는 같은 화면을 쭉 보는게 더 나을테고요
https://okky.kr/questions/1470374
그럼 비동기는 무조건 나쁜걸까?
RestTemplate(blocking) VS WebClient(non-blocking)
이전 직장에서 RestTemplate을 썼으나 속도에 큰 문제가 없었다.
val response = restTemplate.getForObject("https://api.example.com/data", String::class.java)
이 코드는 응답이 돌아올 때까지 스레드가 멈춰있을 것이다. 만약 그 서비스가 충분한 스레드(스프링 부트는 보통 스레드 풀을 사용함)를 가지고 있다면 빠르게 느껴졌을 것이다.
하지만 만약에 천명의 유저가 같은 endpoint에 같은 시간에 요청을 보냈다면, 그 유저들은 각각 1초씩을 응답에 대기해야하고, 1000개의 스레드가 1초동안 아무것도 안하고 있는 꼴이 된다.
이건 속도저하나 crash를 유발할 수 있다.
즉, 당신의 서비스가
- request 경합이 치열하지 않고
- 외부 API의 응답속도가 빠르고
- 서버가 충분한 스레드를 가지고 있고
- CPU에 얽매이지 않았다면
RestTemplate도 충분히 빠르게 느껴졌을 것이다.
하지만 만약 당신의 서비스가
- 외부의 느린 API를 한 번의 요청에 5개씩 요청한다거나
- 1000명 이상의 유저 경합이 발생한 트래픽을 감당해야한다거나
- 서버리스나 저메모리 같은 제한된 상황이라거나
- 스레드를 낭비하지 않고 스케일을 효율적으로 관리해야하는 상황이라면
동기 기능은 당신에게 문제가 될 것이다.
이로서 ClientResponse에서 시작하여
Mono를 거치고
비동기까지 알아본 뒤의 포스팅을 마친다...
'【 개발 이야기 】' 카테고리의 다른 글
spring이 아닌 일반 kotlin 프로젝트에 직접 gradle 추가하기 (feat... kotlin fun 함수 실행버튼 없음) (1) | 2025.04.11 |
---|---|
[kotlin] data, enum, object 클래스의 각 용도와 강점 (0) | 2025.04.07 |
14자, 20자 날짜 포맷 (1) | 2025.03.31 |
[Intellij] 외부 라이브러리 직접 추가하기 (2) | 2025.03.28 |
@ModelAttribute와 인터페이스 (0) | 2025.03.26 |