동기냐 비동기냐 그것이 문제로다

WebFlux를 곁들인

Posted by Yan on April 14, 2025

비동기는 무조건 빠른가? NO
IO 병목 심한 API 서비스를 WebFlux로 바꿔야 할까?
비동기는 블로킹을 줄이는 것일 뿐, 연산 자체가 빠른 게 아님
오히려 잘못 설계된 비동기 처리는 컨텍스트 전환 비용으로 더 느려질 수도 있음

동기(Synchronous) 처리

  • 개념: 클라이언트 요청이 들어오면, 해당 요청을 처리하는 동안 쓰레드가 작업을 마칠 때까지 대기한다. 결과가 나올 때까지 다른 작업은 못한다.
  • Spring MVC (Servlet 기반) 같은 전통적인 웹 프레임워크가 동기처리에 해당됨
  • 작동방식 : 요청 → 컨트롤러 → 서비스 → DAO → 응답 (이 모든 과정이 한 쓰레드에서 순차적으로 실행됨)
  • 장점:
    • 구조가 단순하고 디버깅이 쉽다.
    • 기존 스프링 생태계와 연동이 잘 된다 (ex. Spring Security, AOP 등)
  • 단점:
    • I/O 작업(예: DB, 외부 API 호출 등)이 느리면 쓰레드가 블로킹되어 서버 자원이 낭비됨.
    • 많은 요청이 동시에 들어오면 쓰레드 수가 급증해서 성능이 급격히 저하될 수 있음.

비동기(Asynchronous) 처리

  • 요청을 받았을 때, I/O 등 시간이 오래 걸리는 작업은 다른 작업으로 미뤄두고, 쓰레드는 즉시 반환되어 다른 요청을 처리할 수 있도록 한다.
  • Spring WebFlux (Reactor 기반) 가 대표적인 비동기 논블로킹 방식
  • 작동 방식: 요청 → Mono/Flux 기반의 논블로킹 처리 → 이벤트 발생 시 응답 반환
  • 장점:
    • 적은 수의 쓰레드로도 많은 요청을 동시에 처리 가능 → 높은 확장성(Scalability)
    • 특히 외부 API 호출, DB I/O 등 네트워크 병목이 많은 서비스에 적합
  • 단점:
    • 콜백 지옥 혹은 복잡한 흐름 제어(이를 해결하기 위한 연산자 학습 필요)
    • 기존 라이브러리와의 호환성 문제 (블로킹 API 사용 금지 등)
    • 디버깅이 어렵고 StackTrace가 끊어져서 추적이 힘듦

Webflux의 동작 방식

  1. 요청이 Mono스트림으로 만들어지고
  2. 그 스트림 위에 각종 변환 연산자 map, flatMap가 체인으로 연결되고
  3. 응답이 올 때 그 체인에 따라 처리되므로, 중간상태를 따로 저장하지 않아도 요청-응답 맥락이 이어지는 구조

요청과 응답은 MonoFlux라는 리액티브 타입을 통해 흐름이 계속 이어져있고,
그 흐름 안에 있는 람다나 콜백 체인 (then, flapMap, map 등)을 통해 context를 유지하고 있다.

예제

1
2
3
4
5
6
7
8
9
10
11
public Mono<String> getUserInfo(String userId) {
    return webClient.get()
        .uri("http://external-api.com/user/" + userId)
        .retrieve()
        .bodyToMono(String.class)
        .map(response -> {
            // 이 response는 바로 위에서 요청한 userId에 대한 결과
            log.info("userId: {}, response: {}", userId, response);
            return "유저 정보: " + response;
        });
}

✅ Servlet vs Reactor

동기 블로킹 방식이냐, 비동기 논블로킹 방식이냐 ?

Servlet은 요청마다 쓰레드를 고정 배치하는 방식, Reactor는 이벤트로 등록해 쓰레드를 비워두는 방식이다.
MVC는 직관적이지만 확장성이 떨어지고, WebFlux는 추상적이지만 고성능에 유리하다.

항목 Servlet (Spring MVC) Reactor (Spring WebFlux)
기반 모델 동기 + 블로킹 비동기 + 논블로킹
기반 스펙 Java Servlet API (javax.servlet) Reactive Streams + Project Reactor
IO 처리 방식 요청마다 하나의 쓰레드 이벤트 루프 기반 (Netty 등)
대표 컨테이너 Tomcat, Jetty (Servlet 지원) Netty (논블로킹 지원)
요청 처리 흐름 쓰레드가 요청~응답까지 전부 책임 요청 이벤트 등록 후 쓰레드는 반환, 응답 시점에 다시 처리
리턴 타입 String, ResponseEntity 등 (즉시 리턴) Mono, Flux (지연 평가)

🔍 Servlet 기반 (Spring MVC)

▶️ 특징

  • 요청이 들어오면 하나의 쓰레드가 요청을 끝까지 처리
  • IO 작업(DB 조회, API 호출)이 느리면 그 쓰레드는 계속 기다림 → 블로킹
  • 일반적인 웹 애플리케이션에는 충분히 적합하고, 디버깅도 쉽다

▶️ 예시 흐름

1
[요청] --> DispatcherServlet --> Controller --> Service --> DAO --> DB 응답 대기 --> 결과 응답

▶️ 장점

  • 기존 생태계와 호환성 좋음 (Spring Security, AOP 등)
  • 학습 난이도 낮음

▶️ 단점

  • 요청마다 쓰레드 1개씩 필요 → 동시 요청 증가 시 OutOfThread 가능
  • 비효율적인 리소스 사용 (특히 IO 병목 심한 경우)

🚀 Reactor 기반 (Spring WebFlux)

▶️ 특징

  • 요청이 들어오면 이벤트로 등록 후 쓰레드를 반환
  • Mono / Flux를 사용해 작업 흐름을 연결 (비동기 체인)
  • Netty처럼 논블로킹 네트워크 처리 가능한 컨테이너 사용

▶️ 예시 흐름

1
2
3
[요청] --> 이벤트 등록 --> Netty 이벤트 루프에서 대기
           ↓
     외부 API 응답 도착 → map/flatMap 체인 실행 → 응답 전송

▶️ 장점

  • 쓰레드 수를 줄이면서도 많은 요청 처리 가능 (High scalability)
  • 지연 평가로 효율적인 리소스 사용
  • 이벤트 기반이므로 고성능 시스템에 적합

▶️ 단점

  • 디버깅 어렵고 StackTrace가 끊김
  • 기존 블로킹 라이브러리와 호환 어려움
  • 학습 난이도 높음