Spring 요청이 어떻게 ThreadLocal에 바인딩 될까?
Spring의 RequestContextHolder와 ThreadLocal 기반 요청 바인딩
Spring MVC는 요청 단위로 다양한 데이터를 처리한다. 이 과정에서 “현재 요청의 컨텍스트 정보”를 스레드 내 어디서든 접근할 수 있도록 하는 메커니즘이 필요하다. 이를 가능하게 하는 핵심 구조가 ThreadLocal 기반 요청 바인딩이며, 중심 역할을 하는 클래스가 RequestContextHolder다.
아래에서는 요청이 들어와서 ThreadLocal에 바인딩되고, 다시 해제되는 전체 흐름과
내부 동작 원리, 그리고 관련 주의사항을 정리한다.
1. 주요 개념 요약
용어역할 / 의미
| 스레드 풀 | WAS(Tomcat, Jetty 등)가 다중 요청 처리를 위해 미리 생성해둔 스레드 집합 |
| ThreadLocal | 각 스레드마다 독립적인 저장 공간을 제공하는 변수 컨테이너 |
| RequestAttributes | Spring에서 HTTP 요청 관련 속성을 담는 추상 인터페이스 (요청 객체, 세션 등) |
| RequestContextHolder | 현재 스레드에 RequestAttributes를 바인딩·조회·해제하는 헬퍼 클래스 |
| ServletRequestAttributes | RequestAttributes의 구현체. 내부에 HttpServletRequest, HttpServletResponse 포함 |
| DispatcherServlet | Spring MVC의 프론트 컨트롤러. 요청 처리의 시작과 끝 담당 |
2. 요청 바인딩 흐름 (입장 → 처리 → 정리)
HTTP 요청이 들어왔을 때 RequestAttributes가 ThreadLocal에 저장되고 해제되는 과정은 다음과 같다.
- 클라이언트 요청이 WAS(Tomcat 등)에 도착
- WAS는 스레드 풀에서 스레드 하나를 꺼내 요청 처리 담당
- 해당 스레드가 DispatcherServlet의 service() 또는 doDispatch() 메서드 진입
- DispatcherServlet 내부 동작
- ServletRequestAttributes 생성 (요청 및 응답 포함)
- RequestContextHolder.setRequestAttributes(attributes, inheritableFlag) 호출
- → 현재 스레드의 ThreadLocal에 RequestAttributes 저장
- 이후 컨트롤러, 서비스, DAO 등에서 요청 처리 진행
- 요청 처리 완료 또는 예외 발생 시 finally 블록 등에서 정리
- RequestContextHolder.resetRequestAttributes() 호출 → ThreadLocal 값 제거
- 응답 반환, 스레드는 스레드 풀로 반환
- 이후 이 스레드는 다른 요청을 처리할 때 재사용될 수 있음
→ 요청 처리 중 어디서든 RequestContextHolder.getRequestAttributes()를 호출하면 현재 요청에 해당하는 속성 객체를 얻을 수 있고, 여기서 getRequest() 등을 통해 원본 HttpServletRequest 접근이 가능하다.
3. RequestContextHolder 내부 구조 (ThreadLocal 저장 + 조회 + 제거)
RequestContextHolder의 내부 구조는 단순하지만 핵심적이다.
public abstract class RequestContextHolder {
private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
new NamedThreadLocal<>("Request attributes");
private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =
new NamedInheritableThreadLocal<>("Request context");
public static void setRequestAttributes(RequestAttributes attributes, boolean inheritable) {
if (attributes == null) {
resetRequestAttributes();
} else {
if (inheritable) {
inheritableRequestAttributesHolder.set(attributes);
requestAttributesHolder.remove();
} else {
requestAttributesHolder.set(attributes);
inheritableRequestAttributesHolder.remove();
}
}
}
public static RequestAttributes getRequestAttributes() {
RequestAttributes ra = requestAttributesHolder.get();
if (ra == null) {
ra = inheritableRequestAttributesHolder.get();
}
return ra;
}
public static void resetRequestAttributes() {
requestAttributesHolder.remove();
inheritableRequestAttributesHolder.remove();
}
}
핵심 요약
- setRequestAttributes(…)
- → 스레드 전용 ThreadLocal 또는 상속 가능한 InheritableThreadLocal 중 하나에 저장
- getRequestAttributes()
- → 일반 ThreadLocal에서 먼저 조회, 없으면 inheritable 쪽 확인
- resetRequestAttributes()
- → 두 저장소 모두 제거 → 메모리 누수 방지
- inheritable = true 설정 시 자식 스레드로 컨텍스트 전달 가능
→ 기본은 스레드 독립 저장 구조지만, 필요 시 자식 스레드에도 요청 속성 전파가 가능하다.
4. 트랜잭션 등 인프라 레벨 ThreadLocal 사용 구조
Spring의 인프라 계층(트랜잭션, 보안, 로깅 등)도 모두 ThreadLocal 기반 구조를 사용한다. RequestContextHolder 외에도 TransactionSynchronizationManager가 대표적인 예다.
주요 ThreadLocal 필드
필드명설명
| resources | 현재 스레드에 바인딩된 리소스 맵 (DB 커넥션, JPA EntityManager 등) |
| synchronizations | 트랜잭션 완료 시 실행할 콜백 목록 |
| currentTransactionName | 현재 트랜잭션 이름 |
| currentTransactionReadOnly | 트랜잭션이 읽기 전용인지 여부 |
동작 흐름
- 트랜잭션 시작
TransactionSynchronizationManager.initSynchronization();
- → 현재 스레드의 ThreadLocal 구조를 초기화 (빈 저장소 생성)
- 리소스 바인딩
TransactionSynchronizationManager.bindResource("DataSource", connection);
- → 현재 스레드의 resources 맵에 커넥션 바인딩
- → 이 스레드 내에서 실행되는 DAO·Repository는 동일 커넥션을 공유
- 트랜잭션 종료 (커밋/롤백)
- 등록된 콜백(synchronizations) 실행
- 리소스 정리 및 ThreadLocal 초기화
- 내용 클리어
TransactionSynchronizationManager.clear();
5. ThreadLocal 사용 시 주의사항 및 한계 (비동기 / 스레드 전환)
ThreadLocal은 현재 스레드 내에서만 유효하다는 점이 가장 중요하다. 즉, 스레드가 변경되거나 재사용되는 환경에서는 반드시 주의해야 한다.
| 비동기 처리 (@Async, CompletableFuture 등) | 비동기 스레드는 기존 요청 스레드와 다르므로, RequestContextHolder.getRequestAttributes() 호출 시 null 또는 IllegalStateException 발생 가능 | 비동기 실행 전에 현재 RequestAttributes 캡처 → 새 스레드에 복원 → 실행 후 정리 |
| 스레드 풀 재사용 | ThreadLocal 값이 남아 있으면 다음 요청에 영향 (컨텍스트 누수) | 요청 처리 완료 시 resetRequestAttributes() 호출 필수 |
| InheritableThreadLocal 과다 사용 | 자식 스레드로 불필요한 데이터가 전파될 수 있음 | 필요한 경우에만 사용 |
비동기 처리 시 컨텍스트 전파 문제
아래는 비동기 코드에서 발생할 수 있는 문제 예시이다.
@RequestMapping("/async")
public void asyncExample() {
CompletableFuture.runAsync(() -> {
// 새로운 스레드에서는 RequestContextHolder 값이 없음
System.out.println(RequestContextHolder.getRequestAttributes()); // null
});
}
- CompletableFuture.runAsync()는 새로운 스레드에서 실행된다.
- ThreadLocal은 스레드 단위이므로 기존 요청 스레드의 값이 복사되지 않는다.
→ 결과적으로 비동기 코드에서는 요청 컨텍스트(RequestAttributes)가 끊기게 된다.
Spring의 ThreadPoolTaskExecutor에는 setTaskDecorator(TaskDecorator) 메서드가 존재하며, 이를 이용하면 Runnable이 실행되기 전후에 래퍼를 삽입할 수 있습니다.
executor.setTaskDecorator(new TaskDecorator() {
@Override
public Runnable decorate(Runnable runnable) {
RequestAttributes context = RequestContextHolder.getRequestAttributes();
return () -> {
try {
if (context != null) {
RequestContextHolder.setRequestAttributes(context);
}
runnable.run();
} finally {
RequestContextHolder.resetRequestAttributes();
}
};
}
});
스레드 풀 재사용 시 주의
WAS의 스레드는 풀로 관리되기 때문에, ThreadLocal 값이 제거되지 않으면 다음 요청에서 이전 데이터가 남을 수 있다.
예시 시나리오:
- A 사용자의 요청 → 스레드 #1 사용 → ThreadLocal에 A 정보 저장
- 정리하지 않고 스레드 반환
- B 사용자의 요청 → 같은 스레드 #1 재사용 → A의 정보가 그대로 남아 있음
이러한 상황은 보안 문제 및 데이터 혼동을 유발할 수 있다. 반드시 요청 처리 종료 시점에 ThreadLocal을 remove() 또는 resetRequestAttributes()로 정리해야 한다.
InheritableThreadLocal 사용 시 주의
InheritableThreadLocal은 부모 스레드의 값을 자식 스레드로 복사한다. 하지만 비동기나 병렬 처리 환경에서 불필요한 컨텍스트가 전파되어 다른 요청의 데이터가 섞이는 경우가 발생할 수 있다. → 기본적으로 일반 ThreadLocal을 사용하고, 정말 필요한 경우에만 inheritable = true를 설정하는 것이 안전하다.
6. 전체 요청 흐름 요약
- 요청 진입 → WAS가 스레드 할당
- DispatcherServlet 진입 → ServletRequestAttributes 생성
- RequestContextHolder.setRequestAttributes() 호출 → ThreadLocal에 저장
- 컨트롤러, 서비스, DAO 등 동일 스레드 내에서 요청 컨텍스트 접근 가능
- 요청 처리 완료 또는 예외 발생 시 resetRequestAttributes()로 정리
- 스레드 반환 및 재사용 가능 상태로 복귀
부가적으로, Spring의 트랜잭션 관리, 보안 컨텍스트, 로깅(MDC) 등도 모두 ThreadLocal을 활용해 “격리된 현재 스레드 문맥”을 유지한다.
정리
RequestContextHolder는 Spring MVC 요청 처리의 핵심 기반 구조로, 별도의 파라미터 전달 없이도 서비스나 유틸리티 클래스 등 어디서든 현재 요청 정보를 조회할 수 있게 해줍니다.
하지만 ThreadLocal 기반 구조를 안전하게 운영하기 위해서는 반드시 두 가지를 기억해야 합니다.
- 정확한 정리(remove/reset 호출): Tomcat 등 WAS의 스레드 풀 재사용 시 이전 상태가 남아 발생하는 메모리 누수나 데이터 혼선 방지
- 비동기 전파 처리:
@Async등 스레드 전환 시의 컨텍스트 유지 관리
ThreadLocal은 편리하지만 수명 관리가 소홀하면 치명적인 버그로 이어질 수 있으므로, 바인딩과 해제의 생명주기를 완벽히 이해하고 사용하는 것이 무척 중요합니다.
Comments