
Spring Boot 프로젝트에 멀티스레딩을 도입할 때 ExecutorService를 활용하는 이유, 적용 방식, 그리고 시스템 자원을 효과적으로 사용하는 방법까지 정리해보았습니다.
대규모 요청 처리, 비동기 작업, 외부 API 병렬 호출, 파일 처리 등 I/O 중심 작업에서 성능 병목을 피하려면 멀티스레딩이 필요합니다.
Spring의 기본 서블릿 기반 요청 처리 방식은 각 요청당 하나의 스레드를 사용하기 때문에, 복잡한 비동기 처리가 필요한 경우 직접 스레드 관리를 할 수 있는 구조가 필요합니다.
ExecutorService는 Java의 java.util.concurrent 패키지에 포함된 인터페이스로, 스레드 풀을 구성하고 효율적인 스레드 실행을 관리해줍니다.
직접 스레드를 생성/제어하는 방식보다 안전하고, 유지보수가 쉽습니다.
@Configuration
public class ExecutorConfig {
@Bean
public ExecutorService taskExecutor() {
return Executors.newFixedThreadPool(10); // 동시에 최대 10개의 작업 처리
}
}
@Service
public class ReportService {
private final ExecutorService executorService;
public ReportService(ExecutorService executorService) {
this.executorService = executorService;
}
public void generateReports(List<Long> reportIds) {
for (Long id : reportIds) {
executorService.submit(() -> {
// 실제 보고서 생성 로직
processReport(id);
});
}
}
private void processReport(Long id) {
// time-consuming logic
try {
Thread.sleep(3000);
System.out.println("Report created: " + id);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
좀 더 정밀한 제어를 위해 아래처럼 사용 가능 합니다.
@Bean
public ExecutorService customThreadPool() {
return new ThreadPoolExecutor(
5, // core pool size
10, // max pool size
60L, TimeUnit.SECONDS, // idle thread keep-alive time
new LinkedBlockingQueue<>(100), // queue size 제한
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy() // 큐가 가득 찼을 때 정책
);
}
스프링 종료 시 Executor를 안전하게 종료해야 리소스 누수 방지 가능 합니다.
@PreDestroy
public void shutdown() {
executorService.shutdown();
}
Future<String> result = executorService.submit(() -> "작업 완료");
String output = result.get();
ExecutorService를 활용한 멀티스레딩은 단순한 비동기 처리를 넘어서 시스템 전반의 처리 성능을 높일 수 있는 강력한 도구 입니다.
하지만 자원 관리, 예외 처리, 스레드 풀 설정을 신중히 해야 진짜 '성능 향상'으로 이어진다. 실무에서는 JVM 튜닝, 로깅, 모니터링과 함께 적용하는 것을 추천 합니다.
멀티스레딩은 도구일 뿐, 무작정 쓰면 독이 된다. 필요한 지점에서 정밀하게 활용하는 게 관건입니다.