Rob Pike 세미나. 전반에는 Concurrency 개념, 목적, 중반 이후에서는 Concurrency에 기반한 Go 언의 특징, Concept, Concurrency Compoistion 예를 설명한다.


Concurrency가 주는 혜택을 단순히 Request에 대한 Response를 빨리 이끌어 낼 수 있는 방법이라고 생각했었는데, Go 언어에서는 Program을 처리하기 위한 일을 나누는 분배 방안으로 접근한다. Concurrency에 대한 관점을 Softwarre 구조 모델의 방법으로 본다.


CSP 개념 기반의 언어들은 Limbo, Erlang들이 이미 있다. 단지 Concurrency를 표현하는 방식의 차이가 있을 뿐.


Rob Pike가 읽어보라는 논문도 찾아봐서 봐야겠다.


정리하면 Parallelism is about performance, Concurrency is about program design. 




https://blog.heroku.com/archives/2013/2/24/concurrency_is_not_parallelism


http://blog.golang.org/go-programming-session-video-from


http://www.youtube.com/watch?v=f6kdp27TYZs

Posted by initproc
,

 

G1 Concept

  • G1에서 연속적인 메모리 공간을 region이란 이름의 고정된 크기로 나눈다.
  • eden, survivor, old 영역은 고정된 크기로 나눈 이 region을 사용한다. region이 곧 eden이거나 survivor, old일 수 있다.
  • 기본 region 목표 수치는 2048개(2K)개의 공간으로 나눌 수 있도록 한다. 만약 8G가 Java Heap 최대 크기라면 한 region의 크기는 4MB가 되겠다. (8192MB/2048 = 4MB)
  • region 공간은 1MB ~ 32MB 사이로 -XX:G1RegionSize 옵션을 이용해 조정 가능하지만, 사용자가 직접 튜닝하는 것은 추천하지 않는다.
  • eden, survivor, old 이외에도 2가지 다른 타입의 region이 존재한다. Humongous region과 Available / Unused region이다.
    • Humongous region은 객체의 크기가 큰 경우 사용하는 영역다. 만일 객체가 region 영역의 크기보다 1/2 (논문에선 1/3)보다 큰 경우 Humongous region을 사용하도록 한다.
    • Avaialable / Unused region은 아직 사용하지 않은 영역을 의미한다.
    • Humongous region에 대한 GC 동작은 최적화 되어 있지 않다. 따라서 큰 객체 Size가 G1 GC를 사용하는데 문제라면 왜 큰 객체가 문제가 되는지 프로그램을 분석하는게 순서가 되겠다. 또한 큰 객체 사용의 또 다른 문제는 Java에서 큰 객체를 할당하는 것은 일반적인 크기의 객체를 할당하는 것보다 Overhead가 크므로 프로그램 설계에 문제가 없는지 봐야 한다.

 

 

 

G1 GC 수행 단계

  • G1 GC 수행 과정은 Evacuation Pauses(=Minor GC), Concurrent Cycle, Mixed GC가 있다.

  • Evacuation Pauses(=Minor GC)

    • Evacuation Pauses(=Minor GC)일 때 eden과 survivor에 있는 유효 객체(Live objects)를 적절한 region을 찾아 대피시키도록(Copying) 한다.

    • Evacuation Pauses(=Minor GC)일 때 GC 대상의 region 선택은 사용자가 옵션으로 지정한 pause time과 G1 GC 내부에서 사용하는 휴리스틱 알고리즘에 의해 선택된다. 그 이유는 G1 GC의 중요한 목표 중 하나는 실시간성 향상에 있기 때문이다.(Hard real-time이 아닌 휴리스틱 알고리즘에 의한 Soft real-time Goal이다.)

    • Evacuation Pauses는 빨리 완수하기 위해 JVM에서 Multi-threaded로 동작한다.

    • Young Generation Stop-the-world 구간이다. (CMS와 다름)

    • Survivor region은 liveness objects를 판단하기 위해, Eden region은 pause time을 예측하는데 사용된다.

    • minor GC 때마다 Eden과 Survivor 영역 크기는 변경될 수 있다.(CMS와 다름. Parallel OLD GC에서는 Adpatable하게 사이즈가 변경가능한 옵션이 있던것으로 기억하는데, CMS도 있는지 잘 모르겠음. 보통 쓰지 않는 기능...)

    • Eden에 있는 live object는 Survivor 영역으로, Survivor from은 Survivor to로, Survivor to는 OLD 영역으로 이동한다는 내용이 있는 반면, 유효 객체(live objects)가 Available/Unused region으로 이동되고, survivor region으로 바뀐다는 내용도 있다. (전자가 맞는 듯 하다. 기존 Generation 기반 GC 개념 자체를 바꿀 필요가 없을 테니까..)

    • 논문에서는 첫 Evacuation Pauses는 일정 용량이상 차면 수행 되고, 그 이후에는 사용자가 지정한 pause duration과 per-byte copying Cost에 의해 결정된다고 한다. 얼마나 많은 Live object를 to-survivor regions에 복사할지 추정하는데 단위 바이트당 소요될 Cost와 pause duration P의 곱이 제한 시간 내 복사할 수 있는 용량으로 산정해서 동작한다.

    • 각 region에는 Live Data Counting 정보를 가지고 있다. 이 정보를 바탕으로 region 영역이 먼저 Garbage 되어야 하는지 결정된다.(그래서 이름이 Garbage First, G1 이다).

    • External Root Scanning: gc thread들이 registers, thread stacks에서 root 노드를 검색하는 단계

    • Update Remembered Sets: RSet 정보를 갱신하는 단계

      • Processed Buffers: worker thread가 얼마나 많은 update 버퍼 정보를 갱신했는지 보여준다.

    • Scan Rsets: 업데이트 된 Rset 정보를 기반으로 region을 향해 가진 reference를 가진 객체들을 탐색한다.

    • Object Copy: Scan Rsets에서 탐색한 객체들을 복사하기 시작한다. Eden에 있는 객체들을 from-space로, from-space에 있는 객체들을 to-space로, to-space에 있던 객체들을 Old 영역으로 복사한다.

    • Termination: GC thread들은 객체 탐색 및 객체 복사의 일이 끝나면 Termination 단계에 들어간다. Task 처리의 로드밸런싱 효율을 높이기 위한 방법으로 Work-stealing 기법을 사용하고 있다.

    • Parallel Worker Other time: 위에서 언급한 일 이외의 다른 부수적인 일을 처리한 시간
       

  • Concurrent Cycle

    • -XX:InitiatingHeapOccupancyPercent (IHOP) 에서 정한 수치가 넘어가면 동작한다.

    • OLD 영역을 GC하기위한 전초 작업이다.

      • initial mark: multi-threaded로 동작하며, minor GC때 수행 한다. (CMS와 다름. CMS에서 initial mark는 stop-the-world 구간이다.)

      • Root region Scanning: multi-threaded로 동작하며, Application과 Concurrent하게 동작한다. 이때 Old 영역으로 reference를 가진 survivor-regions을 검색한다. 이 단계가 끝나야 Evacuation Pauses(Minor GC)가 동시에 실행될 여지가 발생한다.(Survivor regions 영역은 Evacuation Pauses(Minor GC)에서도 탐색하기 때문)

      • Concurrent marking: multi-threaded로 동작하고, Application과 Concurrent하게 동작한다. 모든 rechable / live 객체들을 마킹한다. 이 때부터 Evacuation Pauses와 동시에 실행 가능하다.

        • GC 로그 예) [GC concurrent-mark-start]
                           [GC concurrent-mark-end, ...

      • Remark: Stop-the-world 구간이다. multi-threaded로 동작한다. marking 작업을 마무리.

        • GC 로그 예) [GC remark ....

      • Cleanup: Partly Stop-the-world 구간이다. multi-threaded로 동작한다. region 영역의 liveness 정보를 갱신하고, 적절한 free region을 식별한다. 논문에 의하면 Cleanup 할 때, 각 region의 GC 효율을 기준으로 정렬하는데(G1 first, cost efficiency), GC 효율 측정 factor에 remember set의 size등을 고려하는데 이러한 작업을 하기 위한 기본 데이터 값들을 정리한다.
        (Updates region liveness and identifies completely free regions along with remembered set scrubbing)

        • GC 로그 예) [GC cleanup, ...

      • Concurrent-Cleanup: empty region 자료구조를 Reset하고, available/Unused regions list에 추가한다.

        • GC 로그 예) [GC concurrent-cleanup-start]
                            [GC concurrent-cleanup-end, ...

  • Mixed GC

    • Mixed GC일 때 Young영역과 Old 영역을 Garbage collection 한다.(단어에서 의미하듯 Mixed이다.)

    • OLD region 영역의 GC 선택 기준은 liveness를 기준으로 판단한다. Garbage Collection 효율을 높이기 위해 liveness가 높은 것은 재사용될 가능성이 높다고 판단하기에 liveness가 적은 것을 GC하도록 한다.(따라서 Garbage First, G1이라는 이름이 붙었다.)

    • Mixed GC는 기본적으로 8회 수행되도록 되어 있다. 1회의 Mixed GC에 모든 OLD 영역이(Old regions) Garbage collection되지 않는다. 한번에 Garbage를 정리하기에 Cost가 매우 크기 때문이다.
    • minor GC 처럼 Eden과 Survivor 영역 크기는 변경될 수 있다.

    • Mixed GC는 Evacuation Pauses때 수행하는 단계와 동일하다. Evacuation Pauses인데 OLD region을 추가적으로 GC 하는 것이다. Mixed GC = Evacuation Pauses(Young GC + OLD GC)

    • Mixed GC 때 선택되는 CSet은(Old regions) -XX:G1MixedGCCountTarget (defaults to 8) -XX:G1OldCSetRegionThresholdPercent 에 영향받는다. OLD 영역에 관한 CSet Tuning과 관련있다.

    • Mixed GC 선택 대상 결정은 -XX:G1MixedGCLiveThresholdPercent,-XX:G1HeapWastePercent에 영향 받는다.

G1에서 사용하는 중요 자료구조(G1's data structures)

  • Remember Set (RSet)

    • reference를 가진 Object들이 어느 Region에 있는지 알기 위해 사용하는 자료구조다. (이 Reference를 왜 알아야 하냐면 Live Object을 알기 위함이다. Object가 가진 Reference의 Origin을 왜 알아야 하는지는 Baker's Incremental GC 알고리즘을 이해하자. 참고자료는 Baker's Incremental Copying Collector 알고리즘, 간략히 이야기하면 Reference를 가진 Objects를 찾아 live object를 복사하는 알고리즘이다. Baker's에서는 live object 검색 시간이 O(n)*rootset 의 시간 복잡도가 걸리지만, Rset을 사용해서 개선 시킬 수 있다.)

    • GC 로그에서 이 Reference 정보를 갱신(Update)하고 검색하는데 소요되는 시간을 보여준다.

  • Collection Set (CSet)

    • GC가 수행될 Region의 모음.

    • GC 로그에서 CSet을 선택하고, Processing 후 CSet을 해제하는데 걸린 시간을 볼 수 있다. Evacuation Pauses와 Mixed GC할 때 사용하는 자료구조이다.

G1 옵션

  • -XX:+UseG1GC 필수, -Xms, -Xmx 는 Optional

  • -XX:MaxGCPauseMillis: default 200ms(확인이 필요)

  • -XX:InitiatingHeapOccupancyPercent (aka IHOP): 기본값은 자바 Heap의 45%, Concurrent Cylce의 시작 시기를 조정할 수 있는 옵션

  • -Xmn, -XX:NewSize/-XX:MaxNewSize/-XX:NewRatio, -XX:SurvivorRatio Young 영역 공간에 대한 옵션 Tuning은 G1의 휴리스틱 동작을 비활성화한다(Disable adaptive G1's heuristic algorithm). 따라서 설정하지 않는 것이 좋다.

  • -XX:G1MixedGCLiveThresholdPercent (defaults to 65): – OLD region내 live object가 이 옵션 이상 값으로 존재한다면 GC 대상에서 제외한다.
  • -XX:G1HeapWastePercent (defaults to 10): – 얼마나 많은 region이 waste되어도 용납할지 결정한다. Heap을 낭비해도 좋다고 판단하는 값. mixed cycle의 종료 시점을 결정한다.

  •  -XX:GCPauseIntervalMillis =200 (for a pause interval target of 200ms) : G1 GC 주기 조정


G1 GC Tuning

  • GC가 Application의 병목현상의 원인인지부터 확인한다. GC가 Application 동작에 문제를 일으킨다면, GC Tuning을 한다.

  • GC가 Application의 병목현상이 원인인지는 GC로그를 보고 판단한다.

  • GC Pauses 최대, 평균 시간, GC의 동작 횟수(얼마나 자주 동작하는가)를 확인한다.  

  • GC 로그에서 각 GC 동작 스텝의 user, sys, real time 로그를 확인한다.

  • G1 GC의 경우 -XX:+PrintAdaptiveSizePolicy 옵션을 사용하면 GC가 수행되는 region 정보를 볼 수 있다.

  • Mixed GC 옵션 Tuning은 다음 4가지 옵션을 고려한다.

    • -XX:G1MixedGCCountTarget (defaults to 8)

    • -XX:G1OldCSetRegionThresholdPercent

    • -XX:G1MixedGCLiveThresholdPercent

    • -XX:G1HeapWastePercent

  • CSet Tuning: CSet의 병목현상은 CSet의 범위를 조정해서 완화시킬 수 있다. Mixed GC 옵션을 활용하여 Tuning한다.

  • RSet Tuning: 의 Tuning은 다음 옵션을 고려한다.

    • -XX:G1RsetUpdatingPauseTimePercent(사용자가 지정한 puase time의 10%): RSet update할 시간 조정

    • XX:+ParallelRefProcEnabled: User time이 오래 걸린다면, Scaling을 의심한다. remark 단계에서 reference processing 시간을 보고 scaling이 잘 안되는 경우 사용.

 

정리

  • 기존 GC가 Space based 동작이라면 G1 GC는 Time based 동작으로 적시에 GC를 수행할 수 있게 되었다.

  • Young 영역에 대한 GC 시작은 사용자가 지정한 Pause time에 의해 휴리스틱하게 결정된다. Young 영역에서 GC할 CSet의 선택도 역시 Pause time에 영향 받는다. GC 수행 종료 시점도 마찬가지로 Pause time에 종속된다.

  • Concurrent Cycle은 OLD 영역을 GC하기 전에 필요한 정보를(region별 gc cost, 객체 liveness count) 얻기 위해 수행한다. Mixed GC의 전 작업이다.

  • OLD 영역에 대한 GC 수행 시작은 IHOP이 결정한다.

  • OLD 영역 GC 수행시 선택될 CSet은 다음 두 옵션에 의해 영향 받는다.

    • -XX:G1MixedGCCountTarget (defaults to 8): Mixed GC 최소 수행 작업 횟수

    • -XX:G1OldCSetRegionThresholdPercent (defaults to 10): Mixed GC 수행시 선택할 최대 OLD region 개수, 자바 Heap의 Percentage로 값을 설정한다.

  • CMS의 경우 Eden, Survivor 영역의 크기는 고정되어 있지만, G1은 GC이전과 GC이후에 각 영역의 Size는 변동(Adaptable) 될 수 있다.

  • 기존 Young, OLD 컨셉을 버리고 Region으로 나누고 Region별 live object정도를 판단함으로써 더 명확한 Garbage 객체 결정을 할 수 있게 되었다. 또한 프로그램 구조적으로 Parallel, Concurrent하게 동작하기에 용이한 구조이기에 Multi Processor에 사용하기 적합한 구조이다. 이와 더불어 GC 시점과 대상에 대한 휴리스틱한 알고리즘을 넣음으로써 High allocation Rate, Large Heap을 요구하는 Application의 요구사항을 충족시켜줄 수 있게 되었다.

  • Collection 단계를 Evacuation, Concurrent Cycle, Mixed GC 단계로 나눈 이유: CMS 혹은 Parallel GC같은 기존의 GC 수행 형태는 Minor GC와 OLD GC 이 두 단계로 나눌 수 있다. 최대한 Concurrent 그리고 Parallel하게 동작하기 위해 기존의 OLD GC의 단계를 Concurrent Cycle(GC 객체 검색 및 마킹:Scanning and Mark)과 Mixed GC(GC 수행 및 정리:Evacuation and Cleanup) 이 두 단계로 분리한 것이다. G1 GC에서 Concurrent Cycle은 Minor GC와 동시에 수행이 가능하다.(단 Root region scanning 이후에 가능하다.) 그리고 Concurrent Cycle이 끝나면 이때 Marking한 정보를 기반으로 Mixed GC가 수행된다.

  • Snapshot At The Begging(SATB): 논문에서 SATB란 단어가 나오는데, 이 단어의 의미는 특정 순간의 내용을 기록한다는 의미이다. G1 GC에서는 IHOP이 넘는 그 순간에 Concurrent Cycle을 시작해서 Snapshot을 찍고(OLD 영역의 GC할 대상을 찾아 기록함) Mixed GC에서 SATB에서 정리한 정보를 바탕으로 OLD region garbage collection이 연속(기본 8회) 수행된다. 당연하지만 Mixed GC때 Young 영역의 region도 같이 Garbage collection된다.


Posted by initproc
,

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을 인자로 받아 처리하는 방식으로 패치하는 것은 어떨까 싶다.




Posted by initproc
,