어제 오늘 내일

[Spring WebFlux] 어노테이션 기반 컨트롤러: MVC 개발자라면 10분 만에 적응 가능! 본문

IT/SpringBoot

[Spring WebFlux] 어노테이션 기반 컨트롤러: MVC 개발자라면 10분 만에 적응 가능!

hi.anna 2026. 4. 1. 09:15

 

WebFlux를 시작할 때 가장 큰 장벽은 "새로운 문법을 배워야 한다"는 두려움입니다.
하지만 스프링 부트는 기존 MVC 스타일의 코드를 99% 재사용할 수 있게 해 줍니다.

오늘 보여드릴 코드를 보면 "어? 이게 WebFlux라고? MVC랑 똑같은데?"라고 하실 겁니다.
하지만 그 속은 완전히 다른 비동기 엔진(Netty)으로 돌아가고 있죠.


1. 의존성 확인

먼저 spring-boot-starter-web이 아니라 spring-boot-starter-webflux가 필요합니다.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
    // implementation 'org.springframework.boot:spring-boot-starter-web' // 이건 빼세요!
}

이제 톰캣(Tomcat) 대신 네티(Netty) 서버가 뜹니다.


2. 단건 조회: User 대신 Mono<User>

기존 MVC 컨트롤러와 비교해 보겠습니다.

❌ Spring MVC (Blocking)

@RestController
public class MvcController {
    @GetMapping("/mvc/users/{id}")
    public User getUser(@PathVariable Long id) {
        // DB에서 다 가져올 때까지 쓰레드 차단 (3초 대기)
        return userService.findById(id); 
    }
}

✅ Spring WebFlux (Non-Blocking)

@RestController // 똑같음!
public class WebFluxController {

    @GetMapping("/webflux/users/{id}")
    public Mono<User> getUser(@PathVariable Long id) {
        // DB에 요청만 보내고 즉시 리턴 (0.01초)
        // 스프링이 나중에 이 Mono를 구독(subscribe)해서 결과를 클라이언트에 줌
        return userService.findById(id);
    }
}

차이점: 리턴 타입이 User가 아니라 Mono<User>입니다.
이게 답니다. 정말 쉽죠?


3. 목록 조회: List<User> 대신 Flux<User>

여러 건을 조회할 때도 마찬가지입니다.

@GetMapping("/webflux/users")
public Flux<User> getAllUsers() {
    // List<User>를 기다리지 않고, 데이터가 흐르는 파이프(Flux)만 리턴
    return userService.findAll();
}

브라우저의 반응:
일반적인 JSON 응답(application/json)일 경우, Flux의 모든 데이터가 모일 때까지 기다렸다가 [user1, user2, ...] 형태의 JSON 배열로 한 방에 내려줍니다. (사실상 사용자는 차이를 못 느낍니다.)

WebFlux의 진가를 보려면 "스트리밍"을 해야 합니다.


4. WebFlux의 필살기: 스트리밍 (SSE)

데이터가 10,000개인데, 다 모일 때까지 기다리면 사용자는 답답합니다.
"1개 찾을 때마다 바로바로 화면에 띄워줄 수 없을까?"
이때 사용하는 것이 SSE (Server-Sent Events)입니다.

// MediaType을 꼭 지정해야 함! (text/event-stream)
@GetMapping(value = "/stream/users", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<User> streamUsers() {
    // 1초마다 유저 데이터가 하나씩 툭, 툭 전송됨
    return userService.findAll()
            .delayElements(Duration.ofSeconds(1)); // 시각적 효과를 위한 지연
}

결과:
브라우저나 curl로 요청해 보면, 로딩 바가 도는 게 아니라 1초마다 데이터가 한 줄씩 찍히는 것을 볼 수 있습니다. 이게 바로 Non-Blocking Streaming입니다.


5. 요청 본문 받기 (@RequestBody)

데이터를 받을 때도 MonoFlux로 받습니다.

@PostMapping("/users")
public Mono<User> createUser(@RequestBody Mono<UserDto> userDtoMono) {
    // 요청 본문(Body)도 비동기로 들어옵니다.
    return userDtoMono.flatMap(dto -> userService.save(dto));
}
  • 장점: 파일 업로드처럼 큰 데이터가 들어올 때, 메모리에 다 올리지 않고 스트림으로 처리할 수 있어 효율적입니다.

6. 동작 원리: 누가 구독(Subscribe)하나요?

지난 시간에 "구독(subscribe) 안 하면 아무 일도 안 일어난다"고 했죠?
그런데 컨트롤러 코드에는 subscribe()가 없습니다. 누가 하는 걸까요?

바로 스프링 프레임워크(정확히는 Netty와 Reactor Netty Bridge)가 대신해 줍니다.

  1. 요청이 들어오면 핸들러(컨트롤러)를 찾습니다.
  2. 컨트롤러가 MonoFlux를 던져줍니다. (아직 실행 안 됨)
  3. 스프링이 그 리턴 값을 구독(Subscribe)합니다.
  4. 데이터가 발생(onNext)할 때마다 HTTP 응답 버퍼에 씁니다.
  5. 완료(onComplete)되면 연결을 끊습니다.

그래서 우리는 파이프라인 조립(map, flatMap)만 잘해놓고 리턴하면 되는 것입니다.


마치며

오늘의 결론입니다.

  1. WebFlux 컨트롤러는 MVC와 어노테이션(@RestController)이 똑같다.
  2. 리턴 타입만 Mono (단건), Flux (다건)로 바꾸면 된다.
  3. SSE(text/event-stream)를 사용하면 데이터를 실시간으로 흘려보낼 수 있다.

MVC 개발자라면 10분도 안 걸려서 적응하셨을 겁니다.
하지만 WebFlux에는 이것 말고도 "함수형 엔드포인트(Functional Endpoints)"라는 또 다른 스타일이 있습니다. (마치 람다식처럼 라우팅을 정의하는 방식이죠.)

다음 포스팅에서는 "컨트롤러 클래스가 없다고?" 함수형 엔드포인트(Router & Handler) 작성법에 대해 알아보겠습니다.

도움이 되셨다면 좋아요와 댓글 부탁드립니다! 😊

반응형
Comments