
최근 애플리케이션의 성능 최적화와 동시에 동시성 처리가 중요한 이슈로 떠오르고 있습니다.
이 글에서는 스레드 생성 비용, Runnable 인터페이스의 한계, 그리고 ThreadPoolExecutor와 Future를 활용해
어떻게 효율적인 비동기 작업 처리를 할 수 있는지에 대해 자세히 알아보겠습니다.
스레드를 직접 생성할 경우,
이런 작업들은 상당한 비용을 수반합니다. 특히, 많은 작업을 짧은 시간 내에 처리해야 하는 경우라면
스레드 생성 및 소멸에 드는 오버헤드가 전체 성능 저하로 이어질 수 있습니다.
예시:
다수의 작업을 순차적으로 처리하는 경우, 작업마다 새로운 스레드를 생성하면 매번 OS에 부담을 주게 됩니다.
자바에서 스레드를 구현할 때 흔히 사용되는 Runnable 인터페이스는 다음과 같은 한계가 있습니다.
run() 메서드는 실행 결과를 반환하지 않으므로, 작업 결과를 외부에서 직접 받아볼 수 없습니다.join() 등을 통해 스레드가 종료되길 기다려야 합니다.이러한 한계를 극복하기 위해 등장한 것이 Callable 인터페이스입니다.
Callable은 결과를 반환할 수 있으며 예외 처리도 보다 유연하게 할 수 있습니다.
Runnable 예시:
public class MyRunnable implements Runnable { @Override public void run() { // 작업 수행 System.out.println("Runnable 작업 수행"); } }
Callable 예시:
public class MyCallable implements Callable<Integer> { @Override public Integer call() { // 작업 수행 후 결과 반환 return 42; } }
스레드 생성 비용 문제와 Runnable의 한계를 보완하기 위해 ThreadPoolExecutor를 사용합니다.
ThreadPoolExecutor는 요청이 들어올 때마다 새로운 스레드를 생성하는 대신, 미리 생성된 스레드 풀을 활용해 작업을 할당합니다.
또한, submit() 메서드를 사용하면 Future 객체가 즉시 반환되어 요청 스레드가 블로킹되지 않습니다.
예시:
ExecutorService executor = Executors.newFixedThreadPool(5); Future<Integer> future = executor.submit(new MyCallable()); // 요청 스레드는 다른 작업을 수행할 수 있음.
Future 객체는 제출된 작업의 미래 결과를 나타냅니다.
Future.get() 메서드를 호출하면 두 가지 상황이 발생할 수 있습니다:
이러한 동작 덕분에 요청 스레드는 작업 결과가 필요한 시점에만 기다리게 되어,
비동기적으로 작업을 수행하면서도 결과를 적시에 받을 수 있습니다.
예시:
// 비동기적으로 두 작업을 제출 Future<Integer> future1 = executor.submit(task1); Future<Integer> future2 = executor.submit(task2); // 필요 시 결과를 블로킹 호출로 받아올 수 있음 Integer result1 = future1.get(); Integer result2 = future2.get();
Future는 아직 완료되지 않은 작업을 취소할 수 있는 기능도 제공합니다.
cancel(boolean mayInterruptIfRunning) 메서드는 다음과 같이 동작합니다:
Thread.interrupt()를 호출하여 중단시킵니다.이러한 기능을 통해 필요 없는 작업에 대한 자원 낭비를 방지할 수 있습니다.
예시:
// 작업 취소 예제 if (!future.isDone()) { future.cancel(true); // 실행 중인 작업이라도 중단을 시도 }
예외 처리:
Future의 get() 메서드 호출 시 InterruptedException과 ExecutionException에 대한 처리가 필요합니다.
예외 상황에 대비하여 적절한 로그 기록이나 리트라이 로직을 구현하세요.
스레드 풀 사이즈:
애플리케이션의 특성에 맞춰 스레드 풀의 크기를 조정하세요.
너무 작은 풀은 작업 대기열이 쌓일 수 있고, 너무 큰 풀은 자원 낭비를 초래할 수 있습니다.
자원 정리:
ExecutorService 사용 후에는 shutdown() 또는 shutdownNow()를 호출해 스레드 풀을 적절히 종료하는 것이 좋습니다.
실시간 모니터링:
운영 중인 스레드 풀의 상태를 모니터링하여, 필요 시 동적으로 스레드 수를 조정하거나 문제를 사전에 파악할 수 있도록 하세요.
스레드 생성 비용으로 인한 성능 문제와 Runnable 인터페이스의 한계를 극복하기 위해,
자바에서는 ThreadPoolExecutor와 Future를 통한 비동기 처리 기법을 제공합니다.
이를 통해 불필요한 스레드 생성 오버헤드를 줄이고,
요청 스레드가 필요할 때만 결과를 블로킹하여 받아올 수 있는 유연한 구조를 구현할 수 있습니다.