개발/TIL

spring webflux 에 대한 개요

ebang 2025. 2. 27. 23:53

 

spring webflux 란 

비동기식 서버(Netty) , 혹은 MSA 에서 다른 서비스에 데이터를 요청(WebClient) 할 때 유용하게 사용할 수 있는 웹 프레임워크이다. 

Spring MVC가 동기 방식(Servlet 기반)이라면, WebFlux는 Netty 기반의 논블로킹 방식으로 동작한다. 

 

Reactive Streams 에 기반한 기술이며, 구성요소는 RouterFunctions, Annotation, WebClient가 있다. (비동기 HTTP client, RestTemplate의 대체제.)

 

동작 방식은 Event Loop 로써, 요청이 들어오면 큐에 등록하고 IO 작업이 완료되면 콜백함수를 실행한다.

사용하는 데이터 타입은 Flux(stream) 혹은 Mono.(값이 0또는 1개) 이다. 

 

 

<Spring WebFlux 의 동작 방식> - Publisher-Subscriber 패턴을 중심으로

Spring WebFlux 를 사용할 때는 Publisher-Subscriber 패턴을 사용한다.

Publisher 는 데이터를 생성하는 주체이고, Subscriber는 데이터를 소비하는 주체이다.

Subscriber 가 Publisher 를 구독하면, subscribtion 객체를 이용해서 서로 소통이 가능하며 이 객체에는 Subscriber 가 소비하는 데이터의 양이 정의되어 있다.

Publisher 는 이 데이터의 양에 맞는 데이터를 생성해서 Subscribtion 객체에 전달한다.

 

Publisher 가 data 생성, subscription 이 onSubscribe 함수 호출, Subscriber 가 request, subscription 이 데이터를 계속 송신. (onNext) 데이터를 수신 완료 혹은 에러가 났다면 onComplete/onError 를 수행한다.

 

 

Publisher의 종류

Publisher 에는 Mono, Flux 두가지 종류가 있는데 Mono는 하나의 데이터를 생성한다. Mono를 이용해서 요청을 수신한다면, client 가 결과가 생성되기를 기다리는 것이 아니라 생성되는 동안 다른 작업을 진행할 수 있다.

Flux 는 여러개의 데이터를 생성한다. Flux 를 이용해서 요청을 처리한다면, server 가 결과를 생성하는 동안, 다른 요청을 처리할 수 있다.

 

 

<WebFlux를 이용한 Netty 서버>

비동기식으로 데이터를 처리하는 WebFlux 를 100% 활용하기 위해서는 비동기식 웹 서버가 필요한데, 여기서 사용되는 것이 바로 Netty 서버이다.

이벤트 루프를 여러개 두고 사용하는 mulit-event-loop 이 특징이다. 각 이벤트 루프는 싱글 스레드 기반이다.

 

 

<WebFlux의 일부, WebClient>

비동기식으로 http request 를 보내고 처리하기 위한 라이브러리이다. 

기존 Spring MVC 에서는 이런 http request 는 RestTemplate 으로 수행했는데, 동기 방식만 지원했었다.

 

<WebFlux 와 Database 동작>

- MongoDB 와 함께 사용

기존 ORM, RDB 와 통신할 때는 동기적으로 수행하기 떄문에, 함께 사용하는데 좋지 않다.

MongoDB 는 비동기식, 논블러킹으로 동작이 가능하기 때문에 함께 사용하기에 적합하다.

 

 

 

Spring WebFlux를 이용한 외부 서버 요청  코드 예시

Spring WebFlux에서 비동기적으로 외부 API에 요청을 보내고 응답을 처리하는 방법은 WebClient를 사용하는 것이다. 
WebClient는 Spring 5에서 도입된 비동기 논블로킹 HTTP 클라이언트이며, 기존 RestTemplate의 대체제로 사용된다. 

 

WebClient 는 아래 예시처럼 직접 생성하든지, private final로 선언후 자동으로 spring bean으로 등록하게끔 하는 방법이 있다. 

후자의 경우, @Configuration 어노테이션을 통해 WebClient의 설정을 따로 해두는 것이 일반적이다. 

 

전자에 대한 코드 예시.

import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@Service
public class UserService {
    private final WebClient webClient;

    // WebClient Bean을 생성자에서 초기화
    public UserService(WebClient.Builder webClientBuilder) {
        this.webClient = webClientBuilder.baseUrl("https://jsonplaceholder.typicode.com").build();
    }

    // 1. 단일 사용자 정보 가져오기 (Mono)
    public Mono<UserResponse> getUserById(Long userId) {
        return webClient.get()
                .uri("/users/{id}", userId)
                .retrieve()
                .bodyToMono(UserResponse.class);
    }

    // 2. 전체 사용자 목록 가져오기 (Flux)
    public Flux<UserResponse> getAllUsers() {
        return webClient.get()
                .uri("/users")
                .retrieve()
                .bodyToFlux(UserResponse.class);
    }

    // 3. 새로운 사용자 생성 요청 (Mono)
    public Mono<UserResponse> createUser(UserResponse userRequest) {
        return webClient.post()
                .uri("/users")
                .bodyValue(userRequest)
                .retrieve()
                .bodyToMono(UserResponse.class);
    }
}

 

retrieve() 가 실행될 때 실제 요청이 수행되고,  bodyToMono() 혹은 bodyToFlux 까지 실행하고나면 결과가 Mono / Flux 에 저장되게 된다. 이후에 .map() 을 통해 응답을 가공할 수도 있다. 

 

에러 처리 혹은 리트라이 로직도 추가할 수 있다. 

 

 

public Mono<UserResponse> getUserById(Long userId) {
    return webClient.get()
            .uri("/users/{id}", userId)
            .retrieve()
            .
            .onStatus(status -> status.is4xxClientError(), 
                response -> Mono.error(new RuntimeException("클라이언트 오류 발생!")))
            .onStatus(status -> status.is5xxServerError(), 
                response -> Mono.error(new RuntimeException("서버 오류 발생!")))
            .bodyToMono(UserResponse.class);
}

 

 

외부 API 요청이 일시적으로 실패(예: 네트워크 오류, 서버 다운, 5xx 오류)할 경우, 자동으로 재시도(Retry) 하는 기능을 추가할 수 있따. 

Spring WebFlux의 retry()  retryWhen() 메서드를 사용하면 특정 조건에서 비동기 요청을 재시도할 수 있다. 

 

import reactor.util.retry.Retry;
import java.time.Duration;

public Mono<UserResponse> getUserByIdWithRetry(Long userId) {
    return webClient.get()
            .uri("/users/{id}", userId)
            .retrieve()
            .onStatus(status -> status.is5xxServerError(),
                response -> Mono.error(new RuntimeException("서버 오류 발생!")))
            .bodyToMono(UserResponse.class)
            .retryWhen(Retry.fixedDelay(3, Duration.ofSeconds(2))); // 2초 간격으로 최대 3회 재시도
}


public Mono<UserResponse> getUserByIdWithExponentialBackoff(Long userId) {
    return webClient.get()
            .uri("/users/{id}", userId)
            .retrieve()
            .bodyToMono(UserResponse.class)
            .retryWhen(
                Retry.backoff(3, Duration.ofSeconds(1)) // 첫 번째 재시도는 1초 후
                    .maxBackoff(Duration.ofSeconds(10)) // 최대 10초까지 대기
                    .jitter(0.5) // 랜덤 지연 추가 (0~50% 변동)
                    .filter(ex -> ex instanceof RuntimeException) // 특정 예외에서만 재시도
            );
}

Retry.fixedDelay() 는 고정된 값을, Retry.backoff(3, Duration.ofSeconds(1)) 를 사용하면 지수적으로 증가하는 값 만큼 지연하면서 재시도를 수행하게 된다. 

 

Mono, Flux 데이터를 가지고 여러 요청을 동시에 비동기적으로 수행할 수도 있고, 동기적으로 수행하면서 응답을 가공할 수도 있는데, 

곧 추후의 글에서 관련된 내용의 글을 작성하도록 하겠다. 

'개발 > TIL' 카테고리의 다른 글

java 개선된 switch 문 사용하기  (2) 2025.03.01
Validation과 Exception Handling  (2) 2025.03.01
kubernetes에서 yml 로 pod 관리하기  (0) 2025.02.26
java record 알차게 사용하기  (0) 2025.02.25
@EntityGraph 어노테이션  (0) 2025.02.24