Java의 비동기 처리인 CompletableFuture와 Reactive Streams에 대해 알아 보겠습니다.

Java에서 비동기 프로그래밍은 높은 성능과 확장성을 필요로 하는 애플리케이션에서 필수적인 요소입니다. 비동기 처리를 통해 I/O 작업을 효율적으로 수행하고, 응답 시간을 단축할 수 있습니다. Java는 CompletableFutureReactive Streams를 활용하여 비동기 프로그래밍을 효과적으로 구현하는 방법에 대해 알아 보겠습니다.

1. Java에서 비동기 프로그래밍이 필요한 이유

1) 블로킹(Blocking) vs. 비블로킹(Non-Blocking)

  • 블로킹 방식: 하나의 작업이 완료될 때까지 다음 작업이 대기합니다. (예: Thread.sleep() 사용)
  • 비블로킹 방식: 작업이 대기하지 않고, 완료되면 콜백을 통해 처리됩니다. (예: CompletableFuture)

2) 비동기 프로그래밍의 장점

  • CPU 활용 극대화: 여러 작업을 병렬로 실행 가능하게 합니다.
  • 응답 속도 향상: I/O 작업을 효율적으로 처리합니다.
  • 리소스 절약: 필요할 때만 리소스를 사용하여 최적화 시킵니다.

Java는 CompletableFutureReactive Streams를 활용하여 이러한 문제를 해결합니다.


2. CompletableFuture를 이용한 비동기 처리

1) 기본적인 CompletableFuture 사용법

import java.util.concurrent.CompletableFuture;

public class CompletableFutureExample {
    public static void main(String[] args) {
        CompletableFuture.supplyAsync(() -> {
            return "Hello, CompletableFuture!";
        }).thenAccept(System.out::println);
    }
}
  • supplyAsync()는 별도의 스레드에서 실행합니다.
  • thenAccept()를 사용해 결과가 준비되면 실행합니다.

2) 여러 작업을 병렬 실행하기

import java.util.concurrent.CompletableFuture;

public class ParallelTasks {
    public static void main(String[] args) {
        CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> "Task 1 완료");
        CompletableFuture<String> task2 = CompletableFuture.supplyAsync(() -> "Task 2 완료");
        
        task1.thenCombine(task2, (result1, result2) -> result1 + " & " + result2)
             .thenAccept(System.out::println);
    }
}
  • thenCombine()을 사용하여 두 개의 작업을 동시에 실행하고 결과를 조합을 합니다.

3) 예외 처리

CompletableFuture.supplyAsync(() -> {
    if (Math.random() > 0.5) throw new RuntimeException("에러 발생");
    return "성공";
}).exceptionally(ex -> "예외 발생: " + ex.getMessage())
  .thenAccept(System.out::println);
  • exceptionally()를 활용하여 예외 발생 시 대체 값 제공합니다.

3. Reactive Streams를 이용한 비동기 처리

1) Reactive Streams란?

Reactive Streams는 비동기 데이터 흐름을 처리하는 표준 API입니다. 주요 구현체로는 Project Reactor(Spring WebFlux), RxJava 등이 있습니다.

Reactive Streams의 주요 개념:

  1. Publisher: 데이터를 생성하는 역할을 합니다.
  2. Subscriber: 데이터를 소비하는 역할을 합니다.
  3. Processor: 데이터를 가공하는 역할을 합니다.

2) Reactor를 활용한 비동기 스트림 처리

import reactor.core.publisher.Flux;

public class ReactiveExample {
    public static void main(String[] args) {
        Flux.just("데이터 1", "데이터 2", "데이터 3")
            .map(String::toUpperCase)
            .subscribe(System.out::println);
    }
}
  • Flux.just()는 여러 개의 데이터를 스트림으로 생성합니다.
  • map()을 통해 데이터를 변환 가능합니다.
  • subscribe()를 호출해야 데이터가 전송됩니다.

3) 비동기 데이터 스트림 결합하기

import reactor.core.publisher.Mono;

public class ReactiveCombine {
    public static void main(String[] args) {
        Mono<String> task1 = Mono.just("Task 1 완료");
        Mono<String> task2 = Mono.just("Task 2 완료");
        
        task1.zipWith(task2, (result1, result2) -> result1 + " & " + result2)
             .subscribe(System.out::println);
    }
}
  • Mono.zipWith()를 사용하여 두 개의 작업을 병렬로 실행하고 결과를 결합합니다.

4) 백프레셔(Backpressure) 처리

Reactive Streams는 과도한 데이터 흐름을 조절하는 백프레셔(Backpressure) 개념을 지원합니다.

Flux.range(1, 1000)
    .onBackpressureDrop()
    .subscribe(System.out::println);
  • onBackpressureDrop()을 사용하면 처리할 수 없는 데이터는 자동으로 폐기됩니다.

4. CompletableFuture vs. Reactive Streams 비교

기능 CompletableFuture Reactive Streams
동시 실행 O O
데이터 스트림 처리 X O
콜백 기반 O O
백프레셔 지원 X O
사용 용도 단일 작업 비동기 처리 대량 데이터 흐름 처리
  • CompletableFuture는 단일 또는 병렬 작업을 수행할 때 적합합니다.
  • Reactive Streams는 대량의 데이터를 실시간으로 처리할 때 유용합니다.

결론

Java의 비동기 처리인 CompletableFuture와 Reactive Streams에 대해 알아 보겠습니다. Java의 비동기 프로그래밍은 성능 최적화와 응답 시간 단축에 필수적인 요소이며,  CompletableFuture는 단일 작업의 비동기 실행에 적합하며, Reactive Streams는 대규모 데이터 스트림 처리에 유용하니 이 방법을 배워 효율적인 프로그램을 개발해 보시길 바랍니다.

Leave a Comment