1. 소개
- 현대의 소프트웨어 시스템은 점점 더 많은 동시 사용자와 데이터를 처리해야 합니다.
- 전통적인 동기-블로킹 방식의 프로그래밍은 이러한 요구사항을 감당하기 어려워졌고, 이는 비동기-논블로킹 프로그래밍의 필요성으로 이어졌습니다.
1.1 학습 목표
- 이 문서를 통해 다음을 이해할 수 있습니다
- 비동기-논블로킹 프로그래밍의 기반이 되는 각 계층의 핵심 개념
- 하위 계층의 기술이 상위 계층에서 어떻게 추상화되고 발전하는지
- 네트워크 프로토콜부터 웹 애플리케이션까지 전체 스택의 유기적 관계
1.2 왜 비동기-논블로킹인가?
- 전통적인 동기-블로킹 방식의 한계
- 요청당 하나의 스레드 필요
- 스레드 생성과 컨텍스트 스위칭 비용
- 메모리 사용량 증가
- 확장성 제한
- 비동기-논블로킹 방식의 이점:
- 적은 수의 스레드로 많은 요청 처리
- 시스템 리소스 효율적 사용
- 높은 처리량과 확장성
- 반응성 향상
- 이제 이러한 비동기-논블로킹 아키텍처가 어떻게 발전해왔는지, 가장 기본이 되는 네트워크 프로토콜부터 살펴보겠습니다.
2. 신뢰성 있는 데이터 전송의 시작: TCP
- TCP는 신뢰성 있는 데이터 전송을 보장하는 프로토콜입니다.
2.1 연결 생명주기
- 연결 수립 (3-way Handshake)
- SYN → SYN+ACK → ACK
- 초기 시퀀스 번호 동기화
- 양방향 통신 준비
- 데이터 전송
- 시퀀스 번호로 순서 보장
- ACK를 통한 신뢰성 확보
- 윈도우 사이즈로 흐름 제어
- 정상 종료 (4-way Handshake)
- FIN → ACK → FIN → ACK
- TIME_WAIT 상태로 안전한 종료
2.2 TCP 비정상 종료
- TCP 연결이 정상적인 4-way Handshake 과정을 거치지 않고 종료되는 경우를 비정상 종료라고 합니다.
- 비정상 종료는 크게 두 가지 형태로 발생합니다
- RST 패킷을 통한 강제 종료: 연결 종료 시 상대방에게 RST 패킷을 전송하여 즉시 연결을 끊는 방식
- 묵시적 비정상 종료: 물리적 연결 단절이나 시스템 장애로 인해 아무런 종료 신호 없이 연결이 끊어지는 경우
- 묵시적 비정상 종료를 감지하고 대응하기 위해 다음과 같은 방법을 사용합니다
- TCP Keep-Alive: TCP 프로토콜 수준에서 연결 상태를 주기적으로 확인
- Application Layer Heartbeat: 응용 프로그램 수준에서 주기적으로 상태 확인 메시지를 교환
2. OS 레벨의 진화: I/O 모델과 멀티플렉싱
2.1 I/O 모델의 발전
IO Models에서 설명하는 네 가지 I/O 모델의 진화:
- 블로킹 I/O
- 가장 기본적인 모델
- 작업 완료까지 스레드 대기
- 리소스 활용의 비효율성
- 논블로킹 I/O
- 즉시 제어권 반환
- 지속적인 상태 확인 필요
- CPU 자원 낭비 문제
- IO Multifexing
- 하나의 스레드로 여러 I/O 감시
- select/poll의 등장과 한계
- epoll을 통한 성능 개선
- Asynchronous IO
- 완전한 비동기 처리
- Linux의 io_uring 시스템 콜
- 커널이 I/O 완료 통지
- 가장 효율적인 리소스 활용
2.2 Socket과 File Descriptor
- OS Socket에서 설명하는 것처럼:
- 소켓은 네트워크 통신의 엔드포인트
- 파일 디스크립터를 통한 일관된 인터페이스
- "모든 것은 파일이다" 철학의 구현
- 이러한 OS 레벨의 발전은 상위 계층에서 다음과 같은 가능성을 열었습니다:
- 효율적인 리소스 활용
- 확장 가능한 서버 구현
- 이벤트 기반 프로그래밍 모델
3. Java에서의 구현: IO에서 NIO로
3.1 전통적인 블로킹 I/O
- Java IO 패키지는 전통적인 블로킹 I/O를 구현하는데 사용됩니다.
- Java IO를 황용한 TCP Socket Programming.
- Java IO 패키지를 사용해 블로킹 TCP 서버/클라이언트를 구현합니다.
- 블로킹 서버의 한계를 설명합니다.
- 멀티 스레드 방식을 사용해 다중 접속 서버를 구현합니다.
3.2 블로킹 I/O의 한계
- 다중 접속 서버의 구현
- 블로킹 TCP 서버의 한계
- 다수의 클라이언트가 연결하는 경우에는 문제가 발생합니다.
- 처음 연결한 클라이언트가 연결을 종료하기 전까지는 다른 클라이언트의 연결은 listen 큐에 들어가 대기해야 합니다.
- 따라서 다수의 요청을 처리할 수 없다는 문제가 있습니다.
- 이러한 문제를 해결하기 위해 다음과 같은 방법이 제안되었습니다
- 멀티 프로세싱: fork() 함수를 사용해 자식 프로세스를 생성하는 방식
- 멀티 스레딩: 스레드를 사용해 다수의 클라이언트 요청을 처리하는 방식
- I/O 멀티플렉싱: select() 함수를 사용해 다수의 소켓을 모니터링하고, 이벤트가 발생한 소켓을 처리하는 방식
3.3 NIO의 혁신
- Java NIO가 가져온 변화:
- Channel과 Buffer
- 양방향 데이터 전송
- 효율적인 메모리 사용
- 직접/비직접 버퍼 지원
- Selector
- OS의 I/O 멀티플렉싱 추상화
- 이벤트 기반 프로그래밍 지원
- 단일 스레드로 다중 연결 처리
- 이러한 Java NIO의 기능은 상위 계층에서 다음과 같이 활용됩니다
- Netty의 이벤트 루프 구현
- 리액티브 스트림의 비동기 처리
- 고성능 네트워크 애플리케이션 개발
4. 이벤트 기반 아키텍처의 등장: Reactor 패턴과 Netty
4.1 Reactor 패턴
- Reactor 패턴은 동시에 들어오는 여러 종류의 이벤트를 처리하기 위한 동시성을 다루는 디자인 패턴입니다.
- 이벤트 기반 아키텍처의 근간이 되는 패턴으로, 동시에 들어오는 여러 이벤트를 효율적으로 처리하기 위한 구조를 제공합니다
4.1.1 주요 구성 요소
- Reactor (이벤트 루프)
- 무한 반복문을 실행해 이벤트가 발생할 때까지 대기
- 이벤트 발생 시 적절한 핸들러에게 디스패치
- 모든 이벤트 처리의 중심 역할 수행
- Handler
- 디스패치된 이벤트를 받아서 처리
- 실제 비즈니스 로직 수행
- 각 이벤트 타입에 맞는 처리 로직 구현
4.1.2 동작 방식
- 이벤트 루프는 다음 단계를 반복적으로 수행합니다:
- 이벤트 발생 대기
- 이벤트 발생 시 적절한 핸들러로 디스패치
- 핸들러에서 이벤트 처리
- 다시 1단계로 돌아가 반복
4.2 Reactor 패턴과 Netty의 만남
- Reactor 패턴은 Java NIO의 진화와 만나 Netty라는 강력한 프레임워크로 발전했습니다.
- Netty는 Reactor 패턴의 개념을 실전적으로 구현하면서 여러 요소들을 추가했습니다.
5 고성능 네트워크 프레임워크: Netty
- Reactor 패턴을 기반으로 한 고성능 이벤트 처리 비동기-논블로킹 네트워크 프레임워크
- Netty Introduction에서 설명하는:
- NIO의 복잡성 추상화
- 사용하기 쉬운 API 제공
- 높은 성능과 안정성
- EventLoop의 핵심 역할
- EventLoop는 Reactor 패턴의 이벤트 루프를 구현
- 각 Channel은 전용 EventLoop에 할당
- 효율적인 스레드 관리
- 작업 스케줄링
- Components의 주요 구성
- EventLoop
- 이벤트 처리의 핵심
- 스레드 관리와 작업 실행
- Channel
- 네트워크 연결 추상화
- 이벤트 파이프라인 제공
- ChannelHandler
- 데이터 처리 로직
- 체인 형태의 구성
- EventLoop
6. 비동기 스트림 처리의 표준화: Reactive Streams
- ReactiveStream의 핵심 개념
- 비동기 스트림 처리 표준
- 배압(Backpressure) 메커니즘
- Publisher-Subscriber 모델
- Backpressure
- 기존 옵저버 패턴(Push 방식)의 한계에 대해서 설명합니다.
- Push 방식에서 Pull 방식으로의 전환을 통한 데이터 흐름 제어 대해서 설명합니다.
7. Reactive Streams의 구현: Project Reactor
- Project Reactor는 현대적인 애플리케이션이 필요로 하는 고성능, 비동기, 논블로킹 프로그래밍을 지원하는 Java 라이브러리입니다.
- 리액티브 프로그래밍을 위한 다양한 기능을 제공하여, 더 효율적이고 탄력적인 시스템을 구축할 수 있게 해줍니다.
7.1 백프레셔 구현
- Backpressure
- Project Reactor에서의 백프레셔 구현 방식에 대해서 설명합니다.
- Publisher와 Subscriber 간의 데이터 흐름과 흐름 제어의 필요성에 대해서 설명합니다.
- 주요한 4가지 백프레셔 전략에 대해서 설명합니다.
- ERROR: 초과 시 오류
- DROP: 초과 데이터 폐기
- LATEST: 최신 데이터 유지
- BUFFER: 버퍼링 처리
7.2 효율적인 스케줄링
- Scheduler
- Project Reactor에서 제공하는 스케줄러의 종류에 대해서 설명합니다.
- 스케줄러를 사용하는 이유에 대해서 설명합니다.
- 각각의 스케줄러의 특성과 사용 목적에 대해서 설명합니다.
- 스케줄러 오퍼레이터를 사용하는 방법에 대해서 설명합니다.
8. 웹 애플리케이션으로의 통합: Spring WebFlux
- SpringWebflux
- 비동기-논블로킹 웹 스택
- Reactor 기반 구현
- 높은 확장성 제공
9. 계층 간 통합의 의미
9.1 수직적 통합
TCP (신뢰성 있는 전송)
↓
OS I/O 모델 (효율적인 I/O 처리)
↓
Java NIO (추상화된 비동기 I/O)
↓
Netty (이벤트 기반 네트워킹)
↓
Reactive Streams (표준화된 비동기 스트림)
↓
Project Reactor (실용적인 구현)
↓
Spring WebFlux (웹 애플리케이션 통합)