해당 글은 https://www.baeldung.com/java-future 를 번역한 글입니다.
1. 개요
이 튜토리얼에서는 Future에 대해 알아보고자 한다. 자바 1.5부터 생긴 인터페이스로, 비동기 호출과 병렬 처리 하는데 꽤나 유용하다.
2. Future 만들기
간단히 말하자면, Future 클래스는 미래에 나오는 비동기 연산 결과를 나타내는 클래스다. 이 결과는 처리가 완료되면 Future 에 최종적으로 나올 것이다.
그럼 Future 객체를 만들고 반환하는 메소드를 어떻게 작성하면 될까?
오래 도는 메소드는 기다리는 동안 다른 프로세스를 실행할 수 있다는 점에서 비동기로 처리하기에 좋은 후보다.
Future의 비동기 특성을 사용 하기에 적합한 작업은 다음과 같다.
- 수학적, 과학적 계산 등의 연산이 하드한 프로세스
- 거대한 자료구조를 다루는 경우 (빅데이터)
- 외부(remote) 메소드 호출 (파일 다운로드, HTML 스크래핑, 웹 서비스)
2.1 FutureTask와 함께 Future 구현하기
예제로 정수값의 제곱값을 계산하는 아주 간단한 클래스를 만들어볼 것이다. 이건 당연히 '오래 도는 메소드' 부류는 아니지만, Thread.sleep() 을 넣어서 종료되기 전에 1초 정도 지속되도록 한다.
public class SquareCalculator {
private ExecutorService executor
= Executors.newSingleThreadExecutor();
public Future<Integer> calculate(Integer input) {
return executor.submit(() -> {
Thread.sleep(1000);
return input * input;
});
}
}
실제로 계산을 수행하는 코드는 call() 메소드 안에 포함되어 있고, 람다식으로 제공될 것이다. 위에서 볼 수 있듯이, 앞서 언급한 Thread.sleep() 호출 이외에는 별거 없다.
하지만 Callable 과 ExecutorService 를 사용하는 것에 초점을 맞추면 이 코드는 좀 더 흥미로워진다.
Callable은 결과값을 반환하는 task를 나타내는 인터페이스로, 한 개의 call() 메소드를 가지고 있다. 여기서 우리는 그 객체를 람다식으로 만들었다.
Callable 객체를 만든다고 뭐가 된 것은 아니다 : 이 객체는 executor 에 전달되어야 가치있다. Executor는 새로운 스레드에서 task를 실행시키고 가치 있는 Future 객체를 반환시켜주는 역할을 한다. 여기서 ExecutorService가 등장한다.
ExecutorService 객체에 접근하는 방법에는 몇 가지가 있는데, 대부분은 유틸리티 클래스 Executor의 정적 팩토리 메소드로부터 제공된다. 이 예제에서 우리는 기본 newSingleThreadExecutor() 를 사용하였는데, 이는 ExecutorService를 한번에 하나의 스레드를 다룰 수 있도록 한다.
ExecutorService 객체가 있으면 submit()을 호출하고 Callable을 인수로 전달하기만 하면 된다. 그런 다음 submit()은 task를 시작하고 Future 인터페이스의 구현체인 FutureTask 객체를 반환할 것이다.
3. Future 소비(Consume)하기
앞서 우리는 Future의 객체를 만드는 방법을 배웠다. 이 섹션에서는 Future API 를 구성하는 모든 메소드를 둘러봅으로써, 앞서 만들어본 Future 객체를 어떻게 다뤄야 할지 볼 것이다.
3.1 isDone()과 get() 사용하여 결과 얻기
Future<Integer> future = new SquareCalculator().calculate(10);
while(!future.isDone()) {
System.out.println("Calculating...");
Thread.sleep(300);
}
Integer result = future.get();
이 예제에서, 우리는
In this example, we'll write a simple message on the output to let the user know the program is performing the ca
3.2 cancel() 로 Future 취소하기
우리가 task를 트리거 했는데 어떤 이유로 더 이상 결과를 알 필요가 없어졌다고 가정해보자. 우리는 executor에게 그 작업을 멈추고 기반 스레드에게 interrupt 하라고 executor에게 알려주는 Future.cancel(boolean) 을 사용할 수 있다.
Future<Integer> future = new SquareCalculator().calculate(4);
boolean canceled = future.cancel(true);
위 코드에서 Future의 객체는 작업을 절대 끝내지 않을 것이다. 정확히 말하면, 우리가 객체의 get()을 cancel() 호출 다음에 호출한다면, CancellationException 이 터질 것이다. Future.isCancelled() 는 이미 Future가 취소되었는지를 알려주기 때문에 그를 사용하여 CancellationException 을 피할 수 있다.
cancel() 호출이 실패할 수도 있는데, 이 경우에 반환값이 false가 된다. cancel()이 인자로 boolean 값을 받는다는 것이 중요하다. 이는 task를 실행시키는 스레드가 interrupt될지 말지를 결정한다.
4. ThreadPool들과 멀티스레딩 심화
현재 우리의 ExecutorService는 단일 스레드로 돌아가고 있다. 왜냐하면 Executors.newSingleThreadExecutor로 획득되기 때문이다.
SquareCalculator squareCalculator = new SquareCalculator();
Future<Integer> future1 = squareCalculator.calculate(10);
Future<Integer> future2 = squareCalculator.calculate(100);
while (!(future1.isDone() && future2.isDone())) {
System.out.println(
String.format(
"future1 is %s and future2 is %s",
future1.isDone() ? "done" : "not done",
future2.isDone() ? "done" : "not done"
)
);
Thread.sleep(300);
}
Integer result1 = future1.get();
Integer result2 = future2.get();
System.out.println(result1 + " and " + result2);
squareCalculator.shutdown();
위 코드의 결과값은 다음과 같다.
calculating square for: 10
future1 is not done and future2 is not done
future1 is not done and future2 is not done
future1 is not done and future2 is not done
future1 is not done and future2 is not done
calculating square for: 100
future1 is done and future2 is not done
future1 is done and future2 is not done
future1 is done and future2 is not done
100 and 10000
프로세스가 병렬로 돌지 않는다는 건 자명하다. 우리는 두번째 task가 첫번째 task가 끝날 때에만 시작해 전체 프로세스가 끝내는데 2초 내외로 걸린다는 점을 알 수 있다.
우리의 프로그램을 정말 멀티 스레드로 돌리려면, 다른 형태의 ExecutorService 를 사용해야 한다.
그럼 Executors.newFixedThreadPool() 팩토리 메소드로부터 스레드 풀을 제공받아 사용할 경우 우리의 예제가 어떻게 바뀌는지 보자.
public class SquareCalculator {
private ExecutorService executor = Executors.newFixedThreadPool(2);
//...
}
SquareCalculator 클래스에서 간단한 변화와 함께, 우리는 이제 2개의 스레드를 동시에 사용하는 executor를 얻었다.
똑같은 클라이언트 코드를 다시 돌리면, 우리는 이제 다음과 같은 결과를 얻게 된다.
calculating square for: 10
calculating square for: 100
future1 is not done and future2 is not done
future1 is not done and future2 is not done
future1 is not done and future2 is not done
future1 is not done and future2 is not done
100 and 10000
이제 훨씬 보기 좋아졌다. 이제 두 task들이 동시에 시작하고 종료하며 총 1초정도 걸린다는 것을 확인할 수 있다.
스레드 풀을 만드는데 사용될 수 있는 다른 팩토리 메소드들도 있다.
Executors.newCachedThreadPool() 는 가능할 때 스레드 풀을 재사용하고, Executors.newScheduledThreadPool()는 특정 시간만큼 간격을 두고 명령을 실행하도록 스케줄한다.
ExecutorService에 대해서 더 알고 싶다면, 이 글을 읽으면 된다.
'번역자료 (구글번역은 못 참지) > Baeldung' 카테고리의 다른 글
스프링 이벤트 (0) | 2022.05.22 |
---|---|
@Async 로 스프링 비동기 처리 (0) | 2022.05.20 |
스프링 Task Scheduler 가이드 (0) | 2022.05.20 |