Java에서 비동기 프로그래밍은 높은 성능과 확장성을 필요로 하는 애플리케이션에서 필수적인 요소입니다. 비동기 처리를 통해 I/O 작업을 효율적으로 수행하고, 응답 시간을 단축할 수 있습니다. Java는 CompletableFuture
와 Reactive Streams
를 활용하여 비동기 프로그래밍을 효과적으로 구현하는 방법에 대해 알아 보겠습니다.
1. Java에서 비동기 프로그래밍이 필요한 이유
1) 블로킹(Blocking) vs. 비블로킹(Non-Blocking)
- 블로킹 방식: 하나의 작업이 완료될 때까지 다음 작업이 대기합니다. (예:
Thread.sleep()
사용) - 비블로킹 방식: 작업이 대기하지 않고, 완료되면 콜백을 통해 처리됩니다. (예:
CompletableFuture
)
2) 비동기 프로그래밍의 장점
- CPU 활용 극대화: 여러 작업을 병렬로 실행 가능하게 합니다.
- 응답 속도 향상: I/O 작업을 효율적으로 처리합니다.
- 리소스 절약: 필요할 때만 리소스를 사용하여 최적화 시킵니다.
Java는 CompletableFuture
와 Reactive 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의 주요 개념:
- Publisher: 데이터를 생성하는 역할을 합니다.
- Subscriber: 데이터를 소비하는 역할을 합니다.
- 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
는 대규모 데이터 스트림 처리에 유용하니 이 방법을 배워 효율적인 프로그램을 개발해 보시길 바랍니다.