프로세스와 스레드, 그리고 나의 프로젝트
프로세스와 스레드를 처음 배웠을 때, 나는 아래와 같이 이해를 했었다.
- 프로세스: 운영체제(OS) 관점에서 하나의 프로그램이 실행되는 단위.
- 스레드: 실행된 프로세스 안에서 동작하는 더 작은 실행 단위.
개념은 어렴풋이 알았지만, 내가 만드는 스프링 부트 프로젝트와 이 개념들이 실질적으로 어떻게 연결되는지 명확하게 짚고 넘어가고 싶어 정리가 필요했다.
✅ 프로세스와 스레드의 관계: JVM, 톰캣, 스프링
java -jar myapp.jar
명령어로 스프링 애플리케이션을 실행하면, 운영체제는 JVM(Java Virtual Machine)을 하나의 프로세스로 인식하고 실행한다.
그리고 이 JVM 프로세스 내부에서는 수많은 스레드들이 각자의 역할을 수행하며 동작한다. 이 스레드들은 역할에 따라 관리 주체가 나뉘는데, 예를 들어 main
메소드를 실행하는 스레드, JIT 컴파일러 스레드 등 핵심적인 스레드는 JVM이 직접 관리한다.
특히, 웹 요청과 관련된 스레드들은 내장 웹 서버인 톰캣(Tomcat)이 관리 한다. 스프링 웹 개발자로서 우리가 주로 다루게 되는 스레드가 바로 이 톰캣의 스레드 풀에 있는 스레드들이다.
최종 정리
- 프로세스(Process): JVM 자체가 하나의 프로세스입니다.
- 스레드(Thread): JVM 프로세스 안에서 실행되는 작은 실행 단위입니다.
- 관리 주체: 스레드는 역할에 따라 JVM, 톰캣, 스프링 등에 의해 관리됩니다.
관리 주체 | 관리 대상 스레드 |
---|---|
JVM | main 스레드, 가비지 컬렉터(GC) 스레드, JIT 컴파일러 스레드 등 |
톰캣 | 클라이언트의 웹 요청을 처리하는 HTTP 요청 처리 스레드 |
스프링 | @Async 등 비동기 작업을 처리하는 별도의 스레드 풀 스레드 |
❓ 질문 1: 웹 요청이 오면 스레드가 계속 새로 생기나요?
“웹 요청이 오면 톰캣이 스레드를 하나 만들고, 그 요청이 내 스프링 애플리케이션 로직을 타면 스프링이 또 다른 스레드를 만드는 건가요?”
결론부터 말하면, 아닙니다.
대부분의 경우 톰캣으로부터 할당받은 스레드 하나가 요청의 시작부터 끝까지를 전담합니다. 하지만 @Async 같은 특정 상황에서는 새로운 스레드가 사용되기도 합니다.
일반적인 요청 처리 과정
- 클라이언트로부터 HTTP 요청이 들어오면, 톰캣은 미리 준비해둔 스레드 풀(Thread Pool)에서 스레드를 하나 할당합니다. 이 스레드가 요청 처리의 주인공이 됩니다.
- 이 스레드는 스프링의
DispatcherServlet
을 호출하고,DispatcherServlet
은 요청에 맞는 컨트롤러 메서드를 찾아 실행합니다. - 이 모든 과정은 처음에 할당된 톰캣 스레드 위에서 순차적으로 실행됩니다.
@Async
를 사용할 경우
만약 컨트롤러가 @Async
어노테이션이 붙은 메서드를 호출하면 상황이 달라집니다.
@Async
메서드는 기존 톰캣 스레드에서 바로 실행되지 않고, 스프링이 관리하는 별도의 스레드 풀로 작업이 위임됩니다.
스프링은 이 스레드 풀에서 새로운 스레드를 꺼내 @Async
메서드를 실행시키고, 기존 톰캣 스레드는 자신의 다음 작업(예: 클라이언트에게 최종 응답 전송)을 계속 진행합니다.
❓ 질문 2: 스프링의 스레드 풀도 직접 설정할 수 있나요?
네, 맞습니다.
톰캣의 스레드 풀 설정을 application.yml
등에서 제어할 수 있듯이, 스프링이 @Async
에 사용하는 스레드 풀도 직접 설정할 수 있습니다. ThreadPoolTaskExecutor
를 Bean
으로 등록하여 세밀하게 제어해 보겠습니다.
1단계: @EnableAsync
어노테이션 추가
먼저, 스프링 부트 애플리케이션에 비동기 기능을 활성화하기 위해 @EnableAsync
어노테이션을 추가합니다.
@SpringBootApplication
@EnableAsync
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
2단계: ThreadPoolTaskExecutor
빈(Bean) 등록
다음으로, 스레드 풀의 상세 설정을 담은 ThreadPoolTaskExecutor
를 설정 클래스에 Bean
으로 등록합니다.
@Configuration
public class AsyncConfig {
@Bean(name = "asyncThreadPool")
public Executor asyncThreadPool() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 🛠️ 스레드 풀 핵심 설정
executor.setCorePoolSize(5); // 기본 스레드 수
executor.setMaxPoolSize(10); // 최대 스레드 수
executor.setQueueCapacity(50); // 대기 큐 사이즈
executor.setThreadNamePrefix("async-task-"); // 스레드 이름 접두사
executor.initialize(); // 스레드 풀 초기화
return executor;
}
}
corePoolSize
: 평상시 유지되는 최소 스레드 수 입니다.maxPoolSize
: 요청이 몰릴 때 최대로 생성할 수 있는 스레드 수입니다.queueCapacity
:corePoolSize
의 스레드가 모두 바쁠 때, 새로운 작업을 대기시키는 큐의 크기입니다. 큐가 가득 차면maxPoolSize
까지 스레드를 추가로 생성합니다.
3단계: @Async
에서 커스텀 스레드 풀 사용
이제 @Async
어노테이션에 위에서 정의한 Bean
의 이름을 명시하여, 우리가 직접 설정한 스레드 풀을 사용하도록 지정할 수 있습니다.
@Service
public class EmailService {
@Async("asyncThreadPool") // 위에서 정의한 빈 이름을 사용
public void sendEmail(String to, String subject, String body) {
// 이메일 전송 로직 (새로운 스레드에서 실행됨)
System.out.println("메일 전송 스레드: " + Thread.currentThread().getName());
// ...
}
}