LocaleContextHolder는 Spring 프레임워크에서 멀티스레드 환경의 현재 스레드에 바인딩된 Locale(지역 설정) 정보를 손쉽게 획득할 수 있도록 설계된 핵심 유틸리티 클래스입니다. 이 클래스는 ThreadLocal을 래핑하여, HttpServletRequest 객체를 명시적으로 전달하지 않고도 Service 계층이나 유틸리티 클래스 등 애플리케이션의 어느 곳에서든 현재 요청의 언어 환경을 가져올 수 있게 해줍니다.
다음은 LocaleContextHolder의 주요 API와 실제 활용 시나리오에 대한 상세 설명입니다.
핵심 API 분석
LocaleContextHolder가 제공하는 메서드는 크게 세 가지 범주로 나눌 수 있습니다: 정보 조회, 설정/초기화, 상속성 구성.
1. 정보 조회 (Getter)
| 메서드 시그니처 | 설명 | 대표 활용 예 |
|---|---|---|
getLocale() |
현재 스레드에 연결된 Locale을 반환합니다. 설정된 값이 없으면 JVM의 기본 Locale을 반환합니다. |
Service 계층에서 MessageSource를 통해 국제화 메시지를 가져올 때 사용합니다. |
getTimeZone() |
현재 스레드에 연결된 TimeZone을 반환합니다. 설정된 값이 없으면 JVM의 기본 시간대를 반환합니다. |
DateFormatter 등을 사용하여 날짜/시간 형식을 지정할 때 활용합니다. |
getLocaleContext() |
Locale과 TimeZone을 모두 포함하는 완전한 LocaleContext 객체를 반환합니다. |
언어와 시간대를 동시에 처리해야 하는 고급 시나리오에 사용합니다. |
2. 설정 및 초기화 (Setter & Reset)
| 메서드 시그니처 | 설명 | 대표 활용 예 |
|---|---|---|
setLocale(Locale locale) |
지정된 Locale을 현재 스레드에 바인딩합니다. |
커스텀 인터셉터에서 요청 파라미터(예: ?lang=ko)에 따라 언어를 설정할 때 사용합니다. |
setLocaleContext(LocaleContext context) |
Locale과 TimeZone을 함께 설정합니다. |
완전한 컨텍스트를 한 번에 설정해야 하는 상황에 활용합니다. |
resetLocaleContext() |
중요: 현재 스레드에 바인딩된 컨텍스트 정보를 제거합니다. | 비동기 스레드나 수동 설정 후 finally 블록에서 반드시 호출하여 메모리 누수나 스레드 재사용으로 인한 데이터 오염을 방지해야 합니다. |
3. 상속성 구성 (Inheritable)
기본적으로 ThreadLocal 데이터는 자식 스레드로 전달되지 않습니다. Spring은 이 문제를 해결하기 위해 "상속 가능" 변형을 제공합니다.
| 메서드 시그니처 | 설명 | 대표 활용 예 |
|---|---|---|
setLocale(Locale locale, boolean inheritable) |
Locale을 설정합니다. inheritable이 true이면 자식 스레드가 이 Locale을 자동으로 상속받습니다. |
비동기 태스크 시작 전에 자식 스레드가 부모 스레드의 언어 환경을 자동으로 사용하도록 할 때 사용합니다. |
setLocaleContext(LocaleContext context, boolean inheritable) |
위와 동일하나, 전체 컨텍스트 객체에 대해 적용합니다. | 위와 동일한 상황에서 사용합니다. |
주요 활용 시나리오 및 코드 예제
시나리오 1: Service 계층에서 국제화 메시지 조회 (가장 일반적)
이는 LocaleContextHolder의 가장 핵심적인 용도입니다. MessageSource와 결합하면 웹 계층이 아닌 곳에서도 간편하게 국제화를 구현할 수 있습니다.
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
private final MessageSource msgSource;
public OrderService(MessageSource msgSource) {
this.msgSource = msgSource;
}
public String getOrderStatusText(String statusCode) {
// 1. 현재 요청의 Locale 자동 획득 (예: ko-KR)
// 2. 획득한 Locale에 기반하여 적절한 메시지 조회
return msgSource.getMessage(statusCode, null, LocaleContextHolder.getLocale());
}
}
시나리오 2: 커스텀 인터셉터로 언어 동적 전환
브라우저의 Accept-Language 헤더 대신 URL 파라미터(예: ?lang=en)를 통해 언어를 강제로 전환하려면 인터셉터를 작성할 수 있습니다.
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.context.i18n.LocaleContextHolder;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.Locale;
@Component
public class LangSwitchInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String langParam = request.getParameter("lang");
Locale targetLocale;
if (langParam != null && !langParam.isBlank()) {
// 파라미터 파싱 (예: "ko_KR" -> Locale.KOREAN)
targetLocale = Locale.forLanguageTag(langParam.replace('_', '-'));
} else {
// 기본값으로 브라우저 헤더 사용
targetLocale = request.getLocale();
}
// 현재 스레드의 LocaleContext를 수동으로 설정
LocaleContextHolder.setLocale(targetLocale);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
// 요청 종료 후 반드시 정리 (스레드 풀 문제 방지)
LocaleContextHolder.resetLocaleContext();
}
}
시나리오 3: 비동기 스레드 처리 (주의사항 및 해결책)
ThreadLocal의 격리 특성 때문에, @Async 어노테이션이 적용된 메서드나 커스텀 스레드 풀 내부의 자식 스레드는 부모 스레드의 Locale을 기본적으로 획득할 수 없습니다.
잘못된 접근: 자식 스레드에서 직접 LocaleContextHolder.getLocale()을 호출하면 null 또는 시스템 기본 Locale을 반환받게 됩니다.
올바른 방법 A: 명시적 전달 (권장, 안전하고 명확함)
// 부모 스레드
Locale parentLocale = LocaleContextHolder.getLocale();
taskExecutor.execute(() -> {
try {
// 자식 스레드에 명시적으로 설정
LocaleContextHolder.setLocale(parentLocale);
// 비즈니스 로직 실행...
} finally {
// 반드시 정리
LocaleContextHolder.resetLocaleContext();
}
});
올바른 방법 B: 상속 가능 플래그 사용
// 설정 시 inheritable 플래그를 true로 지정
LocaleContextHolder.setLocale(userLocale, true);
// 이후 생성된 자식 스레드(표준 Thread 생성 방식인 경우)는 자동으로 상속받습니다.
// 그러나 스레드 풀 환경에서는 여전히 TaskDecorator 등을 이용한 명시적 관리가 권장됩니다.
시나리오 4: 통합 예외 처리
@ExceptionHandler(BusinessException.class)
public ResponseEntity<String> handleBusinessFailure(BusinessException ex) {
// 현재 요청의 언어 정보 획득
Locale currentLocale = LocaleContextHolder.getLocale();
// 해당 언어로 번역된 에러 메시지 반환
String localizedMessage = msgSource.getMessage(ex.getErrorCode(), null, currentLocale);
return ResponseEntity.status(400).body(localizedMessage);
}
주의 사항
- 웹 환경 의존성:
LocaleContextHolder의 데이터는 기본적으로DispatcherServlet에 의해 초기화됩니다.main메서드나@Scheduled태스크 같은 비웹 환경에서 직접 호출할 경우, 수동으로setLocale을 호출하지 않으면 기본값이나 빈 값을 반환할 수 있습니다. - 리소스 정리 필수: 인터셉터나 필터에서
setLocale을 수동으로 호출했다면, 반드시finally블록에서resetLocaleContext()를 호출해야 합니다. Tomcat과 같은 웹 컨테이너는 스레드를 재사용하므로, 이전 요청의 언어 설정이 다음 요청을 "오염"시킬 위험이 있습니다. Locale.getDefault()와의 차이점:Locale.getDefault(): 서버 운영체제의 기본 언어를 반환합니다 (전역 정적 값).LocaleContextHolder.getLocale(): 현재 사용자 요청의 언어를 반환합니다 (동적 스레드 바인딩 값).