JSTL FMT는 다국어 내용을 처리하거나, 날짜와 같은 숫자나 문자의 형식화를 담당하는 템플릿 라이브러리이다.
이중에서 날짜를 형식화해서 나타내는 formatDate가 있는데 보통 사용법은 다음과 같다.
<fmt:formatDate value="${today}" pattern="yyyy년 MM월 dd일" />
컨트롤러단계에서는 Date객체를 넘기고, View(JSP)단에서 위와 같이 태그를 작성하면 패턴에 맞추어 문자열 결과가 나오도록 되어 있다. 가령 예를 들면 "1995년 12월 02일" 이런식으로 나오게 된다.
위와 같은 데이터는 보통 가입일이나, 데이터 수정 날짜, 혹은 생일을 나타낼 때 위의 코드를 이용할 수 있겠다.
그런데 갑자기 어떤 특정 유저만(특정 클라이언트만) 제대로 날짜가 나오지 않는다면 어떨까? 날짜값은 맞게 나오는데 패턴이 원하는대로 나오지 않는다면 무엇이 문제일까?
코드의 예를 들어 보면...
Controller.java
Date now = new Date(0);
ModelAndView mav = new ModelAndView("jspview");
mav.addObject("registerday", now);
return mav;
jspview.jsp
<fmt:formatDate value="${registerday}" pattern="yyyy년 MM월 dd일" />
위와 같은 코드를 작성했는데 화면 결과는
화면 결과
(출력결과 : May 29 00:00:00 1999)
(cf. 기대했던 결과: 1999년 05월 29일)
기대했던 결과와 다른 결과과 나온다면 이 문제를 어떻게 봐야할까?
원인 분석
** 위의 상황을 잘 보면 now 객체 데이터 값은 맞는데 패턴만 변형이 되지 않고 있다는 점이다. 즉 데이터 객체는 컨트롤러 단에서 잘 넘겨주고 있다는 것은 유추할 수 있다. 그럼 문제는 JSTL 포멧터의 formatDate에서 원하는 패턴대로 데이터 객체 값을 변형해 주지 못하고 있다는 점이다. 과연 그 원인은 무엇일까?
JSTL도 뜯어보면 사실 Java Code다. 즉 formatDate 내부에서도 Controller에서 넘겨준 now 객체값을 인자로 받고 Java에서 제공해주는 API를 이용하거나 자체적으로 만든 API를 가지고 날짜 객체를 문자화 시키고 있다는 것이다. 그럼 좀 더 생각해보면 Java에서 Date 타입의 값을 문자열로 변경해주는 Class로는 SimpleDateFormat이 있다. 예상컨데 내부적으로는 SimpleDateFormat을 사용하고 있을 것이다.
서론이 좀 긴 듯 하다. 어찌 되었든 원인을 명확히 알기 위해서 이제 formatDate 내부를 좀더 한번 확인해보자.
* 코드 분석(JSTL 소스코드는 (http://archive.apache.org/dist/jakarta/taglibs/standard/source/ 에서 다운 받을 수 있다.)
src.org.apache.taglibs.standard.tag.common.fmt.FormatDateSupport가 formatDate의 실제 동작 클래스 부분이다. 내부 코드를 보면
public abstract class FormatDateSupport extends TagSupport {
public int doEndTag() throws JspException {
String formatted = null;
if (value == null) {
if (var != null) {
pageContext.removeAttribute(var, scope);
}
return EVAL_PAGE;
}
// Create formatter
Locale locale = SetLocaleSupport.getFormattingLocale(
pageContext,
this,
true,
DateFormat.getAvailableLocales());
if (locale != null) { // 여기서 format에(yyyy년 mm월 dd일) 맞도록 변환을 시작
DateFormat formatter = createFormatter(locale);
// Apply pattern, if present
if (pattern != null) {
try {
((SimpleDateFormat) formatter).applyPattern(pattern);
} catch (ClassCastException cce) {
// 역시 SimpleDateFormat을 사용한다.
formatter = new SimpleDateFormat(pattern, locale);
}
}
// Set time zone
TimeZone tz = null;
if ((timeZone instanceof String) && ((String) timeZone).equals("")) {
timeZone = null;
}
if (timeZone != null) {
if (timeZone instanceof String) {
tz = TimeZone.getTimeZone((String) timeZone);
} else if (timeZone instanceof TimeZone) {
tz = (TimeZone) timeZone;
} else {
throw new JspTagException(
Resources.getMessage("FORMAT_DATE_BAD_TIMEZONE"));
}
} else {
tz = TimeZoneSupport.getTimeZone(pageContext, this);
}
if (tz != null) {
formatter.setTimeZone(tz);
}
formatted = formatter.format(value);
} else {
// 적절한 Locale을 찾는데 실패할 경우 기본 toString() 형태로 반환한다. Format에 맞게 변경하지 못했다면 아래 코드가 수행되었을 것이다.
// no formatting locale available, use Date.toString()
formatted = value.toString();
}
...
}
------------------------
static Locale getFormattingLocale(PageContext pc, Tag fromTag,
boolean format, Locale[] avail) {
LocalizationContext locCtxt = null;
// Get formatting locale from enclosing <fmt:bundle>
// JSP에서 태그로 정한 Locale 정보가 있다면 가져온다.
Tag parent = findAncestorWithClass(fromTag, BundleSupport.class);
if (parent != null) {
/*
* use locale from localization context established by parent
* <fmt:bundle> action, unless that locale is null
*/
locCtxt = ((BundleSupport) parent).getLocalizationContext();
if (locCtxt.getLocale() != null) {
if (format) {
setResponseLocale(pc, locCtxt.getLocale());
}
return locCtxt.getLocale();
}
}
// Use locale from default I18N localization context, unless it is null
if ((locCtxt = BundleSupport.getLocalizationContext(pc)) != null) {
if (locCtxt.getLocale() != null) {
if (format) {
setResponseLocale(pc, locCtxt.getLocale());
}
return locCtxt.getLocale();
}
}
/*
* Establish formatting locale by comparing the preferred locales (in
* order of preference) against the available formatting locales, and
* determining the best matching locale.
*/
Locale match = null;
Locale pref = getLocale(pc, Config.FMT_LOCALE);
// JSP내에서 pageContext에 FMT_LOCALE이 설정되어 있으면 application-based고,
// 그게 아니라면 Browser Based로 간다. JSTL에서 setLocale도 정하지 않았고, Browser에서 // 요청한 HTTP헤더의 Accepted-Language값이 비정상적이라면 ...?
if (pref != null) {
// Preferred locale is application-based
match = findFormattingMatch(pref, avail);
} else {
// Preferred locales are browser-based
match = findFormattingMatch(pc, avail);
}
if (match == null) {
// Use fallback locale.
pref = getLocale(pc, Config.FMT_FALLBACK_LOCALE);
if (pref != null) {
mat ch = findFormattingMatch(pref, avail);
}
}
if (format && (match != null)) {
setResponseLocale(pc, match);
}
return match;
}
코드를 직접 분석해 본 결과 JSP 페이지에서setLocale같이 직접 다국어 처리를 하지 않은 상황이거나, Browser에서 페이지 요청시 HTTP내의 Accept-Language 에서 요청한 Locale이 서버내에 없다면 JSTL의 formatDate는 정상 동작하지 않는 것을 알 수 있다.
테스트
fiddler2 같은 Http 패킷 조작 툴에서 Http Accept-Language 헤더를 조작해서 페이지를 요청해보고 실제 결과를 확인해본 결과. 코드에서 지정한 날짜 포멧에 맞게 변환되지 않는 것을 확인할 수 있다.
해결
해결 방향은 여러가지.
가장 간단하게 처리는 jstl tag중에서 setLocale을 추가하는 방법이다. 두 번째는, JSTL 소스를 고치기 어려우니, 컨트롤러 단에서 SimpleDateFormat Class를 이용해서 포멧에 맞게 변환하고, String 결과를 View에 넘기는 것이다. 이 때, Locale 정보는 어떻게 할 것인가에 대한 정책이 필요하다. 가령, 서버의 Locale 정보를 기준으로 할 것인지, 혹은 적절한 Locale을 Class Loading 시점에 올릴 것인지 등등.. 개인적으로는 서버의 환경에도 영향을 받지 않고, 동작하기 위해서는 기존 JSTL 코드 컨셉을 대부분 그대로 가져오고, Browser에서 요청한 Locale을 찾지 못한 경우, 강제로 서버 Locale에 맞추어서 반환하는 것이 괜찮은 것 같다. 마지막으로 JSTL을 수정하는 방법이 있겠다. 위 코드에서 실패했을 경우를 수정하는 것보다는, JSTL에서 인자로 한가지를 더 받아, 원하는 Locale을 찾지 못한 경우, Prefered한 Locale을 인자로 받아 처리하는 방식으로 패치하는 것은 어떨까 싶다.