Octobersky.js 세번째 모임 후기

Octobersky.js 는 작년 10월에 너구리 님이 시작하신 node.js 스터디모임입니다. 시작할 때는 온라인을 중심으로 기간한정으로 진행할 계획이셨지만 진행이 꽤 잘되서인지 계속 진행되고 있고 지난 12일에 명동 커피쉬라는 카페에서 세번째 오프라인 모임이 있었습니다. Octobersky.js에서 생산해낸 node.js 관련문서들도 상당히 많은데 공식페이지 에 잘 정리가 되어 있고 현재는 node.js에 한국 공식커뮤니티로 등록 되어 있습니다. 저야 당연히 node.js를 좋아하기 때문에 처음부터 참가하고 싶었지만 시작할 당시는 책에 탈고작업으로 너무 정신없어서 참가못하고 두번째 모임에는 약속이 겹쳐서 못갔다가 이번에 겨우 참가를 했습니다.

홍보가 많이 된 것 같지는 않아서 얼마나 모일까 걱정도 약간 했습니다만 주말인데도 20명 가까이 모인것 같습니다. 약간 자유스럽게 진행된 행사였기 때문에 미리 발표랑 스케쥴이 정해져 있지는 않고 (너구리님이 사전에 발표부탁이나 발표지원을 받기는 했지만) 즉석에서 발표내용을 포스트잇으로 받아서 진행하였습니다.


node.js happy hacking
- 박난하
첫 발표는 FRENDS 에서 같이 활동하고 있는 박난하님의 발표였습니다. 발표 내용은 저번 FRENDS Hackathon 2012 에서 만드신 node.js black editionnodeman에 대한 내용이었습니다.(해카톤은 후기를 안남겼었다는걸 이제야 깨달았네요 ㅠㅠ) 주 내용은 Black Edition에 대한 부분이었는데 node.js 개발하면서 서드파티 모듈이 무척 중요하지만 너무 많아서 초신자들은 선택하기 어려운 부분도 있고 하던 프로젝트에서 매번 설정하는 것이 아니라 아예 패키징을 해서 배포하는 것을 더 선호하기도 했기 때문에 유용한 서드파티 모듈들은 네이티브 모듈처럼 node.js에 빌트인해서 패키징한 프로젝트입니다. 내장된 모듈들은 Black edition 프로젝트 페이지에서 확인할 수 있습니다.

저번 해카톤에서 약간 공유받은 내용이긴 하지만 그때는 발표모임이 아니라 코딩모임이었기 때문에 잘 이해못했었는데 이번에는 유쾌한 내용으로 발표를 잘 준비해 주신덕분에 이해하기가 좋았습니다. 사실 node.js의 내부에 대한 내용이었기 때문에 약간 어렵고 지루할 수도 있었는데 유쾌한 내용으로 잘 진행해 주셨습니다.

Node.js는 빌드스크립트로 Python을 사용하고 API는 자바스크립트를 사용하며 core(v8)은 CPP로 되어 있으며 libuv, libuv, libeio등의 오픈소스들은 C로 구성되어 있습니다. V8에서 JS2C라는 유틸을 제공하고 있는데 이는 node.js의 기본 API인 http, fs같은 파일을 V8로 컴파일하는 유틸리티입니다. 즉 JS 파일을 컴파일해서 V8에 넣는 역할을 하면 WAF Build Script(wsscript)를 사용합니다. 일반적은 모듈들은 Javascript 파일을 로드하고 V8로 컴파일하지만 네이티브 모듈들은 파일로드 없이 C에서 바로 V8로 컴파일하는 이점이 있습니다.


Node를 실행하고 프롬프트가 뜰 때 까지의 과정은 가장 먼저 src/node_main.cc가 실행되고 src/node.cc가 실행되면서 process 객체(process.binding등)가 생성됩니다. 그리고 lib/**.js 네이티브 모듈들을 V8로 컴파일하고(src/node_javascript.cc) event loop가 시작됩니다. process.binding()은 Node API의 핵심역할인데 CPP 모듈을 자바스크립트로 불어들이는 중개역할을 합니다. 즉 src/node_**.cc를 src/**.js로 불러들입니다. javasciprt를 네이티브 모듈로 만드려면 lib/**.js 파일을 생성하고process.binding을 사용해서 cpp lib을 import합니다. 변경사항이 생기면 WAF 빌드툴로 다시 빌드하게 됩니다.

Node.js의 내부를 본 적이 없고 C는 전혀 모르는 저로서는 약간 어려운 내용이었습니다. 그럼에도 몇달동안 Node.js의 내부를 다 까서 테스트해보면서 알게 된 내용이라 다른 데서는 들어볼 수 없는 엄청난 내공의 node.js 내부구조에 대해서 들을 수 있었습니다.(능력자!)





node.js 불신 자바개발자의 노드 개발(& 삽질)기 - fupfin
fupfin님은 스프링 프레임워크의 고수입니다만 최근에 node.js를 쓰고 계신 관계로 이번에 참여를 하셨습니다. 회사에서 윗분이 node.js를 쓰자고 하셔서 쓰고 있다고 하셨는데 node.js를 별로 좋아하진 않는다고 하셨습니다. 왜 싫었는가 하면 자바스크립트는 엔터프라이즈급 코드에는 적합하지 않다고 생각하셨기 때문입니다. 미투데이에 올린 글들을 증거로 보여주시며 원래 자바스크립트를 싫어했던 건 아니고 예전에는 자바스크립트를 홍보하고 다니셨지만 node.js와 함께 너무 큰 인기를 생기면서 오히려 반대입장을 약간 가지게 되었다고 합니다.

반대 입장이 된 이유는 아직 1.0도 되지 않은 짧은 역사를 가지고 있는데다가 자바스크립트로 비즈니스 로직을 작성하는 것이 싫으셨고 아키텍쳐가 제대로 정립되어 있지 않은 점 때문입니다.(서버사이드 언어에 오랜 역사에 비하면 그렇기는 합니다.) 그리고 자바스크립트는 표현력이 높은 고수준 언어가 아니기 때문에 node.js CPS도 자바스크립트라서 어쩔 수 없이 쓰는 것 뿐이 좋은 스타일이라고는 할 수 없다고 하셨습니다.(CPS에 대해 궁금하시면 pismote lnyarl님이 쓰신 예제로 설명하는 자바스크립트에서의 Continuation-passing style 를 참고하시면 됩니다.) 그리고 Exception을 쓸 수 없는 점도 맘에 안드는 부분이라고 하셨습니다.

그래서 fupfin님이 생각하는 node.js는 비동기에 싱글스레드/프로세스 방식은 사실 새로운 방식은 아니고 성능을 위해서 추상화/설계를 포기한 최적화 기법을 적용했고 I/O에 비해서 CPU 연산이 적을수록 유리하다고 하셨습니다. 그리고 오래된 사람들만 아는 내용이지만 자바스크립트는 처음부터 서버사이드 기술이었습니다. 하지만 이런 저런 부분에도 불구하고 커뮤니티는 최강이라고 생각한다고 하셨습니다.

그래서 맘에 안드는 부분을 해결하기 위해서 step이나 커피스크립트 RabbitMQ로 자바와 연동하는 법을 알아보셨고 node-fibers나 js-coroutine도 알아보셨지만 좀 불안하다고 하셨습니다. 그러다가 자바기반으로 node.js를 만들면 어떨까 하는 생각이 들어서 Rhino를 알아봤지만 엄청나게 느린 속도 때문에 포기(?)했다고 하셨습니다. Rhino는 곧 VM을 개선하겠다고 발표는 했다고 합니다.

그래서 선택한게 Vert.x 입니다.(Vert.x는 node.js에서 영감을 받은 Tim fox가 node.js 같은 메카니즘을 JVM기반으로 구현한 기술입니다. JVM위에서 동작하며 자바뿐만아니라 ruby, javascript등을 지원하면 다른 언어들도 지원예정에 있고 비동기 I/O는 netty를 사용하고 있습니다.) fupfin님이 하시려고 한 것은 node.js의 코드를 vert.x상에서 동작하도록 하신 것으로 프로젝트 명은 nodaja입니다. 그래서 vert.x와 ringojs를 분해 후 재 조립해서 ringojs 엔진은 기동하고 require, console, timer는 구현했지만 event loop를 구현하다가 실패하셨답니다. 그래서 2차 시도로 ver.x를 node.js에 호환되도록 수정하기로 하고 vert.x의 api를 node.js 모듈로 랩핑하려는 시도를 하시는 중에 모임 시간이 다 되서 오셨다고 합니다.(참고로 vert.x의 javascript는 Rhino 기반으로 되어 있는데 commonjs를 구현한 것은 아닙니다. tim fox가 commonjs가 뭔지 몰랐다고 합니다. 그래서 fupfin님이 랩핑작업을 시도하신 거였는데 이날 저녁 vert.x가 commonjs를 지원하기로 발표했습니다. ㅡㅡ;;)

참 흥미로운 내용이었습니다. node.js모임이었지만 반대 의견도 들을 수 있어서 좋았고 몇일 전 Tim fox의 벤치마크공개 로 인해서 온라인에서 엄청 논쟁이 일어났던 Vert.x에 대해 여러가지 들을 수 있어서 좋았습니다. Toby님 이 종종 Vert.x로 꼬셔서 이번에 1.0 나왔길래 조만간 봐야겠다 싶었는데 이번에 fupfin님이 불을 확 당겨주시네요 ㅎㅎㅎ Vert.x가 아무래도 자바쪽 기술이고 최신 기술이라서 대부분의 분들은 Vert.x를 처음 본 터라 발표후 질문 세례가 쏟아졌습니다. Vert.x의 내부에 대한 설명을 하는 자리는 아니었기 때문에 대부분의 질문은 Rhino에 대한 내용이었던것 같습니다.(저도 Rhino는 한번도 안해봐서...)





Flow Control in Node.js - 송형주
Node.js에서 Flow Control에 대한 발표였습니다. node.js에서는 콜백을 자주 사용하기 때문에 신경쓰지 않으면 코드가 복잡해지기 쉽상이고 그렇기 때문에 자연스럽게 Flow Control에 대한 관심으로 이어지게 되는데 Flow Control을 어떻게 하는가와 어떤 모듈들이 있는가에 대한 내용이었습니다. 이 발표의 발표자료는 송형주님이 블로그에 공개 해 주셨습니다.

송형주님은 원래 C를 주로 쓰는 개발자라 웹은 별로 안좋아하셨다고 합니다. fupfin님과 마찬가지로 윗분이 node.js를 선택해서 쓰기 시작했는데 C의 절차적 프로그래밍에 익숙해서 처음에는 콜백, 특히 중첩 콜백까지 나오면 너무 어려웠다고 하셨습니다. 같은 코드인데도 동기코드를 비동기코드로 바꾸니까 익숙치 않아서 너무 어려웠고 Flow Control이 필요하다고 느끼셨다고 합니다.

Flow Control은 3가지 패턴이 있습니다. Serial Execution, Parallel Execution, Limited Parallel Execution입니다. Serial Execution은 순차적으로 실행하는 방식으로 콜백에서 다음 함수를 실행하게 됩니다. Parallel Execution은 순서는 중요하지 않지만 여러 가지 작업이 다 끝나면 어떤 작업을 해야하는 경우에 사용하느 패턴입니다. 각 패턴의 자세한 내용은 발표자료에 잘 나와있으니 발표자료를 참고하시면 될 것 같습니다.(발표자료를 이해하기 좋게 잘 만들어주셔서 ㅎ) 이 Flow Control을 해주는 모듈로 aync, step, slide등이 있습니다. async는 flow control이외의 많은 기능이 있어서 약간 어렵고 step이 사용하기는 간단하다고 하셨습니다.

저는 아직 Flow Control은 쓰고 있지 않습니다. 업무로 하다보니 그 정도로 사이즈가 큰 코드를 작성하지 않고 있기도 하고 한편으로는 충분히 CPS나 Event Emitter를 사용해 본 뒤에 Flow Control을 적용해 보려고 하는 것도 있습니다. 먼저 적용하면 사실 장점도 잘 모르기 때문에 기본으로 써보고 불편함을 느꼈을 때 적용하려고 하는데 조만간은 사용해야 겠다고 생각하고 있었는데 이번에 정리된 내용을 공유받아서 좋았습니다.





자바스크립트 웹어플리케이션 개발경험기 - 이병주
이병주님은 Backbone.js와 require.js를 이용한 웹 어플리케이션 프로젝트를 진행한 경험을 공유해 주셨습니다. 이병주님은 3년차 개발자인데 이번에 혼자서 웹 어플리케이션을 작성하는 프로젝트를 맡게 되었답니다. 기존 자바스크립트가 많은 프로젝트의 문제점은 복잡하고 코드의 양이 많은 데다가 테스트나 빌드도 부족했다고 합니다. 그래서 프로젝트를 구조화 하기 위해서 많은 MVC 프레임워크들을 찾아보았다가 Backbone.js를 선택했다고 합니다. Backbone.js를 선택한 이유는 일단 가볍고 필요한 요소들만 있는데다가 자유도가 높습니다. 그리고 이해하기가 쉬웠고 이번에 개발하려는 프로젝트와 잘 어울린다고 생각했기 때문이랍니다.(Backbone.js를 써보진 않았지만 저도 MVC 중에는 Backbone.js가 제일 낫다고 생각합니다.)

그리고 프로젝트를 하니까 자바스크립트가 몇천 라인까지 나왔는데 관리가 쉽지 않아서 requireJS를 적용해서 각 모듈의 의존성을 해결했다고 합니다. 각 모듈별로 파일을 분리하고 각 모듈의 의존성은 requireJS로 명시해서 자동으로 의존 모듈이 로드되도록 해서 개발하니까 관리가 쉬웠고 테스트는 Jasmine과 Phantom.js로 했고 배포할때는 requireJS에서 제공하는 r.js로 다시 하나의 파일로 패키징했습니다. AMD의 단점중 하나인 HTTP 요청이 많아지는 문제를 r.js로 해결하고 개발시에만 AMD를 사용해서 모듈화를 쉽게했습니다.

개발하면서 느낀 문제는 Backbone.Model과 Collection이 단순해서 1뎁스 속성의 변경이벤트만 지원했기 때문에 BindingModel을 직접 개발해서 적용했으면 Nested View를 지원하지 않기 때문에 계층은 하드코딩으로 해결했습니다. 그리고 requireJS를 사용하면서 전역변수가 필요해졌고 requireJS를 사용하지 않은 모듈들은 use.js를 사용해서 해결했답니다.

저는 프론트앤드를 엄청 좋아하지만 주업무가 아니고 최근에는 node.js를 주로 하고 있는터라 예전에 비하면 프론트앤드에서 약간 멀어진 느낌이 있습니다. node.js에 대한 내용은 아니었지만 실제로 프로젝트의 생생한 경험을 들을 수 있어서 좋은 시간이었습니다. 발표하시면서 잘 모른다고 겸손의 말씀을 하셨는데 프론트앤드에서 저정도로 구조화하고 고민해서 하는 프로젝트는 많지 않기 때문에 저 내용만 봐도 내공이 엄청나신듯 합니다. 저도 backbone.js, amd, requireJS는 이름은 다 알고 있어서 실제로 사용해 본건 얼마 안되었기 때문에 더 관심이 가는 내용이었고 requireJS나 AMD는 항상 프로덕션 레벨로만 생각해봤기 때문에 이병주님이 하신 것처럼 개발단계의 모듈관리의 접근으로는 생각해 본적이 없어서 상당히 신선했습니다.




너구리님의 스타일상 자유스럽게 진행된 모임이었지만 각 발표의 내용들은 상당한 내용이었습니다. 발표모임 후에 모여서 node.js에 대한 얘기나 Octobersky.js에 대한 얘기도 많이 나누었는데 즐거운 시간이었습니다. 사실 정확히 어떤 발표내용이 있는지도 몰랐지만 계속 참가하려다 못간 모임이었고 오프라인에서 뵙고 싶었던 분들도 있어서 참여했는데 참 좋은 시간이었네요. 11시에 모여서 6시까지 있었으니 참 오래도 모여있었군요 ㅎ
2012/05/18 03:16 2012/05/18 03:16
크리에이티브 커먼즈 라이센스
Creative Commons License

Comments List

  1. 약속이 겹쳐서 못 갔는데 너무 아쉽네요. 생생한 후기 감사합니다. 그리고pismote가 아니고 lnyarl입니다.

    1. 앗... 자꾸 pismote님 블로그라고 생각해서 자꾸 그렇게 언급을 하는군요. 고쳐놓겠습니다. ㅠㅠ
      이번에도

Leave a Reply

Facebook Comments

[Spring 레퍼런스] 4장 IoC 컨테이너 #12

이 문서는 개인적인 목적이나 배포하기 위해서 복사할 수 있다. 출력물이든 디지털 문서든 각 복사본에 어떤 비용도 청구할 수 없고 모든 복사본에는 이 카피라이트 문구가 있어야 한다.




4.13 LoadTimeWeaver 등록하기
스프링 2.5에서 도입된 context 네임스페이스는 load-time-weaver 요소를 제공한다.
<beans>

    <context:load-time-weaver/>

</beans>




XML기반 스프링 설정파일에 이 요소를 추가하면 ApplicationContext에 대한 스프링 LoadTimeWeaver가 활성화된다. 해당 ApplicationContext내의 어떤 빈도 LoadTimeWeaverAware를 구현할 수 있기 때문에 로드타임 위버(load-time weaver) 인스턴스에 대한 참조를 받는다. 이는 JPA 클래스 변환때문에 로드타임 위빙이 필요한 스프링의 JPA 지원을 함께 사용할 때 특히 유용하다. 더 자세한 내용은 LocalContainerEntityManagerFactoryBean Javadoc에 나와있다. AspectJ 로드타임 위빙에 대해서 더 알고 싶다면 Section 8.8.4, “Load-time weaving with AspectJ in the Spring Framework”를 봐라.




4.14 ApplicationContext의 추가적인 기능들
이 챕터의 도입부에서 얘기했듯이 org.springframework.beans.factory 패키지는 프로그래밍적인 방법을 포함해서 빈을 관리하고 조작하는 기본적인 기능을 제공한다. org.springframework.context 패키지는 추가적으로 더 어플리케이션 프레임워크 지향적인 스타일로 추가적인 기능을 제공하려고 다른 인터페이스를 확장하는 BeanFactory 인터페이스를 확장한 ApplicationContext 를 추가한다. 완전히 선언적인 방법으로 ApplicationContext를 사용하는 많은 사람들은 프로그래밍으로는 생성조차 하지 않고 대신에 J2EE 웹 어플리케이션의 일반적인 시작과정의 일부로 ApplicationContext를 자동으로 인스턴스화 하려고 ContextLoader같은 지원 클래스에 의존한다.

좀 더 프레임워크 지향적인 스타일로 BeanFactory의 기능을 강화하기 위해 context 패키지는 다음 기능도 제공한다.

  • MessageSource 인터페이스로 i18n 스타일의 메세지 접근
  • ResourceLoader 인터페이스로 URL이나 파일같은 리소스 접근
  • ApplicationEventPublisher 인터페이스의 사용으로 ApplicationListener 인터페이스를 구현한 빈에 이벤트 발행(Event publication)
  • HierarchicalBeanFactory 인터페이스로 어플리케이션의 웹 계층같은 특정 계층에 각각 집중하는 여러 가지 (계층적인) 컨텍스트의 로딩

4.14.1 MessageSource를 사용한 국제화
ApplicationContext 인터페이스는 MessageSource라는 인터페이스를 확장하므로 국제화 (i18n) 기능을 제공한다. 스프링은 메시지를 계층적으로 처리할 수 있는 HierarchicalMessageSource 인터페이스도 제공한다. 이 인터페이스들은 스프링이 메세지를 처리한 결과에 기반한 기초를 제공한다. 이 인터페이스들에서 정의된 메서드들은 다음을 포함한다.

  • String getMessage(String code, Object[] args, String default, Locale loc): MessageSource에서 메시지를 획득하기 위해 사용하는 기본 메서드. 지정한 로케일(locale)에 대한 메시지를 찾지 못하면 기본 메시지를 사용한다. 전달한 아규먼트는 표준 라이브러리에서 제공하는 MessageFormat 기능을 사용해서 대체값이 된다.
  • String getMessage(String code, Object[] args, Locale loc): 본질적으로는 앞의 메서드와 같지만 한가지가 다르다: 기본 메시지를 지정할 수 없다. 메시지를 찾을 수 없다면 NoSuchMessageException를 던진다.
  • String getMessage(MessageSourceResolvable resolvable, Locale locale): 앞의 메서드에서 사용했던 모든 프로퍼티들은 이 메서드에서 사용할 수 있는 MessageSourceResolvable라는 클래스로도 감싸진다.
ApplicationContext가 로드되면 자동으로 컨텍스트에서 정의된 MessageSource 빈을 찾는다. 해당 빈을 찾으면 메시지 소스에 위임한 앞의 메서드들을 모두 호출한다. 메세지 소스가 없다면 ApplicationContext는 부모가 같은 이름의 빈을 가지고 있는지 찾아본다. 부모가 같은 이름의 빈을 가지고 있으면 그 빈을 MessageSource로 사용한다. ApplicationContext가 어떤 메시지 소스도 찾을 수 없다면 위에서 정의한 메서스에 대한 호출을 받을 수 있도록 비어있는 DelegatingMessageSource를 인스턴스화 한다.

스프링은 2개의 MessageSource 구현체를 제공하는데 ResourceBundleMessageSource와 StaticMessageSource이다. 둘다 중첩된 메세지를 처리하기 위해 HierarchicalMessageSource를 구현한다. StaticMessageSource는 드물게 사용하지만 프로그래밍적인 방법으로 소스에 메시지를 추가하는 방법을 제공한다. ResourceBundleMessageSource는 다음 예제에 나와있다.
<beans>
<bean id="messageSource"
     class="org.springframework.context.support.ResourceBundleMessageSource">
  <property name="basenames">
   <list>
     <value>format</value>
     <value>exceptions</value>
     <value>windows</value>
   </list>
  </property>
</bean>
</beans>

예제에서 클래스패스에 format, exceptions, windows라는 3개의 리소스 번들이 정의되어 있다고 가정한다. 메시지를 처리하는 어떤 요청이라도 리소스번들을 통해 메시지를 처리하는 JDK 표준방법으로 다룰 것이다. 예제의 의도를 위해 위의 리소스 번들 파일 중 2개의 내용이 다음과 같다고 가정한다.
# in format.properties
message=Alligators rock!




# in exceptions.properties
argument.required=The '{0}' argument is required.



MessageSource 기능을 실행하는 프로그램은 다음 예제에 나와있다. 모든 ApplicationContext 구현체는 MessageSource의 구현체이기고 하므로 MessageSource 인터페이스로 캐스팅 될 수 있다는 것을 기억해라.
public static void main(String[] args) {
  MessageSource resources = new ClassPathXmlApplicationContext("beans.xml");
  String message = resources.getMessage("message", null, "Default", null);
  System.out.println(message);
}



위 프로그램은 다음과 같은 아웃풋이 될 것이다.
Alligators rock!



그래서 요약하자면 MessageSource는 클래스패스 루트경로에 있는 beans.xml 파일에 정의되어 있다. messageSource 빈 정의는 basenames 프로퍼티에서 다수의 리소스 번들을 참조한다. basenames 프로퍼티의 리스트로 전달한 3개의 파일은 클래스패스 루트경로에 있는 파일들이고 각각 format.properties, exceptions.properties, windows.properties라는 이름이 된다.

다음 예제는 메시지 검색을 위해 아규먼트를 전달하는 것을 보여준다. 이러한 아규먼트들을 문자열로 변환해서 검색 메시지의 플레이스홀더에 삽입할 것이다.
<beans>

  <!-- 이 MessageSource는 웹 어플리케이션에서 익숙하다 -->
  <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
     <property name="basename" value="test-messages"/>
  </bean>

  <!-- 위의 MessageSource를 이 POJO에 주입한다 -->
  <bean id="example" class="com.foo.Example">
     <property name="messages" ref="messageSource"/>
  </bean>

</beans>

public class Example {

  private MessageSource messages;

  public void setMessages(MessageSource messages) {
      this.messages = messages;
  }

  public void execute() {
      String message = this.messages.getMessage("argument.required",
          new Object [] {"userDao"}, "Required", null);
      System.out.println(message);
  }

}



execute()를 호출의 결과는 다음과 같은 아웃풋이 될 것이다.
The userDao argument is required.



국제화 (i18n)와 관련해서 스프링의 여러가지 MessageResource 구현체는 표준 JDK ResourceBundle과 같은 로케일(locale) 처리방법과 장애복구 규칙을 따른다. 간단히 말해서 앞에서 정의한 messageSource 예제에서 영국 (en-GB) 로케일로 메시지를 처리하려면 format_en_GB.properties, exceptions_en_GB.properties, windows_en_GB.properties파일을 각각 생성해야 한다.

보통 로케일 처리는 어플리케이션 환경에 의해 관리된다. 이 예제에서 (영국) 메시지가 처리되어야 하는 로케일은 수정으로 지정한다.
# exceptions_en_GB.properties 파일
argument.required=Ebagum lad, the '{0}' argument is required, I say, required.

public static void main(final String[] args) {
  MessageSource resources = new ClassPathXmlApplicationContext("beans.xml");
  String message = resources.getMessage("argument.required",
      new Object [] {"userDao"}, "Required", Locale.UK);
  System.out.println(message);
}



위의 프로그램을 실행하면 다음과 같은 아웃풋이 될 것이다.
Ebagum lad, the 'userDao' argument is required, I say, required.


정의된 MessageSource에 대한 참조를 얻으려고 MessageSourceAware 인터페이스를 사용할 수도 있다. MessageSourceAware 인터페이스를 구현한 ApplicationContext에서 정의된 어떤 빈이라도 빈이 생성되고 설정될 때 어플리케이션 컨텍스트의 MessageSource와 함께 주입된다.

Note
ResourceBundleMessageSource에 대한 대안으로 스프링은 ReloadableResourceBundleMessageSource 클래스를 제공한다. 이 클래스는 같은 번들 파일 형식을 지원하지만 ResourceBundleMessageSource 구현체에 기반한 표준 JDK보다 더 유연하다. 특히 이 클래스는 어떤 스프링 리소스 위치(단지 클래스 패스가 아니라)에서도 파일을 읽을 수 있고 번들 프로퍼티파일의 핫 리로딩을 지원한다.(동시에 중간에서 이들을 캐싱한다.) 더 자세한 내용은 ReloadableResourceBundleMessageSource 자바독을 확인해 봐라.


4.14.2 표준 이벤트와 커스텀 이벤트
ApplicationContext의 이벤트 핸들링은 ApplicationEvent 클래스와 ApplicationListener 인터페이스를 통해서 제공한다. ApplicationListener인터페이스를 구현한 빈이 컨텍스트에 배포되면 ApplicationContext에 퍼블리싱되는 것을 ApplicationEvent가 얻을 때마다 빈에 통지한다. 본질적으로 이는 표준 Observer 디자인 패턴이다. 스프링은 다음과 같은 표준 이벤트를 제공한다.

Table 4.7. 내장 이벤트
이벤트설명
ContextRefreshedEventApplicationContext가 초기화되거나 갱신될 때 퍼블리싱된다. 예를 들어 ConfigurableApplicationContext 인터페이스의 refresh() 메서드를 사용하는 경우다. 여기서 "초기화"는 모든 빈이 로드되고 후처리자(post-processor) 빈을 탐지해서 활성화하고 싱글톤이 미리 인스턴스화되고 ApplicationContext 객체를 사용할 준비가 되었음을 의미한다. 컨텍스트가 닫히지 않는 한 여러 번 갱신될 수 있고 선택된 ApplicationContext는 사실 "핫" 리프레시등을 지원한다. 예를 들면 XmlWebApplicationContext 핫 리프레시를 지원하지만 GenericApplicationContext는 지원하지 않는다.
ContextStartedEventConfigurableApplicationContext 인터페이스의 start() 메서드를 사용해서 ApplicationContext이 시작될 때 퍼블리싱된다. 여기서 "시작한다"는 의미는 모든 Lifecycle 빈이 명시적으로 시작 신호를 받는다는 말이다. 이 신호는 보통 명시적인 멈춤 후에 빈을 재시작하는 데 사용하지만 자동 시작이 설정되지 않은 컴포넌트들을 시작하는 데도 사용할 수 있다. 초기화 때 시작되지 않은 컴포넌트들이 그 예이다.
ContextStoppedEventConfigurableApplicationContext 인터페이스의 stop() 메서드를 사용해서 ApplicationContext이 멈췄을 때 퍼블리싱된다. 여기서 "멈춘다"는 의미는 모든 Lifecycle 빈이 명시적인 멈춤 신호를 받는다는 뜻이다. 멈춰진 컨텍스트는 start() 호출로 재시작할 수 있다.
ContextClosedEventConfigurableApplicationContext의 close() 메서드를 사용해서 ApplicationContext이 닫혔을 때 퍼블리싱된다. 여기서 "닫힌다"는 의미는 모든 싱글톤 빈이 파괴되었다는 의미이다. 닫혀진 컨텍스트는 생명의 끝에 도달한다. 이 컨텍스트는 갱신하거나 재시작할 수 없다.
RequestHandledEventHTTP 요청이 서비스되는 모든 빈에 전하는 웹에 특정화된(web-specific) 이벤트. 이 이벤트는 요청이 완료된 후에 퍼블리싱된다. 이 이벤트는 스프링의 DispatcherServlet을 사용하는 웹 어플리케이션에만 적용할 수 있다.






























자신만의 커스텀 이벤트를 생성해서 퍼블리싱할 수도 있다. 이 예제는 스프링의 ApplicationEvent 기반 클래스를 확장하는 간단한 클래스를 보여준다.
public class BlackListEvent extends ApplicationEvent {
  private final String address;
  private final String test;

  public BlackListEvent(Object source, String address, String test) {
      super(source);
      this.address = address;
      this.test = test;
  }

  // 접근자와 그 외 메서드들...
}



커스텀 ApplicationEvent를 퍼블리싱하려면 ApplicationEventPublisher의 publishEvent() 메서드를 호출해라. 보통 이는 ApplicationEventPublisherAware를 구현한 클래스를 생성하고 스프링 빈으로 등록함으로써 완료된다. 다음 예제는 이러한 클래스를 보여준다.
public class EmailService implements ApplicationEventPublisherAware {

  private List<String> blackList;
  private ApplicationEventPublisher publisher;

  public void setBlackList(List<String> blackList) {
      this.blackList = blackList;
  }

  public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
      this.publisher = publisher;
  }

  public void sendEmail(String address, String text) {
      if (blackList.contains(address)) {
          BlackListEvent event = new BlackListEvent(this, address, text);
          publisher.publishEvent(event);
          return;
      }
      // 이메일 보내기...
  }
}


설정타임시에 스프링 컨테이너는 ApplicationEventPublisherAware를 구현한 EmailService를 탐지할 것이고 자동적으로 setApplicationEventPublisher()를 호출할 것이다. 실제로는 전달한 파라미터는 스프링 컨테이너 자체가 될 것이다. ApplicationEventPublisher 인터페이스를 통해서 어플리케이션 컨텍스트와 간단히 상호작용한다.

커스텀 ApplicationEvent를 받으려면 ApplicationListener를 구현한 클래스를 생성하고 스프링 빈으로 등록해라. 다음 에제는 이러한 클래스를 보여준다.
public class BlackListNotifier implements ApplicationListener<BlackListEvent> {

  private String notificationAddress;

  public void setNotificationAddress(String notificationAddress) {
      this.notificationAddress = notificationAddress;
  }

  public void onApplicationEvent(BlackListEvent event) {
        // notificationAddress를 통해서 적절한 같은 그룹에 알린다...
  }
}


ApplicationListener는 일반적으로 커스텀 이벤트의 타입인 BlackListEvent으로 파라미터화된다. 이는 onApplicationEvent() 메서드가 타입세이프할 수 있고 다운캐스팅이 필요한 경우를 피한다는 의미이다. 필요한 만큼의 많은 이벤트 리스터를 등록할 수 있지만 기본적으로 이벤트 리스너는 동기적으로 이벤트를 받는다. 이는 모든 리스너가 이벤트 처리를 완료할 때까지 publishEvent()가 블락된다는 의미이다. 이 동기방식과 싱글쓰레드 접근의 한가지 이점은 리스너가 이벤트를 받을 때 트랜잭션 컨텍스트를 사용할 수 있으면 퍼블리셔의 트랜젝션 컨텍스트내에서 수행된다는 것이다. 이벤트 발행에 대한 또다른 전략이 필요하다면 스프링의 ApplicationEventMulticaster 인터페이스에 대한 JavaDoc을 참고해라.

다음 예제는 위의 클래스 각각을 등록하고 설정하는 빈 정의를 보여준다.
<bean id="emailService" class="example.EmailService">
  <property name="blackList">
      <list>
          <value>known.spammer@example.org</value>
          <value>known.hacker@example.org</value>
          <value>john.doe@example.org</value>
      </list>
  </property>
</bean>

<bean id="blackListNotifier" class="example.BlackListNotifier">
  <property name="notificationAddress" value="blacklist@example.org"/>
</bean>



emailService빈의 sendEmail()를 호출할 때 블랙리스트에 있는 이메일이 있다면 BlackListEvent 타입의 커스텀 이벤트가 퍼블리시된다. blackListNotifier 빈이 ApplicationListener으로 등록되므로 BlackListEvent를 받아서 적당한 같은 그룹에게 알릴 수 있다.

Note
스프링의 이벤트 메카니즘은 같은 어플리케이션 컨텍스트내의 스프링 빈간의 간단한 통신을 위해 디자인되었다. 하지만 더 복잡한 엔터프리이즈 통합이 필요하면 따로 관리되는 Spring Integration 프로젝트가 잘 알려진 스프링 프로그래밍 모델에 기반해서 경량이고 패턴지향(pattern-oriented) 이고 이벤트기반의 아키텍쳐를 구성하는데 대한 완전한 지원을 제공한다.


4.14.3 저수준 리소스에 편리하게 접근하기
어플리케이션 컨텍스트의 가장 바람직한 사용과 이해를 위해서는 Chapter 5, Resources 챕터에서 설명했듯이 사용자들은 보통 스프링의 Resource 추상화에 스스로 익숙해져야 한다.

어플리케이션 컨텍스트는 Resource를 로드하는데 사용할 수 있는 ResourceLoader이다. Resource는 본질적으로 JDK java.net.URL 클래스의 더 많은 기능이 있는 리치(rich) 버전이다. 사실 Resource 구현체는 적절한 위치에서 java.net.URL의 인스턴스를 감싼다. Resource는 투명한 방법으로 클래스패스, 파일시스템 위치, 표준 URL로 나타낼 수 있는 위치 등 거의 대부분의 위치에서 저수준 리소스들을 획득할 수 있다. 리소스 위치 문자열이 어떤 특별한 접두사가 없는 간단한 경로라면 이라한 리소스의 위치는 실제 어플리케이션 타입을 지정하고 사용한다.

어플리케이션 컨텍스트에 배포한 빈이 특별한 콜백 인터페이스인 ResourceLoaderAware를 구현하도록 설정할 수 있다. 이렇게 함으로써 ResourceLoader처럼 어플리케이션 컨텍스트 자체에 전달된 것을 초기화 시에 자동적으로 다시 호출하게 한다. 정적 리소스에 접근하도록 Resource 타입의 프로퍼티를 노출할 수도 있다. 정적 리소스들은 다른 프로퍼티처럼 주입될 것이다. 이러한 Resource 프로퍼티들을 간단한 경로 문자열로 지정할 수도 있고 빈이 배포될 때 해당 문자열을 실제 Resource 객체로 변환하도록 하기 위해 컨텍스트가 자동으로 등록하는 특별한 JavaBean PropertyEditor에 의존할 수도 있다.

ApplicationContext 생성자에 전달한 위치 경로(location path)는 실제로 리소스 문자열이며 간단한 형식으로 적절하게 특정정한 컨텍스트 구현체처럼 다뤄진다. ClassPathXmlApplicationContext는 간단한 위치 경로를 클래스패스 위치처럼 다룬다. 실제 컨텍스트 타입에 상관없이 클래스패스나 URL에서 정의를 강제로 로드하도록 특별한 접두사를 가진 위치 경로(리소스 문자열)를 사용할 수도 있다.


4.14.4 웹 어플리케이션에 펀리한 ApplicationContext 인스턴스화
ContextLoader등을 사용해서 선언적으로 ApplicationContext 인스턴스를 생성할 수 있다. 물론 ApplicationContext 구현체중 하나를 사용해서 프로그래밍적으로 생성할 수도 있다.

ContextLoader 메카니즘은 ContextLoaderListener와 ContextLoaderServlet 두 가지에서 왔다. 이 둘은 같은 기능이지만 서블릿 2.3 컨테이너에서 신뢰할 수 없는 리스너 버전이 다르다. 서블릿 2.4 명세에서 서블릿 컨텍스트 리스너는 웹 어플리케이션의 서블릿 컨텍스트가 생성되자 마자 즉시 실행되어야 하고 첫번째 요청을 서비스할 수 있어야 한다.(그리고 서블릿 컨텍스트가 셧다운되어야 한다는 부분도 있다.) 이러한 서블릿 컨텍스트 리스너는 스프링 ApplicationContext를 초기화하기에 이상인 곳이다. 모든 것이 같다면 아마도 ContextLoaderListener이 더 나을 것이다. 호환성에 대한 더 자세한 정보는 ContextLoaderServlet 자바독을 봐라.

다음과 같이 ContextLoaderListener를 사용해서 ApplicationContext를 등록할 수 있다.
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/daoContext.xml /WEB-INF/applicationContext.xml</param-value>
</context-param>

<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<!-- 또는 위의 리스너 대신 ContextLoaderServlet를 사용한다
<servlet>
<servlet-name>context</servlet-name>
<servlet-class>org.springframework.web.context.ContextLoaderServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
-->



리스너는 contextConfigLocation 파라미터를 검사한다. 파라미터가 존재하지 않으면 리스너는 기본값인 /WEB-INF/applicationContext.xml를 사용한다. 파라미터가 존재하면 리스너는 미리 정의된 구분자 (콤마, 세미콜론, 공백)로 문자열을 분리하고 어플리케이션 컨텍스트를 검색해야하는 위치로 이 값을 사용한다. Ant 스타일 경로 패턴도 잘 지원한다. 예제들은 모두 "Context.xml"로 끝나는 파일명을 가지고 "WEB-INF" 디렉토리에 있는 /WEB-INF/*Context.xml와 "WEB-INF"의 하위디렉토리에 있는 이러한 모든 파일인 /WEB-INF/**/*Context.xml 이다.

ContextLoaderListener 대신에 ContextLoaderServlet를 사용할 수 있다. 서블릿은 마치 리스너가 하는 것처럼 contextConfigLocation 파라미터를 사용한다.


4.14.5 J2EE RAR 파일처럼 스프링 ApplicationContext 배포하기
스프링 2.5부터는 컨텍스트와 필요한 모든 빈 클래스와 라이브러리 JAR 파일들을 J2EE RAR 배포유닛으로 은닉화해서 RAR 파일로 스프링 ApplicationContext를 배포하는 것이 가능하다. 이는 단독으로 어클리케이션 컨텍스트를 실행한 것과 동일하며 단지 J2EE 환경에 호스팅해서 J2EE 서버 시설들에 접근할 수 있게 된 것이다. RAR 배포는 헤드없이 WAR 파일을 배포하는 시나리오보다 더 자연스러운 대안이다. 헤드없는 WAR 파일이라는 것은 J2EE 환경에서 스프링 어플리케이션컨텍스트를 시작하는데만 사용하는 HTTP 진입점이 없는 WAR 파일을 말한다.

RAR 배포는 HTTP 진입점이 필요없는 어플리케이션 컨텍스트의 이상형이라기 보다는 메시지 앤드포인트와 스케쥴된 잡들로만 이루어져있다. 이러한 컨텍스트의 빈들은 JTA 트랜잭션 매니저와 JNDI에 바인딩된 JDBC DataSource와 JMS ConnectionFactory 인스턴스같은 어플리케이션 서버 리소스들을 사용할 수 있고 플랫폼의 JMX서버와 함께 등록할 수도 있다. - 모든 것은 스프링의 표준 트랜잭션 관리와 JNDI, JMS 지원 부분을 통해서 이루어진다. 어플리케이션 컴포넌트는 스프링의 TaskExecutor 추상화로 어플리케이션 서버의 JCA WorkManager와 상호작용도 할 수 있다.

RAR 배포와 관련한 설정에 대한 자세한 내용은 SpringContextResourceAdapter 클래스의 JavaDoc을 확인해라.

J2EE RAR파일로 스프링 ApplicationContext을 간단히 배포하려면: 모든 어플리케이션 클래스들을 표준 JAR 파일과는 다른 확장자인 RAR 파일로 패키징한다. RAR 아카이브의 루트에 필요한 라이브러리의 JAR를 모두 추가한다. "META-INF/ra.xml" 배포 디스크립터(SpringContextResourceAdapter의 JavaDoc에 나온 것처럼)와 대응되는 스프링 XML 빈 정의 파일(보통 "META-INF/applicationContext.xml")을 추가하고 어플리케이션 서버의 배포 디렉토리에 최종 RAR 파일을 둔다.

Note
이러한 RAR 배포 유닛들은 보통 독립적이다. 컴포넌트를 외부로 노출하지 않으며 심지어 같은 어플리케이션의 다른 모듈에도 노출하지 않는다. RAR 기반 ApplicationContext와의 상호작용은 보통 다른 모듈과 공유하는 JMS 목적지(destination)를 통해서 발생한다. 예를 들어 RAR 기반 ApplicationContext는 잡(job)을 스케쥴링하거나 파일 시스템의 새로운 파일에 반응하는 등의 일도 할 수 있다. 외부의 동기적인 접근을 허용하려면 예를 들어 같은 머신의 다른 어플리케이션 모듈이 사용할 RMI 엔드포인트를 익스포트할 수 있다.




4.15 BeanFactory
BeanFactory는 스프링 IoC 기능의 근간을 이루는 원리를 제공하지만 다른 서드파티 프레임워크와의 통합에서만 직접적으로 사용되고 대부분의 스프링 유저들에게는 이제 전적으로 역사적인 것이다. BeanFactory와 BeanFactoryAware, InitializingBean, DisposableBean같은 관련 인터페이스들은 스프링과 통합된 많은 수의 서드파티 프레임워크들과의 하위호환성 때문에 여전히 스프링에 존재한다. 종종 JDK 1.4 호환성을 유지하거나 JSR-250에 대한 의존성을 없애기 위해 @PostConstruct나 @PreDestroy같은 더 최신 기능을 사용할 수 없는 서드파티 컴포넌트들이 있다.

이 섹션에서는 BeanFactory와 ApplicationContext의 차이점에 대한 추가적인 배경을 설명하고 전형적인 싱글톤 검색으로 직접 IoC 컨테이너에 어떻게 접근하는 지를 설명한다.


4.15.1 BeanFactory냐? ApplicationContext냐?
ApplicationContext을 사용하지 않을 좋은 이유가 있는 것이 아니라면 ApplicationContext을 사용해라.

ApplicationContext가 BeanFactory의 모든 기능을 포함하고 있기 때문에 메모리 소비가 중대한 문제가 될 수 있거나 조금 많은 킬로바이트가 차이를 만들 수 있는 Applet같은 몇가지 상황을 제외하고는 보통 BeanFactory보다 ApplicationContext를 더 권장한다. 하지만 대부분의 전형적인 엔터프라이즈 어플리케이션과 시스템에서는 사용하려는 것이 바로 ApplicationContext이다. 스프링 2.0이나 그 이상의 버전에서는 BeanPostProcessor 확장점을 많이 사용하게 된다.(프록싱하려는 등등) 평이한 BeanFactory만 사용한다면 트랜잭션이나 AOP같은 꽤 많은 지원을 사용할 수 없을 것이다. 최소한 개발자가 추가적인 어떤 작업을 해야한다. 이 상황은 설정에는 실제로 잘못된 곳이 없기 때문에 혼란스러울 수 있다.

다음 표는 BeanFactory와 ApplicationContext 인터페이스와 구현체가 제공하는 기능 목록이다.

Table 4.8. 기능 매트릭스
기능BeanFactory
ApplicationContext
빈 인스턴스화/연결YesYes
자동 BeanPostProcessor 등록NoYes
자동 BeanFactoryPostProcessor  등록
NoYes
편리한 MessageSource 접근 (i18n에 대한)No
Yes
ApplicationEvent 발행NoYes









BeanFactory 구현체와 함께 빈 후처리자(bean post-processor)를 명시적으로 등록하려면 다음과 같은 코드를 반드시 작성해야 한다.
ConfigurableBeanFactory factory = new XmlBeanFactory(...);

// 필요한 BeanPostProcessor 인스턴스를 등록한다
MyBeanPostProcessor postProcessor = new MyBeanPostProcessor();
factory.addBeanPostProcessor(postProcessor);

// 팩토리를 사용해서 시작한다


BeanFactory를 사용할 때 BeanFactoryPostProcessor를 명시적으로 등록하려면 다음과 같은 코드를 반드시 작성해야 한다.
XmlBeanFactory factory = new XmlBeanFactory(new FileSystemResource("beans.xml"));

// Properties 파일에서 몇몇 프로퍼티 값들을 가져온다
PropertyPlaceholderConfigurer cfg = new PropertyPlaceholderConfigurer();
cfg.setLocation(new FileSystemResource("jdbc.properties"));

// 이제 실제로 교체한다
cfg.postProcessBeanFactory(factory);


아주 대다수의 스프링을 사용하는 어플리케이션에서 다양한 ApplicationContext 구현체들은 위의 평이한 BeanFactory 구현체를 선호하기 때문에 두 경우의 명시적인 등록과정은 편리하지 않다. 특히 BeanFactoryPostProcessors와 BeanPostProcessors를 사용할 때 더욱 그렇다. 이러한 메카니즘은 프로퍼티 플레이스홀더 교체와 AOP같은 중요한 기능을 구현한다.


4.15.2 딱 붙은 코드(Glue code)와 나쁜 싱글톤
스프링 IoC 컨테이너가 서비스하는 코드와 생성될 때 컨테이너가 의존성을 제공하는 코드, 컨테이너가 인지하지 못하는 코드는 의존성 주입(DI) 스타일로 대부분의 어플리케이션을 작성하는 것이 가장 좋다. 하지만 때로는 다른 코드와 함께 묶을 필요가 있어서 코드의 딱 붙은 작은 레이어에는 때로 스프링 IoC 컨테이너에 접근하는 싱글톤(또는 quasi-singleton) 스타일이 필요하다. 예를 들어 서드파티 코드는 스프링 IoC 컨테이너에서 새로운 객체들을 얻는 능력없이 직접 새로운 객체들을 생성하려고 할 수도 있다.(Class.forName() 스타일) 서드파티 코드가 생성한 객체가 작은 스텁이나 프록시이면서 실제 객체를 얻는 것을 위임하려고 스프링 IoC 컨테이너에 접근하는 싱글톤 스타일을 사용한다면 제어의 역전은 여전히 대부분의 코드를 획득한다. 그러므로 대부분의 코드는 여전히 컨테이너를 알지 못하거나 어떻게 접근해야 하는지 모르고 모든 이득을 보장한 채로 다른 코드와 커플링이 없다. EJB는 평이한 자바 구현 객체에 위임하고, 스프링 IoC 컨테이서 획득하려고 이 스텁/프록시 접근을 사용할 것이다. 스프링 IoC 컨테이너 자체는 이상적으로 싱글톤이 되지 않기 때문에 각 빈이 자신의 싱글톤이 아닌 스프링 IoC 컨테이너를 사용하는 빈에서는 메모리 사용이나 초기화 시간 면에서 비현실적이 될 수 있다. (하이버네이트 SessionFactory같은 스프링 IoC 컨테이너의 빈을 사용할 때)

EJB 2.1 환경에서나 WAR 파일을 넘어서 WebApplicationContexts에 부모로 단일 ApplicationContext를 공유하고 싶을 때 서비스 로케이터 스타일로 어플리케이션 컨텍스트 검색하는 것은 때로 공유된 스프링이 관리하는 컨포넌트에 접근하기 위한 유일한 선택사항이다. 이 경우에 이 스프링 소스 팀블로그 글 에서 설명한 ContextSingletonBeanFactoryLocator 로케이터 유틸리티 클래스를 사용해야 한다.
2012/05/15 01:57 2012/05/15 01:57
크리에이티브 커먼즈 라이센스
Creative Commons License

Leave a Reply

Facebook Comments

  • Categories

    List (769)
    BlaBlaBla~ (114)
    JAVA (128)
    Scala (49)
    .NET (21)
    PHP (1)
    Database (29)
    Programming (109)
    Publishing (35)
    Javascript (117)
    node.js (60)
    CoffeeScript (10)
    Ruby on Rails (11)
    RIA (10)
    Web 2.0 & Semantic (46)
    Ubuntu (6)
    Mobile (23)
  • Tag Cloud

  • Calendar

    «   2012/05   »
        1 2 3 4 5
    6 7 8 9 10 11 12
    13 14 15 16 17 18 19
    20 21 22 23 24 25 26
    27 28 29 30 31    
  • Archives

  • Node.js 프로그래밍
    JavaScript JS Documentation: JS RegExp lastIndex, JavaScript RegExp lastIndex, JS RegExp .lastIndex, JavaScript RegExp .lastIndex
    SAVE THE
    DEVELOPERS <!>
    Upgrade IE 6 Now!
  • Recent Posts

  • Recent Comments

  • Recent Trackbacks

  • Recent My Delicious

  • Site Stats

    • Total hits: 1599939
    • Today: 427
    • Yesterday: 2073
  • 2397

    1714

    0

    -30 days

    today : 427

    Google PageRank Checker Powered by  MyPagerank.Net