유성

WebFlux 의 중심, Netty 이벤트 루프는 누가 깨우는가? 본문

Architecture

WebFlux 의 중심, Netty 이벤트 루프는 누가 깨우는가?

백엔드 유성 2026. 1. 3. 18:14

이 글에서는 WebFlux, 정확히 말하면 Netty의 이벤트 루프 스레드가 어떤 메커니즘으로 구동되고 제어되는지 그 근본적인 원리를 알아본다.

 

1. 이벤트 루프의 주도권은 OS 커널에 있다.

당연한 이야기지만, 이벤트 루프를 깨워 일을 시키는 실질적인 주체는 OS 커널이다.

사용자가 WebFlux 서버에 요청을 보냈을 때, 데이터가 네트워크 카드에서 이벤트 루프까지 도달하는 과정을 시간 순서대로 살펴보자.

 

데이터 유입과 소켓 디스크립터의 생성

먼저 사용자의 요청은 패킷에 담겨 네트워크를 통해 서버의 네트워크 카드(NIC)에 도달한다.

이때 NIC와 CPU(커널)는 협력하여 이 전기 신호를 정리하고 조합하여 '소켓 디스크립터(Socket Descriptor)'를 생성한다.

 

소켓 디스크립터란? 연결 정보(IP, Port, Protocol), 송수신 데이터 버퍼, 그리고 TCP 상태 정보 등을 커널이 관리하기 쉽게 추상화하여 번호를 매긴 인터페이스(핸들)이다. 프로그램은 이 번호표(FD)만 가지고 복잡한 네트워크 스택에 접근할 수 있다.

 

수동적 대기와 커널의 통지 (Signaling)

소켓 디스크립터가 준비되면 OS 커널은 이를 JVM에게 전달한다. 여기서 핵심은 JVM이 데이터가 왔는지 끊임 없이 확인(Polling)하는 것이 아니라는 점이다.

 

이벤트 루프 스레드는 커널의 시스템 콜(epoll)을 호출한 뒤 잠시 잠들고, OS 커널이 JNI를 통해 "데이터 준비되었으니 일어나라"고 신호를 보낼 때만 깨어나느 수동적인 구조를 가진다.

 

이벤트 루프의 실행

커널의 신호를 받은 JVM은 잠들어 있던 이벤트 루프 스레드를 깨운다.

비로소 깨어난 이벤트 루프는 전달받은 소켓 디스크립터를 보고 Controller부터 로직을 조립한 후 조립이 완료됨과 동시에 실행하게 된다.

2. 비동기 I/O의 기다림: 누가 잠든 스레드를 깨우는가?

사용자의 요청을 받자마자 즉시 응답하는 경우는 드물다.

대부분의 비즈니스 로직은 RDBMS, Redis, 외부 API 호출 등 I/O 작업을 포함한다.

 

WebFlux는 이러한 I/O 요청을 보낸 뒤, 응답이 올 때까지 스레드를 점유하며 기다리지 않고 즉시 커널에 제어권을 반납한다.

 

이 과정은 앞서 살펴본 요청 수신 방식과 동일한 원리로 동작한다.

  1. I/O 요청과 예약: 스레드는 DB나 Redis에 TCP 요청을 보낸 뒤, 해당 연결에 할당된 '소켓 디스크립터'를 이벤트 루프(Selector)에 등록하고 떠난다.
  2. 커널의 감시와 통지(epoll): 외부 시스템으로부터 응답 패킷이 도착하면, OS 커널은 해당 소켓 디스크립터의 상태 변화를 감지하고 JNI를 통해 잠들어 있던 JVM 스레드(이벤트 루프 스레드)를 깨운다.
  3. 컨텍스트 재개: 잠에서 깬 이벤트 루프는 깨어난 원인(FD 번호)을 확인하고, 해당 소켓에 묶여 있던 다음 파이프라인을 호출하여 비즈니스 로직을 이어간다.

코드로 보는 파이프라인의 재개

@GetMapping
public Mono<Void> monoTest() {
  return aRepository.findById(1L)    // (1) I/O 요청 후 스레드 반납/잠듦
      .map(result -> Wrapping(result)) // (2) 소켓 신호 수신 후 이 지점부터 재개
      .then();     // (3) 모든 I/O 완료 후 최종 응답
}

 

위 코드에서 주석 처리된 부분은 물리적으로 끊겨 있는 지점이다.

하지만 소켓 디스크립터라는 '번호표'가 OS 커널과 JVM 사이의 징검다리 역할을 해주기 때문에, 스레드가 바뀌거나 잠들었다 깨어나더라도 마치 하나의 흐름처럼 다음 파이프라인을 정확히 찾아 수행할 수 있는 것이다.

 

3. 소켓 디스크립터가 없는 대기: delayElement는 어떻게 처리될까?

로직 중간에 delayElement(Duration.ofSeconds(5)) 같은 시간 지연이 발생하는 경우, 감시할 소켓 디스크립터(FD)가 존재하지 않는다.

하지만 이벤트 루프는 여전히 잠들고, 정확히 5초 뒤에 깨어난다. 그 비결은 무엇일까?

 

핵심은 wait() 대신 epoll_wait()를 사용한다는 점에 있다.

  1. 동일한 처리 인터페이스: 전통적인 방식이 wait()나 sleep()으로 스레드를 고립시킨다면, Netty의 이벤트 루프는 epoll_wait(..., timeout)을 호출한다.
  2. I/O와 시간의 통합: epoll_wait는 커널에게 "등록된 FD에 이벤트가 생기거나, 혹은 설정한 시간(timeout)이 지나면 나를 깨워줘"라고 요청하는 시스템 콜이다.
  3. 결론: 결국 커널 단에서 보면 네트워크 소켓 신호든, 5초간의 시간 대기든 모두 epoll이라는 동일한 메커니즘으로 처리된다.
@GetMapping
public Mono<Void> monoTest() {
    return aRepository.findById(1L)
            .delayElement(Duration.ofSeconds(5)) // (1) epoll_wait의 timeout 인자로 5초 예약
            .then();                            // (2) 5초 후 커널이 깨우면 다음 파이프라인 재개
}

 

이벤트 루프 입장에서는 깨어난 원인이 '데이터 도착'인지 '시간 만료'인지만 판단할 뿐, 잠들고 깨어나는 통로 자체는 동일하기 때문에 모든 비동기 흐름을 하나의 스레드에서 효율적으로 통합 관리할 수 있는 것이다.

 

4. 이벤트 루프 스레드를 동작(트리거)은 epoll로 통한다

조금 돌아온감이 없지않아 있는데, 결국 WebFlux와 Netty의 모든 동작을 관통하는 실체는 OS 커널의 epoll 시스템 콜 하나로 수렴된다.

이벤트 루프 스레드가 "움직인다"는 것은, 곧 커널이 epoll을 통해 스레드에게 실행 주도권을 넘겨주었음을 의미한다.

 

이 거대한 메커니즘을 코드로 표현하자면 다음과 같은 간결한 무한 루프로 요약할 수 있다.

while (true) {
    // 1. [잠듦] 커널 신호가 올 때까지 스레드가 '완전히' 잠든다.
    // 커널이 FD 이벤트나 타임아웃을 감지하면 비로소 이 메서드를 반환(return)시킨다.
    List<SelectionKey> events = selector.select(timeout); 

    // 2. [깨어남] 어떤 번호표(FD) 때문에 깨어났는지 확인한다.
    for (SelectionKey event : events) {
        
        // 3. [연결] 장부에서 해당 FD(또는 타이머)와 연결된 다음 로직(람다)을 꺼낸다.
        Runnable task = event.attachment(); 
        
        // 4. [실행] 조립된 파이프라인의 다음 단계를 수행한다.
        task.run(); 
    }
}

 

이 루프가 바로 고성능 비동기 서버의 엔진이다.

  • 효율성: 처리할 요청이 없을 때는 selector.select() 지점에서 멈춰 CPU 자원을 전혀 소모하지 않는다.
  • 통합 관리: 네트워크 패킷이 오든, 예약한 시간이 다 되었든 오직 커널이 던져주는 신호에 의해서만 깨어나 순차적으로 일을 처리한다.
  • 주의점: 만약 실행 단계에서 스레드를 멈추게(Blocking) 만들면 전체 무한 루프가 멈춰버려 서버 전체의 마비로 이어진다.

결국 WebFlux의 성능은 단순히 Non-blocking이라서가 아니라, 이처럼 OS 커널과 시스템 프로그래밍 레벨의 효율성을 극한까지 활용하는 구조에서 기인한다.

우리가 작성하는 모든 비동기 체인은 결국 이 단순하고 강력한 루프 안에서 실행될 '작은 업무 조각'들을 예약하는 행위인 것이다.

 

이 부분에 대해서 NIO 처리는 어떤 코드로 진행되는지 궁금하면 아래 글을 참고하자.

 

NIO(New I/O)와 Webflux 비동기 프로그래밍

Java의 New Input/Output을 사용해보며 대규모 처리에서 중요한 개념이라고 생각되어 이를 소개합니다. 1. NIO는 어떤것이고 왜 사용하는가NIO가 등장하기 이전에 사용된던 방식은 Blocking I/O입니다.글에

youseong.tistory.com