Spring WebFlux RequestBody - Raw vs Mono

WebFlux 사용 시 Controller 단에서 RequestBody 를 인자로 받을 때,

다음과 같이 Mono를 받아오도록 작성해야할까?

1
2
3
4
@PostMapping("/mono")
public Mono<SellerOut> createWithMono(@RequestBody Mono<SellerIn> sellerIn) {
return sellerService.createWithMono(sellerIn);
}

아니면 그냥 Raw 객체를 받아오도록 작성해야할까?

1
2
3
4
@PostMapping("/entity")
public Mono<SellerOut> createWithRaw(@RequestBody SellerIn sellerIn) {
return sellerService.createWithRaw(sellerIn);
}

non-blocking 드라이버를 제공해주는 MongoDB를 사용한다고 가정하고 사용성과 성능 관점에서 살펴보자.

사용성

컨트롤러 단에서는 위에서 보는 것처럼 큰 차이가 없다. 서비스가 호출하는 데이터 접근 계층(Data Access Layer)에서 작지만 큰 차이가 발생한다.

ReactiveCrudRepository

스프링 WebFlux 환경에서도 사용하기 쉽게 추상화 된 ReactiveCrudRepository를 통해 쉽게 데이터 저장소에 접근할 수 있다. MongoDB 용으로 특화된 ReactiveMongoRepository를 사용하면 Spring Data 에서 제공해주는 편리한 메서드 이름 조합 방식을 WebFlux에서도 사용할 수 있다. 이는 사용성과 생산성을 높여주는 데 큰 역할을 한다.

그런데 엔티티를 저장할 때 사용되는 save() 메서드는 다음과 같이 Mono가 아니라 Raw 객체를 사용하도록 정의돼 있다.

1
2
3
4
5
6
7
8
9
10
11
12
@NoRepositoryBean
public interface ReactiveCrudRepository<T, ID> extends Repository<T, ID> {

/**
* Saves a given entity. Use the returned instance for further operations as the save operation might have changed the
* entity instance completely.
*
* @param entity must not be {@literal null}.
* @return {@link Mono} emitting the saved entity.
* @throws IllegalArgumentException in case the given {@literal entity} is {@literal null}.
*/
<S extends T> Mono<S> save(S entity); // Mono가 아니라 걍 S

그래서 컨트롤러 단에서 RequestBodyMono로 받아오도록 만들면 save() 호출 전에 block() 같은 것을 호출해서 Mono에서 Raw 객체를 꺼내서 save()에 전달해줘야 한다. 대략 이런 식이 된다.

1
sellerRepository.save(sellerIn.block().toEntity());

쓰기도 안 좋고 보기도 안 좋고 무엇보다 중간에 block()을 호출하므로 Mono를 사용하는 의미가 퇴색된다.

정리하면 결국,

RequestBodyMono로 받아오면 ReactiveCrudRepository랑 궁합이 별로다

ReactiveMongoTemplate

ReactiveMongoTemplate에는 다음과 같이 Mono 타입과 Raw 객체 모두 사용해서 저장할 수 있는 API를 제공해준다. 따라서 저장은 사용성에 큰 차이가 없다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
public <T> Mono<T> save(Mono<? extends T> objectToSave) {
...
}

@Override
public <T> Mono<T> save(Mono<? extends T> objectToSave, String collectionName) {
...
}


public <T> Mono<T> save(T objectToSave) {
...
}

public <T> Mono<T> save(T objectToSave, String collectionName) {
...
}

하지만 ReactiveMongoTemplate를 사용해서 조회할 때 메서드 이름 조합 방식을 사용할 수 없으므로, 아래에 보는 것처럼 Query를 전달해줘야 하므로 아무래도 ReactiveCrudRepository 보다는 사용성이 떨어진다고 할 수 있겠다.

Imgur

RequestBodyMono로 받아오면 ReactiveMongoTemplate를 사용하면 되지만, 조회할 때 ReactiveCrudRepository보다는 불편하다

성능

처음에는 예전 Servlet 관점으로 생각을 해서 RequestBodyMono로 받아온다 하더라도 결국은 이미 Request에서 추출해서 만들어진 Raw 객체를 Mono로 wrapping 해서 컨트롤러에 넣어주는 것일 거라 생각했는데 그렇지는 않은 것 같다.

WebFlux에서는 HttpServletRequest가 아니라 reactive 방식의 ServerHttpRequest를 사용한다. 따라서 Servlet 에서와는 다르게 RequestBodyMono로 받아오면 Raw 객체를 받아 사용하는 것과는 다르게 정말로 reactive한 처리가 가능할 수도 있겠다.

reactive 방식은 처리 속도보다는 자원 사용 관점에서의 효율성을 높이는 방식이므로 응답 속도로 비교하는 것보다는 처리량으로 비교하는 것이 합당할 것 같다. k6(https://k6.io/) 로 가상사용자 100 으로 10초간 3회 돌린 결과는 다음과 같다. 그림 위쪽이 Raw 방식, 아래쪽이 Mono 방식이다. http_reqs 항목으로 비교해보면 두 방식에서 의미있는 큰 차이는 없는 것으로 보인다.

Imgur

Imgur

Imgur

선Raw후Mono? 아니면 선Mono후Raw?

Request에서 먼저 Raw 가 추출된 후에 Mono로 감싸져서 컨트롤러에 전달되는 걸까? 아니면 Request에서부터 계속 Mono(또는 Flux)인 채로 있다가 나중에 Raw가 추출되서 컨트롤러에 전달되는 걸까?

Stack을 뒤져본 결과 분기 지점은 아래와 같음을 확인했다. 하지만 여기에서 위 질문에 대한 답을 바로 얻을 수는 없었다.

Imgur

조금 더 살펴본 결과 아래 그림 단계까지는 netty의 NettyDataBuffer로 존재하다가,

Imgur

아래 그림 단계에서 처음으로 Raw 값이 확인된다.

Imgur

선Raw후Mono 인지 선Mono후Raw 인지는 아쉽게도 명확하게 알아내지 못 했다. 더 시간을 들이면 알아낼 가능성도 있지만 일단은 성능 상 어느 쪽이든 큰 차이가 없어 보이므로 이 정도에서 마무리한다.

마무리

WebFlux 사용 시 Controller 단에서 RequestBody 를 인자로 받을 때,

  • Mono를 받아오도록 작성하면 ReactiveCrudRepository를 사용하기 불편해지고,
    Raw 객체를 받아오도록 작성하면 ReactiveCrudRepository를 편하게 쓸 수 있다.

  • Mono로 받아오든 Raw 객체로 받아오든 아주 특수한 상황이 아니라면 성능 상 둘 중 어느 한 쪽이 훨씬 유리하다고 할 수는 없을 것 같다.


크리에이티브 커먼즈 라이선스HomoEfficio가 작성한 이 저작물은(는) 크리에이티브 커먼즈 저작자표시-비영리-동일조건변경허락 4.0 국제 라이선스에 따라 이용할 수 있습니다.