Java/Spring

Spring에서의 Thread 와 Thread Pool

Z_Z 2025. 5. 21. 11:24
반응형

Spring boot 기준으로 톰캣에 어플리케이션를 올렸다고 생각해보자

  메인 쓰레드의 역할

Java 어플리케이션의 메인 쓰레드(Main Thread)는 프로젝트 내 프로젝트 명 + Application.java 파일을 보면

public static void main(String[] args) 메서드가 존재하는데 어플리케이션의 최초 실행 흐름을 담당하는

단 하나의 초기 진입점이다.

 

Spring Boot 어플리케이션을 실행하면 main() 메서드에서 시작된다.

@SpringBootApplication
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args); // 여기서 시작
    }
}

 

이때 메인 쓰레드가 하는 일

단계 설명
1 Spring Boot 어플리케이션 초기화 (SpringApplication.run)
2 ApplicationContext 생성 및 설정(DI, Bean 등록 등)
3 @ComponentScan, @Configuration, @Bean 처리
4 내장 톰캣(WebServer) 시작
5 @PostConstruct, CommandLineRunner, ApplicationRunner 실행
6 어플리케이션이 완전히 준비된 상태로 run() 에서 리턴

 

👉 이후에는 메인 쓰레드는 더이상 요청 처리나 스케줄링 등엔 관여하지 않는다. 단지 어플리케이션이 죽지 않게

유지하는 기본 thread group 의 일부로 존재한다.

 

✅ 애플리케이션이 동작 중에 사용하는 주요 쓰레드 종류

역할 사용하는 쓰레드
메인 쓰레드(Main Thread) 어플리케이션 실행을 담당(요청 처리나 스케줄링 등엔 관여하지 않음)
HTTP 요청 처리 톰캣의 worker thread(ThreadPoolExecutor)
@Scheduled TaskScheduler (기본 : 단일 쓰레드(ConcurrentTaskScheduler), ThreadPool 로 변경 가능)
@Async ThreadPoolTaskExecutor (비동기 작업용 ThreadPool)
DB 커넥션 처리 커넥션 풀의 쓰레드(예 : HikariCP, Commons-DBCP)
로그 처리 별도 로그 백엔드의 쓰레드 가능 (비동기 설정 시)

 

✅ 메인 쓰레드는 그 이후 어디에 남아 있을까?

메인 쓰레드는 사실 SpringApplication.run() 이후 종료되지 않고, 내부적으로 Tomcat 또는 Netty 서버가 돌아가면서

블로킹 상태로 살아있게 된다.

예를 들어 톰캣 내부에서 awat() 같은 루프를 돌면서 요청을 기다리므로 JVM 이 종료되지 않음

즉, 메인 쓰레드는 주로 "어플리케이션 생명 주기 유지" 에만 관여하게 된다.

┌─────────────────────┐
│   main() 실행 (Main Thread) ─────────────────────┐
└─────────────────────┘                            │
             │                                     ↓
             ↓                         ┌────────────────────────┐
 SpringApplication.run()              │ ApplicationContext 초기화 │
             ↓                         └────────────────────────┘
             ↓                                     ↓
     내장 톰캣 실행 → 톰캣 Worker Thread 시작        ↓
             ↓                                     ↓
  메인 쓰레드는 종료되지 않고 JVM을 지탱하는 상태로 남음

 

✅ 결론

  • 메인 쓰레드는 Spring Boot 어플리케이션 부팅(시작)에만 관여하며, 이후 대부분의 동작(웹 요청, 비동기, 스케줄링 등)은
    각각의 별도 쓰레드 / Thread Pool 에서 수행된다.
  • 메인 쓰레드는 종료되지 않지만, 실제 비즈니스 로직에는 거의 관여하지 않는다.

 

 

1. Thread vs ThreadPool


✔ Thread (직접 생성한 쓰레드)

Thread t = new Thread(() -> {
    // 작업 수행
});
t.start();


위 코드는 직접 OS 쓰레드를 생성하여 실행하는 방법이다
단점 : 많은 요청을 처리하려면 매번 새로운 쓰레드를 생성해야 하므로 비용이 크고 ThreadPool 보다 비효율적이다

✔ ThreadPool (스레드 풀)

ExecutorService executor = Executors.newFixedThreadPool(10);
executor.submit(() -> {
    // 작업 수행
});

 

미리 생성해놓은 Thread Pool에서 쓰레드를 재사용해 실행한다
@Async, @Scheduled, Web 요청 등은 대부분 해당 방법을 사용한다
Spring 에서는 대부분 TaskExecutor, TaskScheduler 등을 통해 사용된다

 

2. HTTP 요청이 들어왔을때

톰캣(서블릿 컨테이너)은 내부에 ThreadPool 을 가지고 있다
사용자 요청이 들어왔을 때 ThreadPool에 있는 worker thread 가 사용자 요청을 받아 처리한다
메인 쓰레드는 위 요청에 사용되지 않고 스프링 어플리케이션 실행만 담당한다

[사용자 요청] → [Tomcat Worker Thread] → [Controller 처리] → [서비스 호출]


3. @Async 사용 시 쓰레드 흐름

Spring에서 @Async 는 별도의 비동기 Thread Pool에서 실행된다

@Service
public class MyService {
    @Async
    public void asyncMethod() {
        // 이 코드는 별도 thread pool에서 실행됨
    }
}

 

흐름 예시 (Web 요청 시)

  1. 사용자가 HTTP 요청 > 톰캣의 worker thread 요청을 받아 요청만 @Async 메서드에 보내 처리한다
    (Controller > service > @Async)
  2. @Async 메서드는 Spring에서 비동기 ThreadPoolTaskExecutor 에서 새로운 쓰레드를 생성하여 처리한다
  3. 요청을 처리했던 기존 톰캣 worker thread 는 즉시 반환되거나 다음 로직을 수행한다.

 

4. 스케줄링 + @Async 동시 사용 시 쓰레드 흐름

예를 들어 아래처럼 60초마다 실행되는 스케줄러가 있고
내부에서 @Async 메서드를 반복 호출한다고 하자

@Scheduled(fixedRate = 60000)
public void schedulerTask() {
    for (int i = 0; i < 5; i++) {
        asyncService.doAsync();
    }
}

@Async
public void doAsync() {
    // 이 코드는 비동기 스레드 풀에서 실행됨
}

 

  1. @Scheduled 는 Spring의 TaskScheduler 가 실행한다
    (기본적으로 ConcurrentTaskScheduler 를 사용하여 싱글 쓰레드로 동작하며 변경 가능하다)
    ThreadPoolTaskScheduler 로 변경 가능
  2. schedulerTask 메서드는 TaskScheduler 의 쓰레드를 통해 실행된다
  3. 내부에서 @Async 의 doAsync() 를 호출하게 되면 비동기 executor 의 ThreadPool 에서 별도의 쓰레드로 실행된다
  4. 동시에 여러번 @Async 메서드를 호출하게 되면 비동기의 ThreadPoolTaskExecutor 에서 저장되어 있는 쓰레드로 실행된다
    풀에 남은 쓰레드가 없으면 대기(queue)하거나 풀 크기를 초과하면 예외가 발생한다


✅ 메인 쓰레드 vs 기타 쓰레드 요약

용도 사용되는 쓰레드
애플리케이션 실행(main()) 메인 쓰레드
HTTP 요청 처리 톰캣 worker thread (pool)
@Scheduled TaskScheduler 쓰레드
@Async TaskExecutor 쓰레드 (비동기 풀)
직접 생성한 Thread OS 수준의 개별 Thread

 


@Async 비동기 Executor 설정방법

@Configuration
@EnableAsync
public class AsyncConfig {
    @Bean
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(20);
        executor.setQueueCapacity(50);
        executor.setThreadNamePrefix("Async-");
        executor.initialize();
        return executor;
    }
}

 


✅ 마무리 요약

  • Spring Boot 에서 HTTP 요청, @Scheduled 의 스케줄링, @Async 의 비동기 처리 등은
    각각 다른 ThreadPool 을 사용한다
  • @Async 는 별도의 비동기 ThreadPoolTaskExecutor 에서 실행된다
  • 톰캣의 어플리케이션이 실행될 때 메인쓰레드가 생성되고 실행하는 역할만 한다. 거의 사용되지 않음!
  • 톰캣의 메인쓰레드를 제외한 ThreadPool 에 있는 worker thread 는 @Async 메서드에 호출만 하고
    실제 작업은 다른 쓰레드가 수행한다.

 

 

 

 

 

 

반응형