Java Spring boot에서 비동기가 필요해서 Future를 공부하고 적용한 내용을 정리했음.
자바는 1.5부터 Future 인터페이슬 통해 비동기를 제공했다. 초기 Future는 문제가 한계가 있었다. Future 작업 자체는 비동기로 될 지라도 그 결과값을 받아서 처리하기 위해선 get()을 통해 return 값을 활용할 수 있었다. get()이 없다면 return 값을 받을 수가 없었는데 막상 get()을 사용하면 future 작업이 끝날 때까지 기다린다. 따라서 비동기가 아니라 동기가 되는 것이다.
Callable<Integer> task = () -> {
Thread.sleep(1000); // 1초동안 대기
return 123; // 결과 반환
};
FutureTask<Integer> future = new FutureTask<>(task);
new Thread(future).start(); // Future 작업 실행
// 다른 작업 ...
Integer result = future.get(); // 결과가 준비될 때까지 Blocking
System.out.println("Result: " + result);
이 문제를 해결하기 위해 Java 1.8버전에서 CompletableFuture가 등장했다.
CompletableFuture는 비동기 작업 실행, 작업 콜백, Future의 조합 등의 기능을 제공한다. 따라서 여러 비동기 작업을 하나의 흐름으로 만들고 적용할 수 있다.
CompletableFuture를 사용하는 방법은 여러 가지가 있다. 나는 thenCompose와 thenRun을 주로 사용했으나 개발 환경에 따라 적용하는 것이 적절하다.
scheduleProvider.startGame(gameInitVO.getGameId(), currentRound)
.thenCompose(v -> {
// IntStream으로 라운드 수만큼 체인 생성
CompletableFuture<Integer> roundChain = CompletableFuture.completedFuture(currentRound);
// 각 라운드에 대해 체인에 비동기 작업을 연결
for (int round = 1; round <= gameStartRequestDTO.getRoundCount(); round++) {
final int currentRoundInLoop = round;
roundChain = roundChain.thenCompose(ignored -> scheduleProvider.roundScheduler(gameId, gameStartRequestDTO, currentRoundInLoop));
}
// 마지막 결과를 `CompletableFuture<Void>`로 변환
return scheduleProvider.scheduleFuture(gameId, gameExistLimitTime);
}).thenRun(() -> {
gameService.finishGame(new SocketDTO(gameStartRequestDTO.getSenderNickname(), gameId, gameStartRequestDTO.getSenderTeamId()));
log.info("{} Game is dead at {}", gameId, LocalDateTime.now());
gameManager.removeGame(gameId);
}).exceptionally(ex -> {
log.error("Error occurred in the CompletableFuture chain: ", ex);
throw new BaseException(BaseResponseStatus.OOPS, gameId);
});
이 외에도 후술할 schedule Future를 사용한 Schedule Executer Service의 schedule을 사용했다. scheduleProvider로 커스텀했다.
CompletableFuture 설명
CompletableFuture를 잘 설명해주신 블로그가 있어서 많이 참고했다.