티스토리 뷰
Locale 공통화는 다국어 서비스를 운영할 때 필수적인 인프라로, 모든 API가 동일한 언어 판별 규칙을 따르도록 일원화한 작업입니다. 이를 통해 코드 중복을 줄이고 서비스 전반의 언어 일관성을 확보했습니다.
왜 Locale이 필요한가?
저희 서비스는 다국어(한국어, 영어) 를 지원합니다.
동적 데이터는 하나의 DB 컬럼을 JSON 형태로 관리하고 있습니다.
예를 들어 블로그 글이 있다고 해봅시다.
글의 제목은 한국어와 영어를 모두 지원해야 하므로 모델은 이렇게 정의됩니다.
class Post {
title: {ko: "Spring Locale 공통화 여정", en: "The Journey to Unify Locale Management in Spring" }
}
이제 이 데이터를 가지고 DTO에서 lang을 받아 응답으로는 요청이 들어온 Lang의 데이터만 내려주도록 구현하였습니다. 이 과정에서 코드 곳곳에서 언어 처리를 제각각 구현하다 보니, 동일한 요청이라도 API마다 다른 규칙으로 언어를 선택하는 문제가 생겼습니다. 누군가는 lang 파라미터만 읽고, 다른 곳은 Accept-Language 헤더만 보고, 기본값도 제 멋대로였습니다. 이런 혼선을 막기 위해 “언어 판별 로직은 한 곳에서 관리한다”는 원칙으로 Locale 설정을 통합했습니다.
설계 목표
- 단일 진실 공급원(Single Source of Truth): 어떤 API든 동일한 룰을 사용.
- 우선순위 명시: language(또는 lang) query → Accept-Language 헤더 → 기본값.
- 지원 언어 목록/기본 언어 재활용: 도메인에서 이미 관리하던 상수를 재사용.
- 확장 용이성: 언어가 늘어나면 설정에서만 수정.
🌍 LocaleResolver란?
Spring MVC에서 LocaleResolver 는
“사용자의 요청(Request)을 기반으로 어떤 언어(Locale)를 적용할지 결정하는 전략 인터페이스” 입니다.
즉, 클라이언트가 보낸 요청의 헤더, 파라미터, 세션 등의 정보를 읽어 “이 요청은 어떤 언어로 응답해야 할까?” 를 판단하고 Locale을 반환합니다.
✅ 주요 역할
- Locale 결정 (resolve)→ resolveLocale(HttpServletRequest request): Locale
- 요청으로부터 Locale을 추출하고, 해당 요청이 어떤 언어로 처리되어야 하는지 결정합니다.
- Locale 변경 (set)→ setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale)
- 사용자가 언어를 변경할 수 있도록 Locale을 설정하거나 저장하는 역할을 수행할 수도 있습니다.
✅ Spring에서 제공하는 기본 구현체
구현체 주요 특징 사용 예시
| AcceptHeaderLocaleResolver | 브라우저의 Accept-Language 헤더 기반으로 Locale 결정 | 대부분의 REST API 서비스 |
| SessionLocaleResolver | 세션에 Locale을 저장하고 재사용 | 로그인 기반의 웹 서비스 |
| CookieLocaleResolver | 쿠키에 Locale 정보를 저장하여 재사용 | 사용자의 언어 설정을 기억해야 하는 웹 사이트 |
⚙️ 동작 흐름
요청이 들어올 때, Spring MVC는 다음 순서로 Locale을 결정합니다.
flowchart TD
A[사용자 요청 수신] --> B[DispatcherServlet]
B --> C[LocaleResolver 선택]
C --> D[resolveLocale() 호출]
D --> E[요청에서 Locale 정보 추출]
E --> F{Locale 정보 유효한가?}
F -->|Yes| G[Locale 반환]
F -->|No| H[기본 Locale 반환]
G --> I[Handler / Controller 실행]
H --> I
- DispatcherServlet은 요청을 받을 때 LocaleResolver를 사용해 Locale을 결정
- resolveLocale() 메서드가 실제 Locale 판별 로직을 담당
- 결과적으로, Controller나 View에서 LocaleContextHolder.getLocale()을 통해 현재 Locale에 접근 가능
구현 핵심
@Configuration
class LocaleConfig {
@Bean
fun localeResolver(): LocaleResolver {
return LocaleResolver(
supportedLanguageOrder = SUPPORTED_LANGUAGE_ORDER,
defaultLanguage = DEFAULT_LANGUAGE,
)
}
}
1. LocaleResolver 커스터마이징
- AcceptHeaderLocaleResolver를 상속해 Spring MVC 진영에 자연스럽게 끼워 넣었습니다.
- 지원 언어는 SUPPORTED_LANGUAGE_ORDER에서 가져와 LinkedHashSet으로 보존해 순서를 유지합니다.
2. Locale Request 우선순위
override fun resolveLocale(request: HttpServletRequest): Locale {
return determineLocale(request)
}
private fun determineLocale(request: HttpServletRequest): Locale {
// 1) language / lang query
normalizeLanguage(request.getParameter("language"))?.let { return it }
normalizeLanguage(request.getParameter("lang"))?.let { return it }
// 2) Accept-Language
parseAcceptLanguage(request.getHeader(HttpHeaders.ACCEPT_LANGUAGE))?.let { return it }
// 3) fallback
return fallbackLocale
}
- language 와 lang 두 파라미터를 모두 허용해 레거시와의 호환성을 유지했습니다.
- 모든 입력은 trim → lowercase → 지원 언어인지 검증을 거칩니다.
3. Header 파싱 안정화
private fun parseAcceptLanguage(header: String?): Locale? {
if (header.isNullOrBlank()) return null
val ranges = runCatching { Locale.LanguageRange.parse(header) }
.getOrDefault(emptyList())
for (range in ranges) {
val normalized = when (val tag = range.range) {
"*" -> fallbackLocale.language
else -> tag.substringBefore('-').lowercase(Locale.ROOT)
}
if (normalized in supportedLanguageCodes) {
return Locale.forLanguageTag(normalized)
}
}
return null
}
- Locale.LanguageRange.parse를 사용해 q 값 가중치까지 고려.
- *와 같이 모든 언어를 허용한다면 곧바로 기본 언어를 반환.
- 국가 코드(/en-US)가 붙어도 언어 코드 부분만 뽑아 일관된 비교를 수행합니다.
적용 효과
- 코드 중복 제거: 모든 컨트롤러에서 LocaleResolver를 공유하므로, 각 API는 언어 우선순위를 다시 구현할 필요가 없습니다.
- 버그 전파 차단: 언어 처리 정책이 바뀌어도 LocaleConfig 한 곳만 수정하면 됩니다.
- 전체 도메인 일관성: 블로그뿐 아니라 Update Notes 등 다른 도메인에서도 동일한 언어 판단 기준을 쓰게 됐습니다.
확장 포인트
- 지원 언어 추가: SUPPORTED_LANGUAGE_ORDER만 수정하면 자동 반영.
- 서비스별 기본 언어: 필요하다면 Resolver를 bean으로 두 개 이상 정의하거나, LocaleContextHolder 기반으로 추가 커스터마이징 가능.
- 테스트: MockMvc에서 Accept-Language를 다양하게 넣어 regression test를 만들면 안정성이 더 올라갑니다.
마무리
Locale 공통화는 눈에 띄는 신규 기능은 아니지만, 글로벌 서비스를 운영할수록 필수 인프라입니다. 작은 규칙 하나가 API 전체의 일관성을 지키는 기반이 되며, 이후 다국어 콘텐츠를 확장할 때도 든든한 바탕이 됩니다. WegglePlus는 이런 작은 경험을 계속 쌓아가며 “언제 어디서든 같은 목소리”를 들려주는 서비스를 목표로 하고 있습니다.
About WegglePlus
WegglePlus는 문제를 정의하고, 이를 해결하는 여러 개의 서비스를 설계·개발·운영합니다.
이 블로그는 그 과정에서의 기술적 선택과 시행착오를 공유합니다.
https://weggle-plus.co.kr
'개발 일기' 카테고리의 다른 글
| 블로그의 RSS 피드 구현하기 (0) | 2026.01.09 |
|---|---|
| Refresh Token은 쿠키, Access Token은 메모리에 (1) | 2025.12.19 |
| 디자인 시스템을 위한 로딩 스피너 컴포넌트 제작기 (0) | 2025.11.06 |
| 혼자 개발하다 보니 코드가 제멋대로… 그래서 Prettier를 깔았다 (1) | 2025.10.19 |
| 데이터 수집을 위한 Clarity 설정기 (0) | 2025.10.08 |
