왜 이걸 알아야 할까?
개발 중에 이런 상황을 겪어볼 수 있다.
- 스레드가 고갈돼서 서버가 멈춘다.
- @Async 붙였는데 오히려 느려졌다.
- WebClient 썼는데 로그 순서가 뒤죽박죽이다.
- ThreadLocal 데이터가 갑자기 사라진다.
이 문제들의 공통점은 "스레드" 이다.
내가 만든 코드가 실제로 어떤 스레드에서 돌아가고 있는지 모른다면, 성능파악과 튜닝도 어려울것이다.
그래서 HTTP 요청 하나당 실제로 몇 개의 스레드가 작동하는지 환경별로 한번 알아보자.
환경 | 설명 |
Spring MVC | 기본 동기 처리(Tomcat) |
Spring MVC + @Async | 일부 작업 비동기 분리 |
Spring MVC + WebClient | 외부 API 호출 시 비동기 |
WebFlux + Netty | 전체 논블로킹, 이벤트 기반 |
요청 하나 = 스레드 하나?는 옛말?
보통 아래와 같이 생각한다.
이 구조는 맞다 그런데 여기에 다음이 붙는 순간 스레드가 복제된다.
- @Async 사용
- WebClient 비동기 콜백
- 로깅 비동기 처리
- Netty 기반 이벤트 루프
각 케이스를 로그와 함께 한번 알아보자.
1. [Spring MVC] 동기 방식
@RestController
public class TestController {
@GetMapping("/sync")
public String sync() {
log.info("스레드: {}", Thread.currentThread().getName());
return "ok";
}
}
실행 결과
- Controller → Service → Repository → 응답까지 모두 하나의 워커 스레드(exec-4) 에서 작동한다.
- 요청 하나 = 스레드 하나 (정확)
2. [Spring MVC + @Async] - 작업을 나눴더니 스레드가 늘어났다.
@Service
public class MyService {
@Async
public void doAsync() {
log.info("비동기 스레드: {}", Thread.currentThread().getName());
}
}
@GetMapping("/async")
public String async() {
log.info("요청 스레드: {}", Thread.currentThread().getName());
myService.doAsync();
return "ok";
}
실행 결과
- 요청은 exec-1 이 처리했다.
- 내부 로직은 별도 스레드(task-1)로 넘어갔다.
- 요청은 하나지만 2개의 스레드가 동작했다.
3. [Spring MVC + WebClient] - 응답을 기다리는 동안 다른 스레드가 개입
@GetMapping("/webclient")
public Mono<String> callApi() {
log.info("요청 시작: {}", Thread.currentThread().getName());
return webClient.get()
.uri("http://localhost:8081/slow")
.retrieve()
.bodyToMono(String.class)
.doOnNext(response -> log.info("응답 받은 스레드: {}", Thread.currentThread().getName()));
}
실행 결과 (Spring WebClinet + 비동기)
- 시작은 exec-2 에서 진행했다.
- WebClient는 내부적으로 다른 스레드 풀에서 응답을 처리했다.
- 요청 하나에 최소 2개 이상의 스레드가 동작했다.
4. [Spring WebFlux + Netty] - 더 이상 스레드 흐름은 예측 불가
@GetMapping("/webflux")
public Mono<String> reactive() {
log.info("요청 시작: {}", Thread.currentThread().getName());
return Mono.just("응답")
.delayElement(Duration.ofMillis(500))
.doOnNext(data -> log.info("응답 처리 스레드: {}", Thread.currentThread().getName()));
}
실행 결과
- 요청은 reactor-http-nio-* 에서 수신했다.
- 응답은 완전히 다른 워커 스레드에서 처리했다 (parallel-*)
- 1 요청에 최소 2개 이상 스레드, 심지어 바뀔 수도 있다.
정리해보자면
환경 | 요청당 스레드 수 | 특징 |
Spring MVC | 1 | 예측 가능, 단순 |
MVC + @Async | 2+ | 스레드 분기 |
MVC + WebClient | 2+ | 응답 처리 별도 |
WebFlux | 2 ~ n | 스레드 다변화, 예측 어려움 |
스레드가 바뀌면 ThreadLocl에 저장한 값은 사라지고,
로깅, 트랜잭션, MDC, 세션 등 "스레드 고정이 전제된 기능"이 깨질 수 있다.
'Back-End > Java' 카테고리의 다른 글
할머니도 이해할 수 있는 자바 Thread (0) | 2025.05.01 |
---|---|
try-catch (0) | 2024.07.06 |
스레드 (0) | 2024.07.05 |
프로세스 (0) | 2024.07.04 |
Java HTTP 통신 (1) | 2024.06.12 |