본문 바로가기
카테고리 없음

Java 비동기 프로그래밍

by devstep 2025. 1. 15.

Thread를 구현하는 2가지 방법

멀티스레딩 프로그래밍에서 사용되는 인터페이스

1. Runnable 인터페이스 구현
2. Callable  인터페이스 구현
- Runnable은 return value가 없기 때문에 ExecutorService에서 비동기로 처리해도 문제가 없었다.
- 하지만 Callable은 return value가 있고, Exception도 발생할 수 있으므로 ExecutorService에서 submit()을 할 때 해당 task가 완료될 때까지 기다리지 않으므로 ExecutorService는 결과를 직접 반환할 수 없다. 그래서 Future라는 특수한 result type을 반환하고, Future를 통해서 나중에 실제 실행 결과를 검색할 수 있다.



 Runnable & Callable Interface Code


1. Runnable 

   public interface Runnable {
       public abstract void run();  
    }


2. Callable 
   - 인자를 받지 않고, 특정 타입의 객체를 리턴한다.
   - call() 수행 중 Exception을 발생시킬 수 있다.

public interface Callable {
   V call() throws Exception;
}

   - 수행 코드

import java.util.concurrent.*;

public class CallableAsyncExample {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        System.out.println("메인 스레드 시작");

        Callable<String> task = () -> {
            Thread.sleep(2000);
            return "비동기 작업 완료!";
        };

        ExecutorService executor = Executors.newSingleThreadExecutor();
        Future<String> future = executor.submit(task); // 비동기 실행 및 결과 대기 가능

        System.out.println("메인 스레드에서 다른 작업 수행 중...");

        // 결과를 기다릴 필요가 없으나, 필요할 때 get() 호출
        String result = future.get(); // 결과를 기다림 (블로킹)
        System.out.println("결과: " + result);

        executor.shutdown();
    }
}

 

ExecutorService에서 Callable 사용 방법

- ExecutorService에서 Callable을 Job으로 등록하고 수행시킬 수 있다.

 

CompletableFuture

  • 비동기 작업 수행: 메인 스레드와 별도로 작업을 수행할 수 있다.
  • 결과 반환 가능: 작업 완료 후 값을 반환할 수 있다.
  • 체이닝 지원: thenApply, thenAccept 등을 사용하여 연속적인 작업 수행 가능.
  • 예외 처리 가능: exceptionally, handle 등을 통해 예외를 쉽게 처리할 수 있다.
  • 논블로킹(non-blocking): Future와 달리 get() 호출 없이도 작업을 연결할 수 있다.

 

import java.util.concurrent.CompletableFuture;

public class CompletableFutureExample {
    public static void main(String[] args) {
        System.out.println("메인 스레드 시작");

        CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
            try {
                Thread.sleep(2000); // 비동기 작업
                System.out.println("비동기 작업 완료");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        System.out.println("메인 스레드에서 다른 작업 수행 중...");
        
        // 결과를 기다리지 않고 비동기 작업 진행
        future.join(); // (옵션) 메인 스레드 종료 전 비동기 작업 완료 대기
        System.out.println("메인 스레드 종료");
    }
}

//결과
메인 스레드 시작
메인 스레드에서 다른 작업 수행 중...
비동기 작업 완료
메인 스레드 종료

 

🎯 정리: Runnable vs Callable vs CompletableFuture

특징 Runnable Callable CompletableFuture
비동기 지원 여부 지원 가능 (결과 없음) 지원 가능 (결과 있음) 완전 비동기 지원 (결과 있음)
결과 반환 여부 없음 있음 있음
예외 처리 Checked Exception 미지원 Checked Exception 지원 Checked Exception 지원
스레드 풀 관리 수동 (Thread) ExecutorService 사용 ExecutorService 사용
Java 버전 1.0 이상 1.5 이상 8 이상

 


ExecutorService란?

  • ExecutorService는 멀티스레딩을 간편하게 다룰 수 있게 해주는 java.util.concurrent 패키지에서 제공하는 스레드 풀 관리를 위한  API. 
  • 스레드 풀 관리로 성능 최적화 가능.
  • 비동기 작업 수행 및 결과 반환을 Runnable, Callable과 함께 지원.
  • 적절한 스레드 풀 선택이 중요 (Fixed, Cached, SingleThread, Scheduled).

ExecutorService는 멀티스레딩의 복잡성을 줄이고, 효율적인 스레드 관리를 제공한다.

기본 개념

  • 스레드 풀 관리: 여러 스레드를 미리 생성해두고 필요할 때 재사용하는 방식.
  • 비동기 작업 지원: 여러 작업을 동시에 수행 가능.
  • 스레드 수 제한: 스레드 수를 제한하여 과도한 스레드 생성 방지.
  • 작업 제출 및 실행: Runnable, Callable을 실행하고, Future를 반환할 수 있음.
  • 자동 종료 지원 안함: shutdown()을 호출해야 스레드 종료.

 

📌 기본 사용법

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ExecutorServiceExample {
    public static void main(String[] args) {
        // 고정 스레드 풀 생성 (스레드 3개)
        ExecutorService executor = Executors.newFixedThreadPool(3);

        // 작업 제출
        for (int i = 1; i <= 5; i++) {
            final int taskId = i;
            executor.submit(() -> {
                System.out.println("작업 " + taskId + " 실행, 스레드: " + Thread.currentThread().getName());
                try {
                    Thread.sleep(1000); // 1초 대기
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        // 스레드 풀 종료 (더 이상 작업을 받지 않음)
        executor.shutdown();
    }
}

//출력결과 (실행 순서는 비동기이므로 다를 수 있음)
작업 1 실행, 스레드: pool-1-thread-1
작업 2 실행, 스레드: pool-1-thread-2
작업 3 실행, 스레드: pool-1-thread-3
작업 4 실행, 스레드: pool-1-thread-1
작업 5 실행, 스레드: pool-1-thread-2

 

📌 스레드 풀 종류

자바의 Executors 클래스는 다양한 ExecutorService를 제공합니다.

1. 고정 크기 스레드 풀 (newFixedThreadPool)

ExecutorService executor = Executors.newFixedThreadPool(3);
 
  • 지정한 수의 스레드만 생성하고, 초과 작업은 대기 상태로 유지.
  • CPU 코어 수에 맞춰 사용하기 적합.

2. 캐시된 스레드 풀 (newCachedThreadPool)

ExecutorService executor = Executors.newCachedThreadPool();
  • 필요할 때 새로운 스레드를 생성하고, 유휴 상태의 스레드는 재사용.
  • 많은 단기 작업에 적합.

3. 단일 스레드 풀 (newSingleThreadExecutor)

ExecutorService executor = Executors.newSingleThreadExecutor();
  • 단일 스레드만 사용.
  • 작업을 순차적으로 실행.

4. 스케줄링된 스레드 풀 (newScheduledThreadPool)

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class ScheduledExecutorExample {
    public static void main(String[] args) {
        ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);

        // 3초 후에 실행
        scheduler.schedule(() -> System.out.println("3초 후 실행!"), 3, TimeUnit.SECONDS);

        // 1초 간격으로 반복 실행 (5초 후 시작)
        scheduler.scheduleAtFixedRate(() -> System.out.println("1초 간격 실행!"), 5, 1, TimeUnit.SECONDS);
    }
}

 

📌 작업 제출 방식

ExecutorService는 Runnable, Callable을 사용해 작업을 제출할 수 있습니다.

1. Runnable (결과 없음)

executor.submit(() -> System.out.println("Runnable 실행 중"));

2. Callable (결과 반환)

import java.util.concurrent.Callable;
import java.util.concurrent.Future;

Callable<String> task = () -> {
    Thread.sleep(1000);
    return "Callable 작업 완료";
};
Future<String> result = executor.submit(task);
System.out.println("결과: " + result.get()); // 결과 대기

📌 스레드 풀 종료 (shutdown() vs shutdownNow() 차이점)

1. shutdown()

  • 새로운 작업을 받지 않고, 진행 중인 작업은 완료.
 
executor.shutdown();

2. shutdownNow()

  • 현재 진행 중인 작업을 즉시 중단하고, 대기 중인 작업은 취소.
 
executor.shutdownNow();

3. awaitTermination()

  • 특정 시간 동안 기존 작업 완료 대기.
 
executor.awaitTermination(5, TimeUnit.SECONDS);

 

 

참고자료

https://winterbe.com/posts/2015/04/07/java8-concurrency-tutorial-thread-executor-examples/
https://codechacha.com/ko/java-callable-vs-runnable/

https://www.geeksforgeeks.org/what-is-java-executor-framework/

ExecutorService - chatGPT

댓글