Java 아키텍처·Spring 용어 사전
리액티브리액티브 · WebFlux · 백프레셔 · 이벤트 루프

Mono·Flux

값이 아니라 '값이 올 파이프라인'을 표현하는 타입. 구독해야 실행되며 블로킹 한 줄이 전체를 무너뜨린다.

값이 아니라 "값이 올 파이프라인" 을 표현하는 타입. Mono는 0~1개, Flux는 0~N개다.

조립과 실행이 분리된다

Mono<Order> mono = repo.findById(id)          // 아직 아무것도 실행되지 않는다
    .map(this::toDto)
    .doOnNext(log::info);
// 여기까지는 '설계도' 만 만들었다

mono.subscribe();     // 이 순간 흐르기 시작한다 (WebFlux 는 프레임워크가 대신 구독한다)

구독하지 않으면 아무 일도 안 일어난다 — 리액티브 입문자가 가장 먼저 겪는 함정이다.

flatMap이냐 concatMap이냐

flux.flatMap(this::call)     // 동시 실행. 완료 순서대로 나온다 → 순서 보장 X
flux.concatMap(this::call)   // 하나씩 순차 실행 → 순서 보장, 느리다
flux.flatMapSequential(...)  // 동시 실행하되 결과는 원래 순서로

순서가 중요한데 flatMap을 쓴 버그는 부하가 낮을 땐 우연히 순서가 맞아 재현이 어렵다.

이벤트 루프와 스케줄러

  • 이벤트 루프 스레드는 코어 수 정도로 소수다

    • 여기서 절대 블로킹하면 안 된다
  • Schedulers.parallel() — CPU 바운드

  • Schedulers.boundedElastic() — 어쩔 수 없는 블로킹 작업 격리용

// 단 하나의 금기
@GetMapping("/orders/{id}")
Mono<Order> get(@PathVariable Long id) {
    Order o = jdbcTemplate.queryForObject(...);   // ❌ 이벤트 루프가 멈춘다
    return Mono.just(o);
}

소수의 루프 스레드 중 하나가 멈추면 그 위의 모든 요청이 함께 멈춘다. 몇 개 겹치면 처리량이 폭락한다.

백프레셔 — 리액티브가 대체 불가능한 지점

생산자가 소비자보다 빠르면 버퍼가 쌓이다 OOM 이 난다

  • 소비자가 request(n) 으로 "N개만 더" 라고 역방향 요구한다

생산자가 속도를 못 늦추면 전략을 고른다

  • onBackpressureBuffer / Drop / Latest

가상 스레드는 백프레셔를 대체하지 못한다 — 동시성 비용 문제를 푼 것이지 흐름 제어 문제를 푼 게 아니다.

가상 스레드가 계산을 바꿨다

벤치마크상 DB·HTTP 바운드에서 처리량이 통계적으로 거의 같고(5~8% 이내) 가상 스레드가 코드 단순함·디버깅에서 이긴다 Java 24 의 JEP 491 이 pinning 이라는 마지막 반론까지 지웠다

  • 순수 I/O 동시성이면 이제 가상 스레드가 더 단순한 답이다
  • 스트리밍 · 백프레셔 · 복잡한 비동기 합성이면 여전히 리액티브

채택 전 반드시 계산할 것 — R2DBC

리액티브를 택하면 DB 는 R2DBC 가 된다. 그리고 R2DBC 에는 JPA 가 없다

  • 영속성 컨텍스트 · 변경 감지 · 지연 로딩이 전부 사라진다
  • 이게 WebFlux 채택률이 꺾인 진짜 이유다

면접 함정

  • "WebFlux + JPA" → 성립하지 않는 조합이다.
  • "리액티브가 CPU를 더 잘 쓴다" → 스레드 자원 효율의 문제였다.

컨텍스트 전파가 다르다

// ThreadLocal 이 안 통한다 — 한 요청이 여러 스레드를 옮겨 다니기 때문이다
MDC.put("traceId", id);                   // ❌ 이후 연산자에서 사라진다

// Reactor Context 를 쓴다
Mono.deferContextual(ctx -> {
    String traceId = ctx.get("traceId");
    return service.call();
}).contextWrite(Context.of("traceId", id));

로깅·보안 컨텍스트·트랜잭션이 전부 이 영향을 받는다. "ThreadLocal 기반 라이브러리는 그대로 쓸 수 없다" 가 리액티브 전환의 숨은 비용이다.

디버깅이 어려운 이유와 대책

Hooks.onOperatorDebug();                  // 스택 트레이스에 조립 지점을 남긴다 (개발용·느리다)
// 또는 reactor-tools 의 ReactorDebugAgent (운영에서도 쓸 만하다)

mono.log()                                // 각 신호(onSubscribe·onNext·onComplete)를 로그로
   .checkpoint("주문 조회")                // 이 지점 이름을 스택에 남긴다

기본 스택 트레이스에는 어느 연산자에서 났는지가 안 나온다 — 이 도구들 없이는 원인 추적이 매우 어렵다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 리액티브 — WebFlux·Reactor·백프레셔