[Java] - Callable, Future

CodeByHan·2025년 9월 28일

자바

목록 보기
6/13

요즘 이력서 쓰느라... 면접 보느라... 코테 하느라... 너무 바쁜 일상을 보내고 있다. 어제는 여자친구랑 불꽃축제를 구경하며 리프레시를 했지만 해야할게 너무 많다!!

오늘은 쓰레드에 대해 좀 더 약간 공부를 해볼려고 한다...(쓰레드는 너무 어려워... 😭)

예전에 프로세스, 쓰레드에 대해 조금 정리를 해두긴 했다.

📚 프로세스(Process), 스레드(Thread), Virtual Thread

자바에서 스레드를 만드는 방법은 2가지가 존재한다. 하나는 Thread 클래스를 상속 받는 방법, 또 하나는 Runnable 인터페이스를 구현하는 방법이 있다.

하지만 이 두가지 방법에는 단점 및 한계가 존재한다!

1 . 저수준 API 의존

  • Thread를 직접 생성하고 start()를 호출해야 해서, 스레드 관리가 직접적으로 필요함
  • 스레드 풀이나 동시성 관리 같은 고급 기능은 직접 구현해야 함

2. 값 반환 불가

  • Runnable은 run() 메서드에서 반환값을 가질 수 없음
  • 결과를 받으려면 외부 변수나 공유 객체를 사용해야 함 → 동기화 필요

3. 오버헤드 발생

  • 매번 새로운 Thread를 만들고 종료하면, 스레드 생성/소멸 비용이 있음

  • 많은 요청이 들어오는 환경에서는 성능 문제 발생 가능

4. 스레드 관리 어려움

  • 여러 스레드를 동시에 제어하거나, 상태를 추적하는 게 번거로움

  • Deadlock, Race Condition 같은 동시성 문제에 직접 대응해야 함

따라서 Java5 부터는 결과를 반환할 수 있도록 CallableFuture를 사용할 수 있도록 한다!!

📌 Callable

Runnable의 발전된 형태로써, 제네릭을 사용해 결과를 받을 수 있다!!

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}
// Runnable: 결과 없음
Runnable r = () -> System.out.println("작업 수행");

// Callable: 결과 있음
Callable<Integer> c = () -> {
    Thread.sleep(1000);
    return 42; // 결과 반환 가능
};
항목RunnableCallable
반환값없음 (void run())있음 (V call())
예외 처리checked 예외 던질 수 없음 → try-catch 필요checked 예외 던질 수 있음 (throws Exception)
실행 방법new Thread(runnable).start()ExecutorService.submit(callable)
스레드 풀 사용가능 (execute()) 하지만 결과 없음submit() 통해 Future<V> 결과 받음

📌 Future

  • 미래에 완료될 Callable의 결과를 다루기 위한 인터페이스
  • Callable 작업이 완료되면 결과를 가져올 수 있음
public interface Future<V> {

    boolean cancel(boolean mayInterruptIfRunning);

    boolean isCancelled();

    boolean isDone();

    V get() throws InterruptedException, ExecutionException;

    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

1. get()

  • 블로킹 방식으로 결과를 가져옴
  • 타임아웃 설정 가능 (get(long timeout, TimeUnit unit))

2. isDone() / isCancelled()

  • isDone() : 작업의 완료 여부 반환 (boolean)
  • isCancelled() : 작업의 취소 여부 반환 (boolean)

3. cancel()

  • 작업을 취소하고 취소 성공 여부를 반환 (boolean)
  • cancel() 호출 후에는 isDone()이 항상 true 반환
  • 파라미터로 true를 전달하면, 쓰레드를 interrupt -> InterruptException 발생

📌 Thread Per Request

간단하게 말하자면 요청(Request) 하나당 하나의 쓰레드(Thread)를 생성하는 방식이다.

Spring Web MVC를 예를 들다면, 1개의 요청에 1개의 쓰레드를 사용하게 되고, Controller, Service, Repository, model 어디에서든 같은 쓰레드를 사용하게 된다.

하지만 이렇게 매 요청마다 쓰레드를 생성하면 비용이 많이 발생한다. 요청이 많아질수록 각 쓰레드는 CPU와 메모리를 많이 잡아먹고 Out Of Memory가 발생할 수도있고, 오버헤드가 많이 발생한다.

따라서 쓰레드를 제한된 개수 만큼 정해두고, 작업 큐(Queue)에 들어오는 작업들을 하나의 쓰레드가 맡아처리하고, 작업처리가 끝난 쓰레드는 다시 작업 큐(Queue)에서 새로운 작업을 처리하는 쓰레드풀이 등장하였다.

Java5에서는 이러한 쓰레드 풀에 대한 기능이 추가되었고, Executor, ExecutorService를 제공한다.

📌 Executor

  • 쓰레드 풀(Thread Poll)의 구현을 위한 인터페이스

  • 등록된 작업(Runnable)을 실행하기 위한 인터페이스

  • 작업 등록과 작업 실행 중에서 작업 실행만을 책임

public interface Executor {

   void execute(Runnable command);

}

쓰레드는 크게 작업의 등록, 실행으로 나눈다. 그중에서 Executor는 ISP 원칙에 따라 등록된 작업을 실행하는 책임만 갖는다. 그래서 전달받은 작업(Runnable)을 실행하는 메서드만 존재한다.

public class ExecutorExample {

    @Test
    void executorRunAsync() {
        final Runnable runnable = () -> System.out.println("Thread: " + Thread.currentThread().getName());

        Executor executor = new AsyncExecutor();
        executor.execute(runnable);

        System.out.println("Main Thread: " + Thread.currentThread().getName());
    }

    // Executor를 구현, Runnable을 새로운 Thread에서 실행
    static class AsyncExecutor implements Executor {

        @Override
        public void execute(final Runnable command) {
            new Thread(command).start(); // 새로운 스레드에서 실행
        }
    }
}

Runnable 작업을 비동기로 실행, Executor 인터페이스를 구현하여 실행 방식을 추상화, 메인 스레드와 작업 스레드가 독립적으로 실행됨

📌 ExecutorService

  • 작업(Runnable, Callable) 등록을 위한 인터페이스
  • Executor를 상속받아 작업 등록 뿐만 아니라 실행을 위한 책임도 갖는다.
  • 대표적으로 TreadPoolExecutorExcutorService의 구현체인데, ThreadPoolExecutor 내부에 있는 블로킹 큐에 작업을 등록해둔다.

비동기 작업

ExecutorService는 비동기 작업을 위한 기능들을 제공해준다고 한다. 특히, 비동기 작업의 진행을 추적할 수 있도록 Future를 반환한다. 반환된 Future들은 모두 실행된 것이므로 반환된 isDone = true

submit

  • 실행할 작업들을 추가하고, 작업의 상태와 결과를 포함하는 Future를 반환
  • Future.get()을 호출하면 작업이 성공적으로 완료된 후 결과를 얻을 수 있음

invokeAll

  • 모든 작업이 완료될 때까지 대기하는 블로킹 방식
  • 동시에 주어진 작업들을 모두 실행하고, 모든 작업이 끝나면 각각의 상태와 결과를 갖는 List<Future>를 반환

invokeAny

  • 가장 빨리 완료된 작업의 결과가 나올 때까지 대기하는 블로킹 방식
  • 동시에 주어진 작업들을 모두 실행하고, 가장 먼저 완료된 하나의 결과를 Future로 반환

AbstractExecutorService와 ExecutorService 메서드 구현

  • AbstractExecutorServiceExecutorService기본 구현을 제공
  • submit, invokeAll, invokeAny 등 메서드의 기본 동작을 제공함

invokeAll 동작 방식

  • 최대 스레드 풀 크기만큼 작업을 동시에 실행
  • 스레드가 충분할 경우: 동시에 실행되는 작업 중 가장 오래 걸리는 작업 시간만큼 소요
  • 스레드가 부족할 경우: 대기되는 작업이 생기므로
    → 가장 오래 걸리는 작업 시간 + 대기 작업 처리 시간 만큼 추가 소요

invokeAny 동작 방식

  • 동시에 실행한 작업들 중 가장 빨리 완료된 작업의 결과만 반환
  • 따라서 전체 실행 시간은 가장 짧게 걸리는 작업 시간만큼 소요
  • 가장 빨리 완료된 작업 외의 나머지 작업들은 자동으로 취소(canceled) 처리됨
  • 작업 진행 중에 작업들이 수정되면 결과가 정의되지 않음

😘 마지막으로...

블로그를 마무리하며 어제 찍은 불꽃축제 사진을 올려본다.
어둠을 뚫고 터져 나오는 저 찬란한 빛들을 보니, 문득 우리 모두가 저 불꽃처럼 살았으면 좋겠다는 생각이 든다. 각자의 자리에서 자신만의 색깔로 밝게 빛나며, 주변을 따뜻하게 비춰주는 그런 사람들로 말이다.
짧지만 강렬한 순간을 위해 모든 걸 쏟아내는 불꽃처럼, 오늘도 누군가에게 작은 빛이 되어주길 바란다.

profile
노력은 배신하지 않아 🔥

0개의 댓글