Spring

[Spring WebFlux] 2. Reactive Core

noahkim_ 2026. 8. 19. 14:04
  • spring-web 기반 구조
  • ✅ reactive web application을 위한 저수준 기반 기능을 제공함
  •  서버: HTTP Server → HttpHandler → WebHandler API → Controller / Functional Endpoint
  •  클라이언트: HTTP Client → ClientHttpConnector → WebClient

 

1. HttpHandler

  • 서로 다른 HTTP Server를 WebFlux의 공통 Reactive 처리 구조에 연결하는 최소 추상화
  •  HTTP Request를 받아 Response를 Non-Blocking으로 처리하고, 처리 완료 여부를 Mono<Void>로 반환
  • ✅ Netty, Tomcat, Jetty 처럼 서로 다른 서버 API를 Webflux가 동일한 방식으로 사용할 수 있도록 추상화

 

코드) handle()

더보기
더보기
Mono<Void> handle(
    ServerHttpRequest request,
    ServerHttpResponse response
);
  • request → 들어온 HTTP 요청
  • response → 여기에 HTTP 응답을 작성
  • Mono<Void>반환할 데이터는 없고, 응답 작업의 완료/실패만 비동기로 표현

 

서버별 Adapter

  • 서버마다 내부 API가 다르므로 Webflux가 Adapter를 사이에 둠
  • ✅ Server → XXXHttpHandlerAdapter → HttpHandler

 

표) 서버별 Adapter

더보기
더보기
Server Adapter
Reactor Netty ReactorHttpHandlerAdapter
Tomcat TomcatHttpHandlerAdapter
Jetty JettyCoreHttpHandlerAdapter
Servlet Container ServletHttpHandlerAdapter

 

예시) Reactor Netty

더보기
더보기
HttpHandler handler = ...;

ReactorHttpHandlerAdapter adapter =
    new ReactorHttpHandlerAdapter(handler);

HttpServer.create()
    .host(host)
    .port(port)
    .handle(adapter)
    .bindNow();

 

2. WebHandler API

  • Spring WebFlux에서 웹 애플리케이션 수준의 요청 처리 기능을 제공하는 API
  • ✅ HttpHandler 위에서 실제 웹 애플리케이션에 필요한 기능을 제공함 
  • ex) 세션, 필터, 예외 처리 등

 

Special bean types

  • WebHttpHandlerBuilder는 다음 Bean들을 자동으로 찾아 WebHandler 처리 체인을 구성함
Bean 개수 역할
WebExceptionHandler 0..N Filter 또는 WebHandler에서 발생한 예외 처리
WebFilter 0..N 요청 전·후 공통 처리
WebHandler 1 실제 요청 처리
WebSessionManager 0..1 WebSession 관리
ServerCodecConfigurer 0..1 Form / Multipart 등의 요청 데이터 파싱
LocaleContextResolver 0..1 요청의 Locale 결정
ForwardedHeaderTransformer 0..1 Forwarded Header 처리

 

Form Data

Multipart Data

Forwarded Headers

  • 프록시나 로드밸런서를 거치기 전, 클라이언트가 원래 요청한 정보를 전달하기 위한 HTTP 헤더
  •  RFC 7239에서 표준 Forwarded 헤더를 정의함
  • ✅ Client 요청 → Proxy가 원래 요청 정보 기록 → Forwarded Header 추가 → Application 전달
  • ➡️ 요청이 프록시를 거치면서 바뀐 정보 때문에, 백엔드가 원래 클라이언트가 어떤 주소로 요청했는지 알 수 있도록 전달하는 정보

 

예시) Forwarded Headers

더보기
더보기
X-Forwarded-Proto: https
X-Forwarded-Host: example.com
X-Forwarded-Port: 443
Forwarded: proto=https;host=example.com
  • 위 두 형태로 제공됨

 

Non-standard Headers

ForwardedHeaderTransformer

  • Forwarded Header 애플리케이션이 보는 요청의 host, port, scheme을 원래 클라이언트 요청 기준으로 수정하는 컴포넌트
  • ⚠️ Forwarded 헤더를 신뢰할 수 있는 프록시가 넣었는지 구별할 수 없음
  • ➡️ Proxy에서 외부사용자가 임의로 넣은 값을 제거하도록 구성해야 함

 

예시) 처리 과정

더보기
더보기

원래 요청이 아래와 같다면

http://10.0.0.5:8080
Forwarded: proto=https;host=example.com
  • 요청 변환 담당: "http://10.0.0.5:8080 → ForwardedHeaderTransformer → https://example.com"
  • 변환에 성공한 Forwarded 헤더는 제거함

 

코드) 빈 등록

더보기
더보기
@Bean
ForwardedHeaderTransformer forwardedHeaderTransformer() {
    ForwardedHeaderTransformer transformer = new ForwardedHeaderTransformer();
	transformer.setRemoveOnly(true); // Forwarded Header 신뢰하지 않고 제거하기
    
    return transformer;
}

 

3. Filters

  • WebHandler가 요청을 처리하기 전/후에 공통 로직을 적용하는 필터
  • 여러 WebFilter가 체인 형태로 실행된 뒤 최종적으로 WebHandler가 요청을 처리함
  •  공통 처리에 사용됨
  •  @Order or Ordered 구현으로 우선순위 지정 가능
  •  Bean 등록 시 자동 적용됨
  • ex) 인증, 로깅, CORS, 요청/응답 가공 등

 

CORS

  • Controller에 Annotation을 사용해 CORS를 세부적으로 설정할 수 있음
  • ✅ Spring Security와 함께 사용할 경우 CorsWebFilter 빈 등록이 권장됨 (Security보다 CORS가 먼저 처리되야 함)

 

예제) Controller에 직접 적용

더보기
더보기
@RestController
@RequestMapping("/api")
@CrossOrigin(origins = "http://localhost:3000")
public class UserController {

    @GetMapping("/users")
    public Flux<User> users() {
        return userService.findAll();
    }
}

 

예제) CorsWebFilter 전체 적용

더보기
더보기
@Configuration
public class CorsConfig {

    @Bean
    public CorsWebFilter corsWebFilter() {
        CorsConfiguration config = new CorsConfiguration();

        config.addAllowedOrigin("http://localhost:3000");
        config.addAllowedMethod("*");
        config.addAllowedHeader("*");
        config.setAllowCredentials(true);

        UrlBasedCorsConfigurationSource source =
                new UrlBasedCorsConfigurationSource();

        source.registerCorsConfiguration("/**", config);

        return new CorsWebFilter(source);
    }
}

 

예제) Spring Security 사용

더보기
더보기
@Bean
SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {

    return http
            .cors(cors -> {})
            .csrf(ServerHttpSecurity.CsrfSpec::disable)
            .authorizeExchange(exchange -> exchange
                    .anyExchange().authenticated()
            )
            .build();
}
  • Request → CORS 처리 → Spring Security Filter Chain → WebHandler
  • SecurityWebFilterChain.cors() → Spring Security에서 CORS 처리를 활성화함
  • 이후 CorsWebFilter 빈 등록하면 적용됨 

 

URL Handler

 

4. Exceptions

  • WebExceptionHandler는 WebFilter 체인이나 최종 WebHandler에서 발생한 예외를 처리하는 컴포넌트
  • Request → WebFilter → WebHandler → Exception → WebExceptionHandler
  • ✅ Spring Bean으로 등록하면 WebFlux 예외 처리 체인에 포함됨
  • ✅ 여러 개 등록 가능
  • ✅ @Order 또는 Ordered를 통해 실행 우선순위 지정 가능

 

구현체

구현체 역할
ResponseStatusExceptionHandler ResponseStatusException의 상태 코드를 HTTP Response에 설정
WebFluxResponseStatusExceptionHandler ResponseStatusExceptionHandler 확장 + 예외 클래스의 @ResponseStatus도 인식

 

코드) ResponseStatusExceptionHandler

더보기
더보기
@GetMapping("/users/{id}")
public Mono<User> getUser(@PathVariable Long id) {
    return userService.findById(id)
            .switchIfEmpty(Mono.error(
                    new ResponseStatusException(
                            HttpStatus.NOT_FOUND,
                            "User not found"
                    )
            ));
}
  • ResponseStatusExceptionHandler → 404 Not Found

 

코드) WebFluxResponseStatusExceptionHandler

더보기
더보기
@ResponseStatus(HttpStatus.NOT_FOUND)
public class UserNotFoundException extends RuntimeException {

    public UserNotFoundException() {
        super("User not found");
    }
}
@GetMapping("/users/{id}")
public Mono<User> getUser(@PathVariable Long id) {
    return userService.findById(id)
            .switchIfEmpty(Mono.error(
                    new UserNotFoundException()
            ));
}
  • UserNotFoundException
  • → @ResponseStatus(HttpStatus.NOT_FOUND)
  • → WebFluxResponseStatusExceptionHandler
  • → 404 Not Found

 

5. Codecs

  • HTTP에서 전달되는 byte 데이터와 Java 객체 사이를 변환하는 기능
  • ✅ Non-Blocking I/O + Reactive Streams Backpressure 기반
  •  HTTP Body(byte) → Decoder / HttpMessageReader → Java Object
  •  Java Object → Encoder / HttpMessageWriter → HTTP Body(byte)

 

Jackson JSON

  • Jackson 라이브러리가 존재하면 WebFlux에서 JSON ↔ Java 객체 변환을 지원함
  •  Mono Flux, Media Type에 따라 처리 방식이 달라짐

 

표) JacksonJsonEncoder

더보기
더보기
대상 처리 방식 핵심
Mono<T> JSON 객체 하나를 완성한 뒤 JsonMapper로 Java 객체 생성 단일 객체 Decode
Flux<T> JSON 객체 하나가 완성될 때마다 바로 Java 객체로 변환 다중 객체 Streaming Decode 가능
지원 형태 JSON Array, NDJSON, JSON Lines, JSON Text Sequences 객체 단위 처리 가능
  • JSON byte stream → TokenBuffer → JsonMapper → Java Object

 

표) JacksonJsonEncoder

더보기
더보기
대상 처리 방식 핵심
Mono<T> 객체 하나를 JsonMapper로 JSON 직렬화 단일 객체 Encode
Flux<T> + application/json 여러 값을 모아 Collection으로 만든 뒤 JSON 배열로 직렬화 일반 JSON 응답
Flux<T> + Streaming Media Type 객체 하나씩 Encode → Write → Flush 실시간 Streaming
SSE Event마다 JSON 인코딩 후 즉시 Flush 지연 없이 이벤트 전달
  • Java Object → JsonMapper → JSON

 

표) String 처리

더보기
더보기
대상 처리 방식 핵심
String 기본적으로 Jackson JSON Codec이 처리하지 않음 CharSequenceEncoder 사용
Flux<String> 문자열 스트림으로 처리 JSON 배열로 자동 변환되지 않음
Flux<String>
→ JSON Array 필요
collectToList()Mono<List<String>>로 변환 후 JSON 인코딩 JSON 배열 형태로 반환 가능
  • Flux<String> → collectToList() → Mono<List<String>> → JSON Array


Form Data

Multipart 

Protocol Buffers

  • Google이 만든 바이너리 직렬화 형식
  • ✅ Java Object → ProtobufEncoder → Binary Data → HTTP
  •  HTTP Binary Data → ProtobufDecoder → Protobuf Message
  • ✅ application/x-protobuf, application/octet-stream, application/vnd.google.protobuf를 지원
Codec 역할
ProtobufDecoder Protobuf → 객체
ProtobufEncoder 객체 → Protobuf
ProtobufJsonDecoder JSON → Protobuf Message
ProtobufJsonEncoder Protobuf Message → JSON

 

Google Gson

Limits

  • 일부 Decoder는 데이터를 객체로 만들기 위해 메모리에 일정량을 버퍼링함
  • ✅ maxInMemorySize를 이용해 서버 측 Codec의 버퍼 최대 한도를 설정함
  •  HTTP Body → Buffer → maxInMemorySize 검사 → Decode

 

설정) limits

더보기
더보기
@Configuration
public class WebFluxConfig implements WebFluxConfigurer {

    @Override
    public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
        configurer.defaultCodecs()
                .maxInMemorySize(2 * 1024 * 1024); // 2MB
    }
}

 

Streaming

  • heartbeat를 주기적으로 보내 연결을 감지함
  •  text/event-stream, application/x-ndjson 지원
  • ✅ 연결이 끊겼는데 아무 데이터를 보내지 않으면 서버가 클라이언트 연결 종료를 늦게 인식할 수 있음
  • ➡️ 주기적으로 데이터를 보내는 것이 중요

 

DataBuffer

  • WebFlux가 사용하는 byte buffer 추상화
  •  Codec의 입력 또는 출력으로 사용됨
  • ✅ Netty ByteBuf / ByteBuffer → DataBuffer → Codec
  •  서버마다 버퍼 Buffer 구현이 다를 수 있음
  • ⚠️ Netty처럼 pooled buffer를 사용하는 환경에서 DataBuffer를 직접 다루는 경우, 사용 후 release 필수

 

설명) buffer pool

더보기
더보기
  • netty는 네트워크 요청을 매우 많이 처리하기 때문에, 매 요청마다 새 byte buffer를 만들면 비용이 커질 수 있음
  • ✅ 매번 새 메모리를 만드는 대신 기존 메모리를 재활용함
  • ✅ Buffer Pool → ByteBuf 빌림 → 사용 → Pool에 반환 → 다음 요청에서 재사용
  • ➡️ 소비한 후 release해야 메모리 누수를 피할 수 있음

 

6. Logging

  • 비동기/멀티스레드 환경에서도 요청 단위로 로그를 추적할 수 있도록 설계됨
  • ✅ DEBUG: 핵심 정보 위주로 간결하게 출력
  • TRACE: DEBUG보다 더 상세한 정보를 제공하지만 불필요하게 과도한 로그는 지양
  • ➡️  WebFlux에서는 하나의 요청이 여러 Thread에서 처리될 수 있으므로 Thread ID 대신 요청별 Log ID를 사용

 

Log Id

  • ⚠️ WebFlux에서는 하나의 요청이 여러 Thread를 오갈 수 있기 때문에 Thread ID 만으로 같은 요청의 로그를 묶기 어려움
  • ➡️ Thread가 아니라 Request 기준으로 로그를 추적함
  • Server: ServerWebExchange에 'LOG_ID_ATTRIBUTE'로 저장됨
  • Client: ClientRequest에 'LOG_ID_ATTRIBUTE'로 저장됨

 

코드) Server LogId 읽기

더보기
더보기
@Component
public class LoggingFilter implements WebFilter {

    private static final Logger log = LoggerFactory.getLogger(LoggingFilter.class);

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        log.info("{} request: {} {}",
                exchange.getLogPrefix(),
                exchange.getRequest().getMethod(),
                exchange.getRequest().getURI());

        return chain.filter(exchange);
    }
}
  • ServerWebExchange#getLogPrefix(): 서버의 Log ID Prefix 가져옴

 

코드) Client LogId 읽기

더보기
더보기
WebClient webClient = WebClient.builder()
        .filter((request, next) -> {
            log.info("{} request: {} {}",
                    request.logPrefix(),
                    request.method(),
                    request.url());

            return next.exchange(request);
        })
        .build();
  • ClientRequest#logPrefix(): WebClient가 보내는 요청의 Log ID Prefix 가져옴

 

Sensitive Data

  • Spring WebFlux는 기본적으로 Form Parameter/Header 마스킹 처리함.
  • ⚠️ DEBUG나 TRACE 로그에는 민감한 정보가 포함될 수 있음 (Request Header, Form Parameter 등)
  • ❗️ 전체 Request 정보를 로그에 출력하려면 명시적으로 활성화해야 함

 

설정) Server 상세 Logging

더보기
더보기
@Configuration
class MyConfig implements WebFluxConfigurer {

    @Override
    public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {

        configurer.defaultCodecs()
                .enableLoggingRequestDetails(true);
    }
}

 

설정) Client 상세 Logging

더보기
더보기
Consumer<ClientCodecConfigurer> consumer = configurer ->
        configurer.defaultCodecs()
                .enableLoggingRequestDetails(true);

WebClient webClient = WebClient.builder()
        .exchangeStrategies(strategies -> strategies.codecs(consumer))
        .build();

 

Appenders

Custom codecs

 

출처

'Spring' 카테고리의 다른 글

[Spring WebClient] 1. Request / Response  (0) 2026.08.19
[Spring WebFlux] 3. DispatcherHandler  (0) 2026.08.19
[Spring WebFlux] 1. Overview  (0) 2026.08.18