[Spring WebFlux] 16. 애너테이션 기반 WebFlux 컨트롤러

y001·2025년 5월 4일

Reactive Programming

목록 보기
24/30
post-thumbnail

이번 글에서는 그 중에서도 애너테이션 기반 WebFlux 컨트롤러에 집중하여, 단순히 Mono를 반환한다고 해서 자동으로 논블로킹이 보장되는 것은 아님을 설명하고, 실제로 논블로킹 구조를 설계하는 방법을 코드 중심으로 다룬다.


1. WebFlux 적용

Spring MVC 스타일에 익숙하다면 WebFlux를 처음 사용할 때 기존 컨트롤러 코드의 리턴 타입만 Mono로 바꾸면 충분하다고 생각하기 쉽다. 실제로 다음과 같은 방식은 매우 흔하게 작성된다.

@RestController
@RequestMapping("/books")
public class BookController {

    @PostMapping
    public Mono<BookResponse> createBook(@RequestBody BookRequest request) {
        Book book = mapper.toEntity(request);               // 동기 변환
        Book saved = service.save(book);                    // 동기 저장
        return Mono.just(mapper.toResponse(saved));         // Mono로 감싸기만 함
    }
}

표면적으로는 Mono<BookResponse>를 반환하고 있으므로 리액티브처럼 보이지만,
실제 처리 과정은 모두 동기적으로 수행되고 있다. DTO 변환, 저장 로직 모두 Blocking 메서드이며, 결과만 Mono로 감싼 구조다.
이런 방식은 리액티브의 이점을 거의 활용하지 못하고, 오히려 헷갈리는 코드가 되기 쉽다.


2. WebFlux 초안 구조: Mono만 사용하는 구조

좀 더 리액티브하게 구성하려고 시도하면 다음과 같은 구조가 된다.

@PostMapping
public Mono<BookResponse> createBook(@RequestBody BookRequest request) {
    Mono<Book> mono = Mono.just(mapper.toEntity(request));
    return mono.map(service::save)
               .map(mapper::toResponse);
}

이전보다 나아 보이지만, 여전히 핵심 로직은 Blocking 메서드(save())로 처리되고 있다.
이 구조의 문제는 Mono가 단순히 감싸는 용도에 그친다는 점이다.
이렇게 되면 내부 로직은 여전히 동기적으로 실행되며, 리액티브 스트림의 논블로킹 특성은 거의 적용되지 않는다.


3. 올바른 리액티브 구조: 흐름 안에서 처리하기

리액티브 구조에서 중요한 것은 처리 흐름 전체가 Mono 또는 Flux 안에서 연결되어 있어야 한다는 점이다.
DTO 변환, 저장, 응답 변환까지 모든 작업이 리액티브 흐름 안에서 수행될 때 비로소 논블로킹 구조가 완성된다.

@PostMapping
public Mono<BookResponse> createBook(@RequestBody Mono<BookRequest> requestMono) {
    return requestMono
            .map(mapper::toEntity)             // DTO → Entity
            .flatMap(service::saveReactive)    // 논블로킹 저장
            .map(mapper::toResponse);          // Entity → Response
}

여기서 중요한 점은 두 가지이다:

  • @RequestBody로 직접 Mono<DTO>를 받는다: HTTP 본문을 비동기적으로 읽기 위해 필요
  • 모든 비즈니스 로직이 map, flatMap 안에서 연결되어야 한다: 외부 I/O 또는 연산이 Blocking이면 안 됨

4. 서비스 계층에서도 논블로킹을 유지해야 한다

리액티브 흐름을 유지하려면 컨트롤러뿐 아니라 서비스 계층도 이에 맞게 구성되어야 한다.
Blocking 저장 로직 대신 Mono를 리턴하는 저장 로직으로 구성해야 한다.

@Service
public class BookService {

    public Mono<Book> saveReactive(Book book) {
        return reactiveRepository.save(book); // 예: R2DBC, Mongo Reactive 등
    }
}

이렇게 구성하면, 데이터 변환 → 저장 → 응답 변환까지 모든 흐름이 비동기적으로 연결된다.
Spring WebFlux가 제공하는 스레드 모델의 효율성과 리소스 활용 효과를 제대로 누릴 수 있다.


5. Controller에서 너무 많은 일을 하지 말자

리액티브 구조라고 해서 모든 처리를 컨트롤러에서 직접 수행하는 것은 바람직하지 않다.
변환이나 흐름 연결은 가능한 한 서비스 계층에서 처리하고, 컨트롤러는 흐름의 출입구만 담당하는 구조가 더 유지보수에 유리하다.

예: 흐름만 넘기는 Controller

@PostMapping
public Mono<BookResponse> createBook(@RequestBody Mono<BookRequest> requestMono) {
    return bookService.createBook(requestMono);
}

예: 논블로킹 처리 포함한 서비스 계층

public Mono<BookResponse> createBook(Mono<BookRequest> requestMono) {
    return requestMono
            .map(mapper::toEntity)
            .flatMap(repository::save)   // 논블로킹 저장
            .map(mapper::toResponse);
}

이 구조는 단위 테스트도 간편하고, 각 계층의 책임도 명확해진다.


6. 흐름 정리: 잘못된 예 vs 올바른 예

항목잘못된 예 (V1)올바른 예 (V2)
@RequestBodyDTO로 직접 받음Mono<DTO>로 받음
DTO 변환 위치Controller 내부Service 내부
저장 방식동기 메서드논블로킹 방식 (R2DBC 등)
흐름 연결없음map / flatMap으로 흐름 구성
논블로킹 여부보장되지 않음완전한 논블로킹

0개의 댓글