Spring Boot 비동기 처리 = 스레드풀?

2025. 3. 16. 21:37·삽질로그

1. 들어가며

이전 포스팅에서 Java의 ExecutorService와 Spring Boot의 스레드풀을 비교하는 방식이 잘못되었다는 피드백(토스 개발자 피셜)을 받았다.

 

2025.03.09 - [삽질로그] - 여러 요청이 동시에 들어오면, Spring Boot는 어떻게 처리할까?

 

여러 요청이 동시에 들어오면, Spring Boot는 어떻게 처리할까?

1. 들어가며지난 글에서는 웹소켓 핸들러를 Event 기반으로 처리하여 동기 방식으로 해결했다.이를 통해 코드가 더 깔끔해지고 유지보수가 쉬워졌지만, 한 가지 새로운 고민이 생겼다. 🔗 이전

mingking2.tistory.com

 

당시 나는 Spring Boot가 멀티스레딩을 지원하여 동시 요청을 처리할 수 있다는 점을 설명하려 했고,

이에 따라 “그럼 Java는 동시 요청을 어떻게 처리하는가?” 라는 질문으로 이어지면서,

Java의 ExecutorService와 Tomcat의 스레드풀을 비교하게 되었다.

 

하지만, 이 비교에는 몇 가지 문제점이 있었다.

따라서 이번 포스팅에서는 왜 비교군이 잘못되었는지 분석하고, 어떤 비교가 더 적절한지 알아보려 한다.


2. 문제점 분석

기존 포스팅에서는 Java의 ExecutorService와 Spring Boot의 스레드풀을 비교하면서 다음과 같은 구조로 정리했었다.

  Java 스레드풀 (ExecutorService) Spring Boot 스레드풀 (Tomcat)
사용 목적 비동기 작업 HTTP 요청 처리
스레드 생성 방식 개발자가 직접 정의
Executors.newFixedThreadPool()
Tomcat이 자동 관리
큐(Queue) 관리 BlockingQueue<Runnable>을 사용하여 대기 작업 저장 accept-count만큼 대기 요청을 저장
새로운 스레드 추가 생성 설정에 따라 동적으로 가능
newCachedThreadPool()
기본적으로 추가 생성하지 않음, 요청을 거부

 

🚨 비교 대상이 일관되지 않음

  • ExecutorService는 일반적인 비동기 작업을 처리하는 스레드풀
  • Tomcat의 스레드풀은 웹 컨테이너 내부에서 HTTP 요청을 처리하는 스레드풀
  • 즉, 사용 목적이 다르므로 직접 비교하는 것은 적절하지 않다.

 

🚨 Spring Boot의 비동기 처리 방식과 Tomcat 스레드풀을 동일선상에서 비교하는 오류

  • Spring Boot의 비동기 처리는 내부적으로 TaskExecutor 를 사용한다.
  • Tomcat의 스레드풀은 HTTP 요청을 처리하는 서블릿 컨테이너의 구조에서 동작한다.
  • 즉, 비동기 작업과 동기 HTTP 요청 처리를 동일하게 비교한 것은 잘못된 접근이다.
비교 항목 Spring Boot TaskExecutor Tomcat의 스레드풀
역할 비동기 작업 실행 HTTP 요청 처리
스레드 생성 ThreadPoolTaskExecutor org.apache.tomcat.util.threads.ThreadPoolExecutor
큐 관리 내부적으로 LinkedBlockingQueue<Runnable> 사용 accept-count로 대기 요청 저장
사용 방식 @Async 또는 CompletableFuture Spring MVC 요청 처리

 


3. 올바른 비교군은 무엇인가?

기존 비교가 잘못되었으므로, 더 적절한 비교군을 찾아보았다.

Spring Boot가 지원하는 비동기 실행 모델과 HTTP 요청 처리 모델을 분리하여 비교해야 한다.

비교군 설명
비동기 작업 처리 비교 Java의 ExecutorService vs Spring Boot의 TaskExecutor
HTTP 요청 처리 비교 Tomcat의 스레드풀 vs WebFlux(Netty)의 이벤트 루프

 

 

이번에는 비동기 작업 처리 비교를 다루고, 다음 글에서 HTTP 요청 처리 비교를 심층적으로 다룰 예정이다.

4. 비동기 작업 처리 비교

앞서 설명했듯이, Spring Boot의 비동기 TaskExecutor와 Tomcat의 스레드풀은 완전히 별개이다.

그렇다면, 클라이언트가 비동기 요청을 했을 때 Spring Boot 내부에서는 어떻게 동작하는가?

 

✅ Spring Boot의 비동기 요청은 어떻게 동작하는가?

1️⃣ Tomcat의 스레드가 요청을 처리

  • 클라이언트가 HTTP 요청을 보낸다.
  • Tomcat의 스레드풀(Worker Thread) 중 하나가 요청을 받는다.
  • 기본적으로 Tomcat의 요청을 하나의 스레드에서 처리하고, 결과를 반환할 때까지 해당 스레드를 점유한다.

2️⃣ @Async 메서드 호출 시, TaskExecutor에서 실행

Spring Boot에서 비동기 처리를 위해 @Async를 적용하면,

→ 별도의 스레드(TaskExecutor의 스레드풀)에서 실행된다.

→ Tomcat의 스레드는 @Async 호출 후 즉시 반환되므로 요청 처리가 빠르게 끝난다.

→ 즉, 클라이언트는 빠르게 응답을 받지만, 실제 작업은 비동기적으로 진행된다.

 

✅  비동기 요청 처리 과정

실제 테스트를 통해 알아보자.

테스트 코드

ThreadPoolTaskExecutor를 사용하여 별도의 비동기 스레드 풀을 설정한다.
@Configuration
@EnableAsync
public class AsyncConfig {

	@Bean(name = "customTaskExecutor")
	public Executor taskExecutor() {
		ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
		executor.setCorePoolSize(5);
		executor.setMaxPoolSize(10);
		executor.setQueueCapacity(500);
		executor.setThreadNamePrefix("taskExecutor-");
		executor.initialize();
		return executor;
	}
}

 

클라이언트 요청을 받아 비동기 메서드를 호출한다.
@GetMapping("/async-test")
public ResponseEntity<?> asyncCall() {
	String threadName = Thread.currentThread().getName();
	log.info("[요청 수신] 현재 실행 중인 스레드: {}", threadName);

	asyncService.asyncMethod();
	return ResponseEntity.ok("응답반환");
}

 

@Async 어노테이션을 활용하여 TaskExecutor의 스레드 풀에서 실행된다.
@Slf4j
@Service
public class AsyncService {
	
	@Async("customTaskExecutor")
	public void asyncMethod() {
		String threadName = Thread.currentThread().getName();
		log.info("[비동기 실행] 현재 실행 중인 스레드: {}", threadName);

		try {
			Thread.sleep(3000);
		} catch (InterruptedException e) {
			Thread.currentThread().interrupt();
		}
		log.info("Async 작업 완료");
	}
	
}

 

테스트  실행 결과

별도의 스레드에서 동작함을 확인할 수 있다.

  • 클라이언트 요청을 받았을 때 → Tomcat의 http-nio-8080-exec-1 스레드에서 실행됨
  • 비동기 작업이 실행될 때 → taskExecutor-1 스레드에서 실행됨

 

HTTP 요청 후 바로 응답 반환

3초 지연을 통해 응답이 즉시 반환된 후에도 비동기 로직이 별도의 스레드에서 계속 실행되고 있음을 확인할 수 있다.

 

정리

비동기 흐름을 순서대로 정리하면 다음과 같다.
  1.  클라이언트가 HTTP 요청을 보냄
  2. Tomcat의 Worker Thread가 요청을 받아 컨트롤러를 실행
  3. 컨트롤러가 @Async 메서드를 호출
  4. TaskExecutor의 스레드풀에서 @Async 메서드 실행 (Tomcat과 별개)
  5. Tomcat의 Worker Thread는 즉시 반환됨 (비동기 작업이 시작됨)
  6. 비동기 작업이 끝나면 TaskExecutor의 스레드가 응답을 반환

Tomcat의 스레드는 요청을 처리하는 즉시 반환되고, 실제 작업은 TaskExecutor의 스레드에서 실행된다.

 

즉, 비동기 요청은 클라이언트가 응답을 기다리지 않는다.

 

✅ 서로 다른 스레드를 사용한다면, 정보를 어떻게 넘기는가?

Spring의 @Async는 프록시 객체가 비동기 실행을 위한 TaskExecutor에 작업을 위임하는 방식으로 동작한다.

요청이 오면 프록시 객체가 먼저 메서드를 가로채고, 이를 비동기 실행을 담당하는 스레드풀에 넘긴다.

이 과정에서 톰캣 스레드는 즉시 반환되며, 비동기 스레드가 실제 로직을 수행하게 된다.

이 프록시 패턴은 AOP(Aspect-Oriented Programming)의 핵심 개념과 연결되는데,
Spring은 트랜잭션(@Transactional), 로깅, 보안 처리 등 다양한 기능을 프록시 기반의 AOP 기법으로 구현한다.
다음 기회에 프록시 객체의 동작 방식과 AOP가 어떻게 활용되는지 더 깊이 다뤄보겠다.

 

✅ TaskExecutor는 어떻게 동작하는가?

TaskExecutor는 Executor를 상속받는다.
Executor는 Java의 ExecutorService를 기반으로 구현된다.

Spring Boot의 비동기 처리는 TaskExecutor를 사용하며, 내부적으로 Java의 ExecutorService를 활용한다.

Tomcat과 완전히 독립적으로 동작하며, 아래와 같은 특징을 가진다.

위의 AsyncConfig 클래스를 참고해주세요.

📌 TaskExecutor의 특징

    • @Async 메서드 실행 시, TaskExecutor의 스레드를 사용
    • Tomcat의 Worker Thread와 독립적으로 동작 (서로 영향을 주지 않음)
    • 비동기 요청이 많아도 Tomcat의 스레드풀 부담이 늘어나지 않음
    • ThreadPoolTaskExecutor(ExecutorService의 구현체)를 활용하여 동적으로 스레드 개수 관리 가능

 

📌 TaskExecutor의 동작 방식

  • ThreadPoolTaskExecutor는 내부적으로 코어 스레드 수(CorePoolSize), 최대 스레드 수(MaxPoolSize), 대기 큐(QueueCapacity) 설정 가능
  • 요청이 많아지면, 대기 큐가 가득 차기 전까지는 새로운 스레드 생성 가능
  • 최대 스레드 수 도달 시, 대기 큐에 요청 저장
  • 대기 큐가 가득 차면 새로운 요청을 거부하거나 예외 발생

 

즉, 비동기 요청이 폭증하면 TaskExecutor의 설정에 따라 응답이 지연될 수도 있으므로 적절한 설정이 필요하다.

 

✅ TaskExecutor와 ExecutorService의 차이점

위에서 Spring Boot의 TaskExecutor는 내부적으로 Java의 ExecutorService를 활용한다고 설명했지만,

둘은 역할과 사용 방식에서 차이가 있다.

비교 항목 Java ExecutorService Spring Boot TaskExecutor
역할 Java 표준 스레드풀 관리 Spring의 비동기 스레드풀 관리
주요 구현체 ThreadPoolExecutor, ForkJoinPool ThreadPoolTaskExecutor
실행 방식 execute() 또는 submit() 사용 @Async 또는 CompletableFuture
스레드 관리 직접 설정 (corePoolSize, maxPoolSize, queueCapacity) Spring이 라이프사이클 관리
사용 목적 일반적인 병렬/비동기 작업 실행 Spring 기반의 비동기 작업 관리
컨텍스트 관리 Spring과 무관하게 동작 Spring 컨텍스트와 연계되어 관리됨

 

📌 TaskExecutor는 ExecutorService를 확장한 개념

  • TaskExecutor는 Executor를 상속받고 있으며, Java의 ExecutorSerivce를 기반으로 구현된다.
  • 하지만 Spring이 관리하는 비동기 컨텍스트와 함께 동작하도록 설계되었다.
  • Java의 ExecutorService를 직접 사용하는 것과 다르게, Spring이 스레드풀을 관리하며 라이프사이클을 자동으로 제어한다.

📌 Spring 컨텍스트와 연계되는 TaskExecutor

  • TaskExecutor는 Spring이 관리하는 Bean이므로, Spring 컨텍스트가 종료될 때 자동으로 스레드풀을 종료한다.
  • 반면, Java의 ExecutorService는 직접 shutdown()을 호출해야 한다.
  • 즉, Spring의 TaskExecutor는 개발자가 스레드풀을 직접 신경 쓰지 않도록 도와주는 역할을 한다.

📌 @Async 기반의 비동기 실행

  • Spring Boot에서 TaskExecutor는 @Async 어노테이션을 사용하여 실행된다.
  • 즉, 비동기 메서드를 호출하면 Spring이 내부적으로 TaskExecutor를 사용하여 새로운 스레드에서 실행한다.
  • Java의 ExecutorService를 직접 사용할 경우, submit()이나 execute()를 수동으로 호출해야 한다.
CompletableFuture는 Java에서 제공하는 비동기 처리 API로, ForkJoinPool을 기본 스레드풀로 사용한다.
이와 관련해서는 별도의 게시글로 설명할 예정이다.

 

 

✅ Tomcat과 TaskExecutor는 어떻게 연결되는가?

Tomcat의 Worker Thread는 단순히 HTTP 요청을 받고 컨트롤러를 실행하는 역할만 한다.

이후 비동기 작업을 실행하는 것은 오직 TaskExecutor의 역할이며, 두 스레드풀은 독립적으로 동작한다.

 

Tomcat의 스레드가 요청을 처리한 후, 비동기 작업을 TaskExecutor에서 실행하고,

최종적으로 TaskExecutor의 스레드가 결과를 클라이언트에 반환한다.

 

즉, Tomcat과 TaskExecutor는 직접적인 연결이 없고, 컨트롤러의 실행 과정에서 간접적으로 연결될 뿐이다.

 


5. 결론

이번 글에서는 이전 포스팅에서 잘못된 비교 대상으로 지적된 Java의 ExecutorService와 Spring Boot의 스레드풀(Tomcat)의 차이점을 분석하고, 올바른 비교군을 찾아 정리했다.

 

비동기 작업 처리를 이해하기 위해 Spring Boot의 TaskExecutor와 Java의 ExecutorService를 비교하면서, Spring Boot의 비동기 요청이 Tomcat의 스레드풀과 별개로 동작한다는 점을 살펴보았다.

 

다음 글에서는 HTTP 요청 처리 방식 비교를 다루면서 Tomcat의 스레드풀과 WebFlux(Netty)의 이벤트 루프 모델을 비교해볼 예정이다.

2025.03.23 - [삽질로그] - Tomcat은 그냥 서버가 아니다?

 

Tomcat은 그냥 서버가 아니다?

1. 들어가며이전 글에서 Java의 ExecutorService와 Spring Boot의 TaskExecutor를 비교하며, Spring Boot의 비동기 요청 처리가 Tomcat의 스레드풀과 별개로 동작한다는 점을 정리했다.  2025.03.16 - [삽질로그] - Spri

mingking2.tistory.com

 

'삽질로그' 카테고리의 다른 글

Tomcat 해부학 - Spring Boot 안에 살아있다?  (0) 2025.04.09
Tomcat은 그냥 서버가 아니다?  (0) 2025.03.23
여러 요청이 동시에 들어오면, Spring Boot는 어떻게 처리할까?  (2) 2025.03.09
RDS 보안 정책 때문에 접속 불가?  (0) 2025.03.04
복잡한 웹소켓 핸들러, Event로 깔끔하게 해결하기  (0) 2025.03.03
'삽질로그' 카테고리의 다른 글
  • Tomcat 해부학 - Spring Boot 안에 살아있다?
  • Tomcat은 그냥 서버가 아니다?
  • 여러 요청이 동시에 들어오면, Spring Boot는 어떻게 처리할까?
  • RDS 보안 정책 때문에 접속 불가?
mingking2
mingking2
에러와 삽질 속에서 성장하는 응애 개발자의 성장 일지
  • mingking2
    Mingking의 삽질 기록
    mingking2
  • 전체
    오늘
    어제
    • 분류 전체보기 (19)
      • 삽질로그 (10)
      • 스타트업 응애기 (1)
      • 프로젝트 도전기 (2)
      • DB 모르는 백엔드 탈출기 (5)
      • 네트워크 (1)
  • 블로그 메뉴

    • 홈
    • 태그
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    n:m
    server
    DB
    WebSocket
    @JoinColumn
    JPA
    Async
    Sync
    context
    CompletableFuture
    spring
    연관관계
    MVC
    orm
    Java
    플로우차트
    batch
    RDB
    servlet
    Tomcat
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
mingking2
Spring Boot 비동기 처리 = 스레드풀?
상단으로

티스토리툴바