CS Repo/프로그래밍 패러다임

스프링 + 비동기 프로그래밍

조금씩 차근차근 2025. 3. 9. 22:05

비동기 프로그래밍은 시스템의 처리 효율을 높이고 블로킹을 최소화하기 위한 핵심 기법이다. 동기 호출로 인한 블로킹 문제를 해결하기 위해 이벤트 드리븐 아키텍처와 같은 패러다임이 채택되며, 이를 통해 처리 흐름을 비동기로 전환하고, 데이터 흐름 제어(backpressure) 및 에러 처리를 체계적으로 관리할 수 있다.

이벤트 드리븐 아키텍처

이벤트 드리븐 아키텍처란, 상태의 변화가 직접적인 메소드 호출로 이루어지지 않고, 자신의 상태 변화를 외부에 알리는데 중점을 두는 아키텍처 형태를 의미한다.

  • I/O 처리와 zip 요청
    I/O 작업 시 상류 스트림에서 zip 요청을 받는다. 내부에서 동기 호출이 이루어지면 블로킹이 발생하므로, 비동기 요청을 일관되게 사용하는 것이 바람직하다.
  • 이벤트와 콜백 메커니즘
    이벤트나 콜백을 통해 작업 완료를 알 수 있다. 상류 스트림에서는 backpressure를 적용한 Flux를 통해 데이터 요청을 관리하며, zip을 통해 Mono로 데이터를 수신한다.

Webflux

복잡한 스트림 처리를 위해 Webflux를 활용할 수 있다. Webflux는 아래와 같은 기본 구조와 스레드 모델을 갖는다.

Webflux 스레드의 기본 구조. NIO를 기반으로 동작한다.

  • 기본 구조
    • 하나의 스레드는 하나의 이벤트 루프로 동작한다.
    • 하나의 이벤트 루프는 하나의 selector를 사용하며, 하나의 selector는 여러 개의 채널을 관리한다.
  • 구조 구성
    • Boss 스레드/이벤트 루프: 요청을 수신하며 단일 스레드(이벤트 루프)를 사용한다.
    • Worker 스레드/이벤트 루프: 실제 요청을 처리하며, 각자 고유한 selector를 가지고 등록된 채널들의 I/O 이벤트를 감시한다. 요청 배분은 주로 라운드 로빈 방식이 사용되며, 부하 기반 방식은 잘 사용되지 않는다.
    • Netty 스레드 모델: 소수의 스레드로 많은 연결을 처리하여 스레드 생성 및 전환 비용을 줄인다.
  • 주요 특징
    • 클라이언트와 서버 양 측의 비동기 통신을 위한 스레드 모델과 동작 방식을 제공한다.
    • Event Driven Architecture를 기반으로 Hot/Cold Sequence와 Marble Diagram을 통해 아키텍처를 시각적으로 이해할 수 있다.
    • 외부 상태에 의존하지 않으며, backpressure, Mono, Flux와 같은 Reactor 컨테이너를 활용하여 데이터 스트림을 관리한다.
    • 예외 처리 방식이 독특하여, onErrorResume, onErrorReturn, onErrorMap을 통해 예외 상황에 따른 대체 처리 및 예외 변환이 가능하다.

zip

  • zip은 여러 개의 출력을 하나의 입력으로 결합하기 위한 도구이다. 주로 Mono 사용 시 여러 입력을 모두 수집할 때까지 대기한 후 하나의 결과로 결합하는 데 사용된다.

backpressure

  • backpressure는 생산자-소비자 문제를 해결하기 위한 매커니즘이다.
  • 문제 상황
    생산자가 너무 빠르게 데이터를 생성하면 소비자가 처리 가능한 속도를 초과하여 메모리 과부하나 시스템 다운 등의 문제가 발생할 수 있다.
  • 해결 방법
    소비자는 생산자에게 데이터 전송 속도를 조절하거나 일정량만 전송하도록 신호를 보낸다. 이를 통해 생산자는 소비자가 처리 가능한 양만큼 데이터를 전송하여 전체 시스템의 안정성을 보장한다.
  • 구현 방식
    리액티브 스트림에서는 Subscription.request(n) 메서드를 사용하여 소비자가 원하는 데이터 개수를 명시적으로 요청하고, 생산자는 이에 맞춰 데이터를 전송한다.

CompletableFuture + DeferredResult vs WebFlux

비동기 처리 방식은 단순한 스트림과 복잡한 스트림으로 구분할 수 있다.

  • 단순한 스트림
    • 특징: 단일 요청에 대해 단일 결과 또는 소량의 결과를 비동기적으로 처리하며, 복잡한 데이터 변환이나 결합 없이 작업 완료 후 결과를 전달한다.
    • 예시: 외부 API로부터 데이터를 받아 클라이언트에 전달하거나, 긴 작업을 비동기로 처리한 후 단일 결과를 반환하는 경우.
  • 복잡한 스트림
    • 특징: 여러 단계의 데이터 처리 파이프라인이 필요하며, 데이터의 변환, 필터링, 결합, 분기, 에러 처리와 backpressure 같은 리액티브 스트림의 핵심 기능을 활용한다.
    • 예시: 실시간 데이터 스트리밍 서비스에서 연속적으로 들어오는 데이터를 가공 및 결합하거나, 여러 소스의 데이터를 결합하고 동기화하는 복잡한 이벤트 처리 파이프라인.
  • 동기 코드와의 통합
    CompletableFuture와 DeferredResult를 동기 코드와 결합한 후 Mono로 변환할 수 있다. 또한, 스프링 5부터는 MVC에서도 Flux와 Mono 반환이 가능하여 간단한 비동기 처리가 지원된다.
  • 예외 처리
    비동기 스레드에서 동작하기 때문에 MVC의 Controller Advice가 예외를 포착하지 못할 수 있다. 이 경우, WebFlux 전용의 글로벌 WebExceptionHandler를 사용하거나 별도로 예외 처리를 지정하여 해결해야 한다.

주의할 점: Spring Web MVC + Webflux

Webflux와 Web MVC를 함께 사용할 경우에는 Netty가 기동하지 않으며, reactor와 DeferredResult가 동일하게 동작한다. 서블릿 3.1의 비동기 API를 활용하여, 요청 시 AsyncContext를 오픈한 후 핸들러 로직이 종료된 것처럼 스레드를 반환하고, 내부 콜백(Mono.fromSupplier 등)이 완료되거나 onNext/onComplete 신호가 오면 서블릿 컨테이너가 스레드 풀에서 스레드를 할당해 응답을 마무리한다.

서블릿 3.1 비동기 흐름은 다음과 같다.

  1. 요청이 들어온 시점에 스레드를 할당받는다.
  2. 비동기로 전환(asyncStarted)한다.
  3. 스레드를 반환한다.
  4. 작업 완료 시 콜백 스레드가 응답을 전송한다.

이와 같이 비동기 프로그래밍과 Webflux의 구조는 시스템 자원의 효율적 사용과 높은 확장성을 보장한다. 각 구성 요소와 메커니즘의 역할을 명확히 이해하고 적절히 활용함으로써, 복잡한 데이터 스트림 처리 및 예외 상황에 대한 견고한 대처가 가능하다.