Java

[Java] Java 8의 특징(6) - CompletableFuture

재담 2022. 2. 28. 18:42

자바 Concurrent 프로그래밍

Concurrent 하다의 의미는 동시에 여러 작업을 할 수 있는 것을 의미한다. 예를 들어 유튜브를 보면서 게임을 한다거나 노래를 들으면서 코딩을 하는 것들이 Concurrent 한 것이다.

 

자바에서 지원하는 Concurrent 프로그래밍에는 다음과 같은 것들이 있다.

  • 멀티 프로세싱(ProcessBuilder)
  • 멀티 스레드(Thread / Runnable)

 

멀티 스레드는 Thread를 상속해서 다음과 같이 구현할 수 있다.

public class ConcurrentClass {
    public static void main(String[] args) {
        HelloThread helloThread = new HelloThread();
        helloThread.start();
        System.out.println("hello : " + Thread.currentThread().getName());
    }

    static class HelloThread extends Thread {
        @Override
        public void run() {
            System.out.println("world : " + Thread.currentThread().getName());
        }
    }
}

 

Thread 클래스는 Runnable 인터페이스를 구현한 클래스이기 때문에 람다를 이용해 간단하게 표현할 수 있다.

public class ConcurrentClass {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> System.out.println("world : " + Thread.currentThread().getName()));
        thread.start();
        System.out.println("hello : " + Thread.currentThread().getName());
    }
}

 

Thread의 주요 기능은 다음과 같다.

  • 현재 스레드 멈추기(sleep) : 다른 스레드가 처리할 수 있도록 기회를 주지만 락을 놔주지는 않는다.
  • 다른 스레드 깨우기(interrupt) : 다른 스레드를 깨워서 InterruptedException을 발생시킨다.
  • 다른 스레드 기다리기(join) : 다른 스레드가 끝날 때까지 기다린다.

 

Executors

Executors고수준 Concurrency 프로그래밍을 할 때 쓰인다. 스레드를 만들고 관리하는 작업을 애플리케이션에서 분리해 Executors에게 위임한다.

 

Executors가 하는 일과 주요 인터페이스는 다음과 같다.

  • 스레드 만들기 : 애플리케이션이 사용할 스레드 풀을 만들어 관리한다.
  • 스레드 관리 : 스레드 라이프 사이클을 관리한다.
  • 작업 처리 및 실행 : 스레드로 실행할 작업을 제공할 수 있는 API를 제공한다.
  • Executor : executor(Runnable)
  • ExecutorService : Executor를 상속받은 인터페이스로, Callable도 실행 가능하고, Executor를 종료시키거나 여러 Callable을 동시에 실행하는 등의 기능을 제공한다.
  • ScheduledExecutorService : ExecutorService를 상속받은 인터페이스로 특정 시간 이후에 또는 주기적으로 작업을 실행할 수 있다.

 

다음과 같이 ExecutorService로 작업을 실행하고 멈출 수 있다.

ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.submit(() -> {
    System.out.println("Hello :" + Thread.currentThread().getName());
});

executorService.shutdown(); // 처리중인 작업 기다렸다가 종료
executorService.shutdownNow(); // 당장 종료

 

 

Callable과 Future

Callable은 Runnable과 유사하지만 작업의 결과를 받을 수 있다. Future비동기적인 작업의 현재 상태를 조회하거나 결과를 가져올 수 있다.

 

결과를 받아서 출력하고 싶다면 다음과 같이 get() 메서드로 구현할 수 있다. get() 메서드는 타임아웃을 지정해서 최대한으로 기다릴 시간을 설정할 수 있고, 블록킹 콜이다.

ExecutorService executorService = Executors.newSingleThreadExecutor();
Future<String> helloFuture = executorService.submit(() -> {
    Thread.sleep(2000L);
    return "Callable";
});

String result = helloFuture.get();
System.out.println(result);
executorService.shutdown();

 

기타 API들을 살펴보자.

 

작업 상태 확인하기

  • isDone() : 작업을 완료했으면 true, 아니면 false를 리턴한다.

 

작업 취소하기

  • cancel() : 작업을 취소했으면 true, 못했으면 false를 리턴한다.
  • 파라미터로 true를 전달하면 현재 진행 중인 스레드를 interrupt 하고 그렇지 않으면 현재 진행 중인 작업이 끝날 때까지 기다린다.

 

여러 작업 동시에 실행하기

  • invokeAll() : 동시에 실행한 작업 중에 제일 오래 걸리는 작업만큼 시간이 걸린다.

 

여러 작업 중에 하날도 먼저 응답이 오면 끝내기

  • invokeAny() : 동시에 실행한 작업 중에 제일 짧게 걸리는 작업만큼 시간이 걸린다.
  • 블록킹 콜이다.

 

CompletableFuture

CompletableFuture 인터페이스는 자바에서 비동기(Asynchronous) 프로그래밍을 가능하게 하는 인터페이스이다. Future를 사용해도 어느 정도 가능했지만 다음과 불편한 점들이 있었다.

  • Future를 외부에서 완료시킬 수 없다. 취소하거나 get()에 타임아웃을 설정할 수는 있다.
  • 블록킹 코드(get() 메서드)를 사용하지 않고는 작업이 끝났을 때 콜백을 실행할 수 없다.
  • 여러 Future를 조합할 수 없다.
  • 예외 처리용 API를 제공하지 않는다.

 

주요 API에는 다음과 같은 것들이 있다.

 

비동기로 작업 실행하기

  • runAsync() : 리턴 값이 없는 경우
  • supplyAsync() : 리턴 값이 있는 경우
  • 원하는 Executor(스레드 풀)를 사용해서 실행할 수 있다. 기본은 ForkJoinPool.commonPool()

 

콜백 제공하기

  • thenApply(Funtion) : 리턴 값을 받아서 다른 값으로 바꾸는 콜백
  • thenAccept(Consumer) : 리턴 값을 받아서 또 다른 작업을 처리하는 콜백
  • thenRun(Runnable) : 리턴 값을 받지 않고 다른 작업을 처리하는 콜백
  • 콜백 자체를 또 다른 스레드에서 실행할 수 있다.

 

조합하기

  • thenCompose() : 두 작업이 서로 이어서 실행하도록 조합
  • thenCombine() : 두 작업을 독립적으로 실행하고 둘 다 종료했을 때 콜백 실행
  • allOf() : 여러 작업을 모두 실행하고 모든 작업 결과에 콜백 실행
  • anyOf() : 여러 작업 중에 가장 빨리 끝난 하나의 결과에 콜백 실행

 

예외 처리

  • exceptionally(Function)
  • handle(BiFunction)

 


Reference