이전 포스팅에선, 동기-블로킹 구조와, 동기-블로킹 구조의 대표적인 예시인 스레드 풀에 대해서 살펴보면서 동기-블로킹 구조의 단점과 한계를 알아보았습니다. 이번 포스팅에서는 동기-블로킹 구조의 단점과 한계를 극복하여 서버의 자원을 효율적으로 사용하면서도, 높은 확장성과 높은 동시성을 구성할 수 있는 비동기-논블로킹 구조와, 리액티브 프로그래밍에 대해 알아보겠습니다.
1990년대 말에는 웹 서버가 1만 개의 동시 연결("C10K 문제")을 감당하는 것이 매우 어려운 일이었습니다. 이 문제를 해결하기 위해 여러 접근법이 제시되었고, 그 중 하나가 비동기-논블로킹 구조입니다. Apache HTTP Server는 기존에 각 요청마다 스레드를 생성하는 방식으로 동작했으나, 이러한 방식은 동시 연결 수가 많아질수록 많은 메모리와 스레드 컨텍스트 스위칭 오버헤드를 발생시켰습니다.
반면, NGINX는 비동기-논블로킹 구조를 도입하여 C10K 문제를 효율적으로 해결했습니다. 이벤트 루프를 기반으로 동작하는 NGINX는 요청을 처리하는 동안 블로킹되지 않으며, 적은 수의 스레드로도 많은 요청을 처리할 수 있습니다. 이처럼 비동기-논블로킹 구조는 동시성 처리의 효율성을 크게 향상시킬 수 있으며, 이러한 접근 방식이 현재 웹 서버 및 다양한 애플리케이션에서 널리 사용되고 있습니다.
리액티브 프로그래밍은 데이터를 이벤트 스트림 형태로 취급하며, 이러한 스트림을 비동기적으로 처리하는 프로그래밍 패러다임입니다. 리액티브 프로그래밍의 핵심 개념은 구독-발행(Publish-Subscribe) 패턴과 배압(Backpressure)입니다.
구독-발행 패턴에서는 데이터의 생산자(발행자)와 소비자(구독자)가 독립적으로 동작하며, 소비자는 데이터의 스트림을 구독함으로써 데이터가 도착할 때마다 알림을 받게 됩니다. 이 방식은 생산자와 소비자의 결합도를 낮춰 유지보수를 용이하게 하며, 시스템의 확장성 또한 높이는 장점이 있습니다.
배압은 소비자가 처리할 수 있는 데이터의 양을 제어하는 메커니즘입니다. 생산자가 너무 많은 데이터를 빠르게 발행할 경우, 소비자가 이를 모두 처리하지 못해 시스템 과부하가 발생할 수 있습니다. 배압은 이러한 상황을 방지하기 위해 생산자가 데이터를 적절히 발행하도록 제어합니다.
Java 9부터 도입된 Flow API는 리액티브 스트림의 표준 인터페이스를 제공합니다. 이 API를 사용하여 비동기 데이터 스트림을 처리할 수 있습니다. 또한, Reactor는 리액티브 프로그래밍을 위한 Java 라이브러리로, Spring WebFlux와 함께 사용되며 다양한 기능을 제공합니다.
Flow API나 Reactor(혹은 RxJava)에서는 리액티브 프로그래밍을 정의하기 위해 Publisher, Subscriber, Subscription, Processor 등의 주요 인터페이스를 사용합니다. Publisher는 데이터를 발행하고, Subscriber는 데이터를 소비합니다. Subscription은 데이터 흐름을 제어하는 역할을 하며, Processor는 Publisher와 Subscriber의 역할을 동시에 수행하는 요소입니다.
Java Flow API를 사용하여 리액티브 프로그래밍을 구현할 때의 기본적인 코드 구조는 다음과 같습니다.
import java.util.concurrent.Flow;
import java.util.concurrent.SubmissionPublisher;
public class FlowExample {
public static void main(String[] args) {
SubmissionPublisher<String> publisher = new SubmissionPublisher<>();
Flow.Subscriber<String> subscriber = new Flow.Subscriber<>() {
private Flow.Subscription subscription;
@Override
public void onSubscribe(Flow.Subscription subscription) {
this.subscription = subscription;
subscription.request(1);
}
@Override
public void onNext(String item) {
System.out.println("Received: " + item);
subscription.request(1);
}
@Override
public void onError(Throwable throwable) {
throwable.printStackTrace();
}
@Override
public void onComplete() {
System.out.println("Done");
}
};
publisher.subscribe(subscriber);
publisher.submit("Hello, Flow API!");
publisher.close();
}
}
위 코드에서는 SubmissionPublisher를 통해 데이터를 발행하고, Subscriber가 이를 구독하여 처리합니다. onSubscribe, onNext, onError, onComplete 메서드를 통해 리액티브 데이터 흐름을 제어하는 기본적인 구조를 확인할 수 있습니다.
Spring WebFlux는 리액티브 프로그래밍을 지원하는 Spring Framework의 모듈로, Java Flow API와 Reactor Project에 기반을 두고 있습니다. WebFlux는 이러한 리액티브 라이브러리를 활용하여 비동기-논블로킹 서버를 쉽게 개발할 수 있도록 해줍니다.
이번 포스팅에서는 비동기-논블로킹 구조의 필요성과 리액티브 프로그래밍의 장점을 살펴보았으며, 자바 진영의 리액티브 스트림의 표준 인터페이스인 Java Flow API에 대해서도 간략히 살펴보았습니다. 다음 포스팅에서는 이를 기반으로 Spring Webflux에 대해 알아보겠습니다.