비동기 시스템은 A,B라는 2개의 Task가 작동한다고 가정했을 때 A의 일이 끝나던 말던 B의 일이 시작되는 시스템을 말한다.
이에 반해서 동기 시스템은 A라는 일이 끝난 후에 B의 일이 시작되는 시스템을 말한다.
그렇기에 MultiThread에 대해서 잘 알고 있어야 한다.
Thread에 대해서는 다음 글을 참조하자.
https://harmony-raccoon.tistory.com/81
[Java] MultiThread란?
MultiThread를 이해하기 위해서는 Process(프로세스)와 Thread(쓰레드)의 차이점 및 Concurrency(동시성)와 Parallelism(병렬성)의 차이점에 대해서 이해하고 있어야 한다.Process프로세스(Process)는 우리가 만든
harmony-raccoon.tistory.com
Java는 크게 Callback을 구현하는 방식과 Future를 사용하는 방식이 존재한다.
Callback
Callback이란 비동기 시스템 안에서 순서가 지켜져야 하는 코드 흐름의 순서를 보장하기 위한 디자인 패턴이다.
Callback을 구현하는 방식은 크게 CompletionHandler, 함수형 인터페이스 사용, Future 객체 사용하는 것 3가지이다.
함수형 인터페이스에 대해서는 다음 글을 참조하자.
https://harmony-raccoon.tistory.com/24
Java 람다식, 함수형 프로그래밍
함수형 프로그래밍(Functional Programming)이란?함수형 프로그래밍은 자료 처리를 수학적인 함수로 규정하고, 상태와 가변 데이터를 멀리하는 프로그래밍 패러다임을 말합니다. 이 프로그래밍 스타
harmony-raccoon.tistory.com
CompletionHandler
CompletionHandler의 인터페이스는 다음과 같다.
public interface CompletionHandler<V,A> {
/**
* Invoked when an operation has completed.
*
* @param result
* The result of the I/O operation.
* @param attachment
* The object attached to the I/O operation when it was initiated.
*/
void completed(V result, A attachment);
/**
* Invoked when an operation fails.
*
* @param exc
* The exception to indicate why the I/O operation failed
* @param attachment
* The object attached to the I/O operation when it was initiated.
*/
void failed(Throwable exc, A attachment);
}
2개의 추상 메서드가 정의되어있다.
Completed 메서드에 내가 진행하고자 하는 로직을 작성하고, 만약 앞서 진행되어야 했던 로직이 실패했다면 그 에러를 처리할 로직을 failed 메서드에 작성하면 된다.
public class Callback implements CompletionHandler<String, Void> {
@Override
public void completed(String result, Void attachment) {
print("Task 2 시작");
try {
Thread.sleep(1_000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
print("Task 2 종료");
}
@Override
public void failed(Throwable exc, Void attachment) {
print("Task 1 실패: " + exc.toString());
}
private void print(String content) {
System.out.println(Thread.currentThread().getName() + " - " + content);
}
}
public class CallbackEx1 {
public static void main(String[] args) {
Callback callback = new Callback();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.submit(() -> {
print("Task 1 시작");
try {
Thread.sleep(1_000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
print("Task 1 종료");
String result = "end";
callback.completed(result, null); // Task1이 끝난 후, Task2의 순서를 보장함.
});
// Task 3가 먼저 실행될 지, Task 1이 먼저 실행될 지 모른다.
print("Task 3 시작");
try {
Thread.sleep(1_000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
print("Task 3 종료");
executorService.shutdown();
}
private static void print(String content) {
System.out.println(Thread.currentThread().getName() + " - " + content);
}
}
이렇게 작성한 후 로직을 실행하면 결관는 다음과 같다.
Functional Interface
Consumer 함수형 인터페이스를 활용해서 Callback 비동기 시스템을 구현하면 다음과 같다.
public class CallbackEx2 {
public static ExecutorService executorService;
public static void main(String[] args) {
executorService = Executors.newCachedThreadPool();
callback(param -> {
print("Task 2 시작");
try {
Thread.sleep(1_000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
print("Task 2 종료");
});
// Task 3가 먼저 실행될 지, Task 1이 먼저 실행될 지 모른다.
print("Task 3 시작");
try {
Thread.sleep(1_000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
print("Task 3 종료");
executorService.shutdown();
}
public static void callback(Consumer<String> callback) {
executorService.submit(() -> {
print("Task 1 시작");
try {
Thread.sleep(1_000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
print("Task 1 종료");
String result = "end";
callback.accept(result); // Task1이 끝난 후, Task2의 순서를 보장함.
});
}
private static void print(String content) {
System.out.println(Thread.currentThread().getName() + " - " + content);
}
}
이렇게 작성한 후 로직을 실행하면 결관는 다음과 같다.
Future
Future라는 객체가 비동기로 실행된 로직의 결과물을 저장해온다.
비동기로 로직을 실행할 때, 필수적으로 필요한 결과물이 존재할 수 있다.
이때, 비동기로 동작하므로 여기서 얻은 결과물이 null일지 실제 결과물인지 알 수 없다.
따라서 Future는 get()메서드를 호출할 때, 동기적으로 작동하여 실제 결과물을 얻을 때까지 잠시 기다린다.
public class CallbackEx3 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newCachedThreadPool();
CallbackCallable cc = new CallbackCallable();
// 작업1 Callable이 리턴한 값을 future에 담는다.
Future<String> future = executorService.submit(cc);
print("Task 3 시작");
try {
Thread.sleep(1_000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(future.get());
print("Task 3 종료");
executorService.shutdown();
}
private static void print(String content) {
System.out.println(Thread.currentThread().getName() + " - " + content);
}
}
이렇게 Task가 종료되는 시점은 Task1의 결과물이 도착한 이후로 설정하고, 코드를 제작하였다.
public class CallbackCallable implements Callable<String> {
@Override
public String call() {
print("Task 1 시작");
try {
Thread.sleep(1_000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
print("Task 1 종료");
return Thread.currentThread().getName() + " - end";
}
private void print(String content) {
System.out.println(Thread.currentThread().getName() + " - " + content);
}
}
위의 테스트 코드를 실행하면 결과는 다음과 같다.
CompletableFuture
Future는 get()이라는 메서드를 호출한다면 메서드를 호출한 쓰레드는 Blocking(잠시 멈춤)상태로 들어가게 된다.
따라서 기약없는 기다림이(언제 자식 쓰레드의 로직이 끝나고 리턴값이 오는지 알 수 없음) 시작되어서 성능이 떨어진다.
이러한 문제를 해결하기 위해서 CompletableFuture라는 클래스가 Java8에서 도입되었다.
CompletableFuture에 대한 메서드는 다음과 같다.
비동기 작업 실행
- runAsync() : 반환값 없는 작업을 실행할 때 사용한다. 함수형 인터페이스인 Runnable를 인자로 받는다.
- supplyAsync() : 반환값이 있는 작업을 실행할 때 사용한다. 함수형 인터페이스인 Supplier를 인자로 받는다.
Callback(콜백) 등록
- thenApply(): 반환값을 받아서 다른 반환값을 리턴한다. 함수형 인터페이스인 Function를 인자로 받는다.
- thenAccept() : 반환값을 받아서 다른 반환값을 리턴하지 않는다. 함수형 인터페이스인 Comsumer를 인자로 받는다.
- whenComplete() : 작업이 완료된 후의 결과(반환값 or 예외)를 처리한다. 함수형 인터페이스인 BiComsumer를 인자로 받는다.
작업 조합
- thenCombine() : 2개의 비동기 작업 결과를 결합하여 데이터를 가공하고 리턴합니다.
- allOf() : 여러개의 비동기 작업이 모두 끝날 때까지 기다립니다.
- anyOf() : 여러개의 비동기 작업 중 하나라도 끝나면 후속 작업을 바로 시작합니다.
예외 처리
- exceptionally() : 비동기 작업 내에서 발행한 예외를 처리합니다.
- handle() : 비동기 작업 후 발생한 반환값 혹은 예외값을 처리할 수 있습니다.
외부에서 완료 가능
- complete() : 외부에서 강제로 비동기 작업을 완료시킵니다.
public class CallbackEx4 {
public static ExecutorService executorService;
public static void main(String[] args) {
executorService = Executors.newCachedThreadPool();
// CompletableFuture 생성
CompletableFuture<String> future = CompletableFuture.supplyAsync(CallbackEx4::task1, executorService);
// 작업이 완료되면 결과를 처리하는 콜백 등록
future.thenAccept(CallbackEx4::task2);
task3();
try {
future.get(); // 결과를 기다림
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
executorService.shutdown();
}
private static void print(String content) {
System.out.println(Thread.currentThread().getName() + " - " + content);
}
private static String task1() {
print("Task 1 시작");
try {
Thread.sleep(1_000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
print("Task 1 종료");
return "end";
}
private static void task2(String result) {
print("Task 2 시작");
try {
Thread.sleep(1_000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
print("Task 2 종료");
}
private static void task3() {
print("Task 3 시작");
try {
Thread.sleep(1_000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
print("Task 3 종료");
}
}
위의 테스트 코드를 실행하면 결과는 다음과 같다.
참고 자료
Java에서의 비동기 프로그래밍
동기? 비동기? 글을 시작하기에 앞서, 동기와 비동기가 무엇인지 간단하게 설명해 보자면 다음과 같다. > 작업을 수행하는 두 주체 A, B가 있다고 가정하자. 동기 (sync) A가 작업을 끝내는 시간에
velog.io
자바에서 비동기 구현하기
해당 개념을 공부하게 된 이유는 내가 맡은 프로젝트에서 외부 API를 호출할 때 해당 서버의 처리와 관계없이 해당 요청을 안전하게 마무리하기 위함이다. 일전에 자바스크립트에서 비동기가 어
velog.io
https://mangkyu.tistory.com/263
[Java] CompletableFuture에 대한 이해 및 사용법
이번에는 자바8에 추가된 CompletableFuture에 대해 알아보도록 하겠습니다. 1. CompletableFuture에 대한 이해[ Future의 단점 및 한계 ]Java5에 Future가 추가되면서 비동기 작업에 대한 결과값을 반환 받을 수
mangkyu.tistory.com
혹시라도 틀린 내용이 있다면 댓글로 알려주시면 감사하겠습니다!!
'Java' 카테고리의 다른 글
[Java] MultiThread란? (3) | 2025.05.28 |
---|---|
[Java] Runtime Data Area란? (1) | 2025.05.26 |
[Java] Garbage Collection란? (1) | 2025.05.23 |
[Java] Execution Engine이란? (1) | 2025.05.22 |
[Java] Class Loader란? (2) | 2025.05.21 |