본문으로 건너뛰기
홈
기술
기술 전체
프로그래밍68
컴퓨터 과학63
AI48
웹 개발36
인프라33
데이터31
소프트웨어 공학18
소개
← 목록으로웹 개발 › 백엔드 › Spring WebFlux

4. Spring WebFlux

목차

Spring WebFlux: 요청 하나를 끝까지 따라가기

Spring MVC와 WebFlux는 모두 HTTP 요청을 처리한다. MVC는 Servlet API 위에 구축됐고, WebFlux는 Reactive Streams 기반의 비동기 웹 스택이다. WebFlux를 선택했다고 애플리케이션의 모든 작업이 자동으로 논블로킹이 되지는 않는다. 컨트롤러가 JDBC나 오래 걸리는 파일 읽기를 호출하면 그 작업은 여전히 호출 스레드를 기다리게 한다. 반대로 Spring MVC도 비동기 요청이나 reactive 반환 타입을 지원한다. 어느 쪽이 적합한지는 요청 흐름 전체의 I/O와 운영 요구로 판단해야 한다.

기존에 정리한 WebFlux 스택 그림

그림에서 보듯 WebFlux는 Spring MVC와 일부 프로그래밍 모델을 공유하지만 실행 경로는 별도다. WebFlux는 Netty 같은 비서블릿 런타임이나 지원되는 Servlet 컨테이너에서 실행할 수 있다. 서버 종류보다 중요한 것은 요청의 연산자와 하위 클라이언트가 실제로 어디에서 기다리는가다.

1. 요청을 Flux로 반환하기

Spring Boot 프로젝트에서 WebFlux starter를 사용한다. 옛 메모의 2.3.5.RELEASE를 새 프로젝트에 고정해서 복사하지 말고, 현재 Boot 버전에 맞는 의존성 관리와 호환성을 확인한다. 아래는 의존성을 추가한 뒤 사용할 수 있는 작은 컨트롤러 예시다.

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import java.util.List;

@RestController
public class FruitController {
    @GetMapping("/fruits")
    public Flux<String> fruits() {
        return Flux.fromIterable(List.of("Apple", "Banana", "Grape"));
    }
}

Flux.fromIterable은 메모리의 세 값을 발행한다. 이 코드는 비동기 외부 I/O를 하지 않으므로 “WebFlux가 빠르다”는 성능 사례가 아니다. 응답 직렬화 방식과 클라이언트의 Accept 헤더에 따라 값을 모아 JSON 배열로 내보내거나 스트리밍 형식으로 보낼 수 있다. 한 값씩 보내는 지연이 요구된다면 적절한 스트리밍 미디어 타입과 클라이언트 소비 방식을 함께 정한다.

sequenceDiagram
  participant Client as 클라이언트
  participant Router as WebFlux 라우팅
  participant Handler as FruitController
  participant Pub as Flux
  Client->>Router: GET /fruits
  Router->>Handler: fruits()
  Handler-->>Router: Flux 반환
  Router->>Pub: 구독
  Pub-->>Client: 응답 값과 완료 신호

컨트롤러가 Flux를 반환하는 시점과 응답이 실제로 소비되는 시점을 구별한다. map, filter 같은 연산자를 조립한 뒤 아무도 구독하지 않으면 실행되지 않는 경우가 많다. WebFlux가 HTTP 응답을 작성할 때 구독한다. 컨트롤러 안에서 subscribe()를 직접 호출하고 곧바로 성공 응답을 보내면 내부 오류가 HTTP 결과와 분리돼 버릴 수 있다.

2. 함수형 라우터로 같은 일을 표현하기

어노테이션 컨트롤러 대신 RouterFunction으로 경로와 핸들러를 조합할 수도 있다. 두 모델은 같은 애플리케이션에서 사용할 수 있지만, 팀이 경로를 찾고 테스트하는 방식을 일관되게 유지하는 편이 좋다.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;

@Configuration
class FruitRoutes {
    @Bean
    RouterFunction<ServerResponse> fruitRoute() {
        return route()
            .GET("/hello", request ->
                ServerResponse.ok().bodyValue("hello world"))
            .build();
    }
}

라우터는 GET /hello에 맞는 요청을 찾고, 핸들러는 Mono<ServerResponse>를 반환한다. bodyValue는 이미 준비된 작은 값을 응답에 넣는 예시다. DB에서 조회하는 경우에는 서비스가 반환한 Mono·Flux를 본문으로 연결한다. 요청 파라미터 검증, 못 찾은 리소스의 404, 서버 오류의 응답 형태는 이 예제에 추가해야 한다.

3. 비동기 HTTP 호출과 블로킹 경계

WebFlux의 WebClient는 비동기 HTTP 클라이언트다. 외부 API를 호출할 때 응답을 기다리며 이벤트 루프를 묶지 않도록 Mono를 반환하고 연산자로 조합한다.

Mono<String> title = webClient.get()
    .uri("/items/{id}", id)
    .retrieve()
    .bodyToMono(String.class)
    .timeout(Duration.ofSeconds(2));

이 조각은 webClient와 유효한 id가 준비됐다는 전제다. 타임아웃 후 호출자에게 어떤 HTTP 상태를 내보낼지, 안전하게 재시도할 수 있는 요청인지 정해야 한다. 결제 POST 같은 부작용 있는 요청을 자동 재시도하면 중복 처리될 수 있다. 응답 본문 크기와 연결 풀도 운영 설정에서 상한을 둔다.

기존 저장소가 JDBC라면 Mono.just(repository.find(id))로 감싸도 이미 find가 호출돼 블로킹한다. 옮기는 동안에는 Mono.fromCallable(() -> repository.find(id)).subscribeOn(Schedulers.boundedElastic())로 실행 경계를 분리할 수 있다. 이것은 제한된 별도 스레드 풀로 블로킹을 옮기는 완화책이지 논블로킹 DB 드라이버가 되는 것은 아니다. 트랜잭션과 스레드 로컬 컨텍스트도 재검토해야 한다.

요구·제약먼저 검토할 선택
기존 MVC와 JDBC 중심, 처리량 문제 없음MVC 유지, 필요하면 WebClient만 도입
여러 느린 외부 I/O의 결과를 조합WebClient와 WebFlux 또는 MVC 비동기 지원 비교
응답을 오래 스트리밍하고 취소를 전파WebFlux와 Reactor의 신호 처리 검토
DB 접근이 모두 블로킹전 구간 WebFlux 전환의 이득을 측정 후 판단
팀이 Reactor 오류·배압을 모름작은 경계에서 시작하고 테스트·관측을 갖추기

WebFlux가 모든 요청을 더 빠르게 만드는 도구는 아니다. 동시 연결 수, 외부 I/O 대기, CPU 사용, 메모리, 응답 지연을 같은 부하 조건에서 비교해야 한다. CPU 계산량이 큰 작업은 논블로킹 I/O 모델만으로 빨라지지 않는다.

4. 실패를 어떻게 테스트할까?

WebTestClient로 정상 JSON 응답과 404·검증 오류를 확인한다. Reactor의 StepVerifier는 Flux의 값뿐 아니라 완료·오류·취소를 검사하는 데 쓴다. 테스트가 Flux 안에서 실제로 일어나는 작업을 구독해야 실패가 드러난다. 외부 HTTP 호출은 응답 지연·타임아웃·연결 거부를 모의해 후속 요청이 무한 대기하지 않는지 확인한다.

운영에서는 요청별 총 지연만 보지 말고 외부 API, DB, 직렬화 구간의 시간과 오류율을 나눈다. 이벤트 루프가 막히는 작업, 무제한 flatMap 동시성, 느린 소비자 때문에 커지는 버퍼를 찾아야 한다. 스레드가 적다는 설명보다 실제 부하에서 이 수치를 확인하는 것이 WebFlux 도입의 근거가 된다.

참고: Spring WebFlux 공식 가이드, 함수형 엔드포인트, WebClient, Reactor의 블로킹 호출 경계.

같은 카테고리의 글