spring로 검색한 결과 :: 시소커뮤니티[SSISO Community]
 
SSISO 카페 SSISO Source SSISO 구직 SSISO 쇼핑몰 SSISO 맛집
추천검색어 : JUnit   Log4j   ajax   spring   struts   struts-config.xml   Synchronized   책정보   Ajax 마스터하기   우측부분

회원가입 I 비밀번호 찾기


SSISO Community검색
SSISO Community메뉴
[카페목록보기]
[블로그등록하기]  
[블로그리스트]  
SSISO Community카페
블로그 카테고리
정치 경제
문화 칼럼
비디오게임 스포츠
핫이슈 TV
포토 온라인게임
PC게임 에뮬게임
라이프 사람들
유머 만화애니
방송 1
1 1
1 1
1 1
1 1
1

spring로 검색한 결과
등록일:2008-04-02 14:48:56
작성자:
제목:자바 개발자를 위한 Ajax: Jetty와 Direct Web Remoting을 사용하여 확장 가능한 Comet 애플리케이션 개발하기 (한글)


Continuations와 Reverse Ajax를 사용하여 이벤트 중심 웹 애플리케이션 구현하기

developerWorks

난이도 : 중급

Philip McCarthy, Java development consultant, Independent

2007 년 9 월 04 일

비동기식 서버 측 이벤트에 의해서 구동되는 Ajax 애플리케이션들은 구현하기가 까다롭고 확장도 어렵습니다. Philip McCarthy는 이러한 문제에 대한 효과적인 솔루션을 이 시리즈로 제시합니다. Comet 패턴을 사용하여 데이터를 클라이언트로 보내고, Jetty 6의 Continuations API를 사용하여 Comet 애플리케이션을 더 많은 클라이언트로 확장할 수 있습니다. 여러분은 Direct Web Remoting 2의 Reverse Ajax 기술과 Comet과 Continuations를 편리하게 활용할 수 있게 되었습니다.
소셜 북마크

mar.gar.in mar.gar.in
digg Digg
del.icio.us del.icio.us
Slashdot Slashdot

웹 애플리케이션 개발 기술로 확실히 자리매김한 Ajax로 인해, 공통적인 Ajax 사용 패턴이 등장하게 되었다. 예를 들어, Ajax는 사용자 인풋에 대한 응답으로 사용되어 서버로부터 전달된 새로운 데이터로 페이지의 부분들을 수정한다. 가끔씩, 웹 애플리케이션의 사용자 인터페이스는 사용자 액션 없이, 비동기식으로 발생하는 서버 측 이벤트에 대한 응답으로 업데이트 되어 Ajax 채팅 애플리케이션에 도착하는 새로운 메시지를 보여주거나 협업 텍스트 에디터에서 또 다른 사용자가 만든 변경 사항들을 디스플레이 한다. 웹 브라우저와 서버 간 HTTP 연결은 브라우저에 의해서만 구축될 수 있으므로, 서버는 변경 사항이 발생할 때 마다 브라우저로 그 변경 사항을 보낼(push) 수 없다.

Ajax 애플리케이션들은 이러한 문제를 해결하기 위해 두 개의 기본적인 접근 방식을 사용한다. 브라우저가 몇 초 마다 업데이트에 서버를 폴링(poll)하거나, 서버에서 브라우저간 연결을 유지하여 데이터를 사용할 수 있을 때 데이터를 전달하는 것이다. 오랜 기간 살아있는 연결 기술을 Comet이라고 한다. (참고자료) 이 글에서는 Jetty 서블릿 엔진과 DWR을 사용하여 Comet 웹 애플리케이션을 간단하고 효율적으로 구현하는 방법을 설명한다.

왜 Comet인가?

폴 링(polling) 방식의 가장 큰 단점은 많은 클라이언트로 스케일링 할 때 생성되는 트래픽의 양이다. 각 클라이언트는 주기적으로 서버를 히트하여 업데이트를 체크하는데, 이는 서버의 리소스에 부담이 된다. 최악의 상황은 Ajax 메일 인박스 같은 흔치 않는 업데이트가 포함된 애플리케이션이다. 이 경우, 대부분의 클라이언트 폴(poll)은 과잉이 되고, 서버는 "어떤 데이터도 없음(no data yet)"이라고 간단히 대답한다. 서버 로딩은 폴링 간격을 늘림으로써 경감될 수 있지만, 서버 이벤트와 클라이언트의 인식 간 지연을 초래한다. 물론, 많은 애플리케이션들을 위해 합리적인 균형이 이루어지고 있고 폴링도 잘 작동한다.

그럼에도 불구하고, Comet 전략의 매력 중 하나는 효율성에 있다. 클라이언트는 폴링의 소란스러운 트래픽 특성을 만들어 내지 않고, 이벤트가 발생하자마자 클라이언트로 퍼블리시 될 수 있다. 하지만, 개방된 연결 역시 서버 리소스를 소비한다. 서블릿은 대기 상태에서 영속 요청을 보유하고 있는 동안, 서블릿은 쓰레드를 독점한다. 이것은 Comet의 확장성을 전통적인 서블릿 엔진으로 제한한다. 클라이언트의 수는 서버 스택이 효율적으로 핸들할 수 있는 쓰레드의 수를 빠르게 능가하기 때문이다.




위로


Jetty 6의 다른 점

Jetty 6는 많은 동시 연결들로 스케일링 되고, 자바™ Nonblocking I/O (java.nio) 라이브러리를 활용하고 최적화 된 아웃풋-버퍼 아키텍처를 사용할 수 있다. (참고자료) Jetty 역시 장기 연결을 다루는 트릭이 있다. 바로 Continuations이 라고 하는 기능이다. 필자는 이 Continuations를 요청 받고, 2초 동안 기다린 다음, 응답을 보내는 간단한 서블릿으로 설명하겠다. 그런 다음, 서버를 핸들할 쓰레드보다 더 많은 클라이언트를 서버가 갖고 있을 때 어떤 일이 발생하는지를 설명하겠다. 마지막으로, Continuations를 사용하여 서블릿을 재구현 하면서, 그 차이를 알게 될 것이다.

다음 예제에서 무엇이 발생하는지를 보다 쉽게 알 수 있도록, Jetty 서블릿 엔진을 단일 요청-핸들링 쓰레드로 제한할 것이다. Listing 1은 jetty.xml의 관련 설정 모습이다. ThreadPool에 총 세 개의 쓰레드를 허용해야 한다. Jetty 서버가 하나를 사용하고, 또 하나는 HTTP 커넥터를 실행하면서 인커밍 요청을 리스닝 한다. 나머지 한 개의 쓰레드는 서블릿 코드를 실행한다.


Listing 1. 단일 서블릿 쓰레드를 위한 Jetty 설정
                
<?xml version="1.0"?>
<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure//EN"
"http://jetty.mortbay.org/configure.dtd">
<Configure id="Server" class="org.mortbay.jetty.Server">
<Set name="ThreadPool">
<New class="org.mortbay.thread.BoundedThreadPool">
<Set name="minThreads">3</Set>
<Set name="lowThreads">0</Set>
<Set name="maxThreads">3</Set>
</New>
</Set>
</Configure>

비동기식 이벤트 대기를 시뮬레이트 하기 위해, Listing 2BlockingServletservice() 메소드를 보여주고 있다. 이것은 Thread.sleep() 호출을 사용하여 완료하기 전에 2,000 밀리초 동안 중지한다. 또한 실행의 시작과 끝에서 시스템 시간을 출력한다. 다른 요청들로부터 아웃풋을 명확히 하기 위해, 식별자로서 사용된 요청 매개변수도 기록한다.


Listing 2. BlockingServlet
                
public class BlockingServlet extends HttpServlet {

public void service(HttpServletRequest req, HttpServletResponse res)
throws java.io.IOException {

String reqId = req.getParameter("id");

res.setContentType("text/plain");
res.getWriter().println("Request: "+reqId+"\tstart:\t" + new Date());
res.getWriter().flush();

try {
Thread.sleep(2000);
} catch (Exception e) {}

res.getWriter().println("Request: "+reqId+"\tend:\t" + new Date());
}
}

여러 동시 요청들에 대한 응답에 서블릿이 어떻게 작동하는지 관찰할 수 있다. Listing 3lynx를 사용하여 다섯 개의 병렬 요청의 콘솔 아웃풋을 보여준다. 이 명령어는 다섯 개의 lynx 프로세스를 시작하면서, 식별 서수를 요청 URL에 붙인다.


Listing 3. 여러 동시 요청에서 BlockingServlet까지의 아웃풋
                

$ for i in 'seq 1 5' ; do lynx -dump localhost:8080/blocking?id=$i & done
Request: 1 start: Sun Jul 01 12:32:29 BST 2007
Request: 1 end: Sun Jul 01 12:32:31 BST 2007

Request: 2 start: Sun Jul 01 12:32:31 BST 2007
Request: 2 end: Sun Jul 01 12:32:33 BST 2007

Request: 3 start: Sun Jul 01 12:32:33 BST 2007
Request: 3 end: Sun Jul 01 12:32:35 BST 2007

Request: 4 start: Sun Jul 01 12:32:35 BST 2007
Request: 4 end: Sun Jul 01 12:32:37 BST 2007

Request: 5 start: Sun Jul 01 12:32:37 BST 2007
Request: 5 end: Sun Jul 01 12:32:39 BST 2007

Listing 3의 아웃풋은 놀랍지 않다. 서블릿의 service() 메소드를 실행하기 위해서는 단 하나의 쓰레드만 Jetty에 사용될 수 있으므로, Jetty는 각각의 요청을 대기열에 놓고 이를 직렬로 제공한다. 타임 스탬프는 응답이 하나의 요청(end 메시지)을 위해 파견된 후에 바로 이를 보여주고, 서블릿은 다음 요청(start 메시지)에 대해 작동을 시작한다. 다섯 개의 모든 요청들이 동시에 보내지기 때문에, 한 개의 요청은 서블릿이 이를 핸들할 수 있기 전에 8초를 기다려야 한다.

서 블릿이 블로킹 되는 동안 어떤 유용한 작업도 수행되지 않는다. 이 코드는 이벤트가 애플리케이션의 또 다른 부분에서 비동기식으로 도착하기를 기다리는 상황을 시뮬레이트 한다. 이 서버는 CPU나 I/O에 속해있지 않다. 요청은 쓰레드-풀 소진의 결과로 인해서만 대기열에 놓인다.

이제 Jetty 6의 Continuations 기능이 이러한 상황에 어떻게 도움을 주는지를 알아보자. Listing 4는 Continuations API를 사용하여 재 작성된 Listing 2BlockingServlet이다. 이 코드는 나중에 설명하겠다.


Listing 4. ContinuationServlet
                
public class ContinuationServlet extends HttpServlet {

public void service(HttpServletRequest req, HttpServletResponse res)
throws java.io.IOException {

String reqId = req.getParameter("id");

Continuation cc = ContinuationSupport.getContinuation(req,null);

res.setContentType("text/plain");
res.getWriter().println("Request: "+reqId+"\tstart:\t"+new Date());
res.getWriter().flush();

cc.suspend(2000);

res.getWriter().println("Request: "+reqId+"\tend:\t"+new Date());
}
}

Listing 5ContinuationServlet에 대한 다섯 개의 동시 요청들의 아웃풋이다. Listing 3과 비교해 보라.


Listing 5. ContinuationServlet에 대한 여러 동시 요청의 아웃풋
                
$ for i in 'seq 1 5' ; do lynx -dump localhost:8080/continuation?id=$i & done

Request: 1 start: Sun Jul 01 13:37:37 BST 2007
Request: 1 start: Sun Jul 01 13:37:39 BST 2007
Request: 1 end: Sun Jul 01 13:37:39 BST 2007

Request: 3 start: Sun Jul 01 13:37:37 BST 2007
Request: 3 start: Sun Jul 01 13:37:39 BST 2007
Request: 3 end: Sun Jul 01 13:37:39 BST 2007

Request: 2 start: Sun Jul 01 13:37:37 BST 2007
Request: 2 start: Sun Jul 01 13:37:39 BST 2007
Request: 2 end: Sun Jul 01 13:37:39 BST 2007

Request: 5 start: Sun Jul 01 13:37:37 BST 2007
Request: 5 start: Sun Jul 01 13:37:39 BST 2007
Request: 5 end: Sun Jul 01 13:37:39 BST 2007

Request: 4 start: Sun Jul 01 13:37:37 BST 2007
Request: 4 start: Sun Jul 01 13:37:39 BST 2007
Request: 4 end: Sun Jul 01 13:37:39 BST 2007

Listing 5에서 주목해야 할 두 가지 중요한 것이 있다. 첫 번째는, 각 start 메시지가 두 번 나타난다. 이것에 대해 걱정하지 말라. 두 번째는, 더 중요한 것으로서, 요청은 이제 큐잉(queueing) 없이 동시에 핸들된다. 모든 startend 메시지의 타임 스탬프는 같다. 결과적으로, 어떤 요청도 2초 이상은 걸리지 않는다. 심지어 단 하나의 서블릿 쓰레드가 실행되더라도 마찬가지이다.




위로


Jetty의 Continuations 메커니즘

Jetty의 Continuations 메커니즘이 어떻게 구현되었는지를 이해해는 것이 Listing 5를 파악하는 열쇠가 된다. Continuations를 사용하기 위해, Jetty는 SelectChannelConnector로 요청을 핸들하도록 설정되어야 한다. 이 커넥터는 java.nio API를 기반으로 구현되고, 각각에 대한 쓰레드를 소비하지 않고 연결을 열 수 있다. SelectChannelConnector가 사용될 때, ContinuationSupport.getContinuation()SelectChannelConnector.RetryContinuation의 인스턴스를 제공한다. (하지만, Continuation 인터페이스에 대해서만 코딩해야 한다. 이식성과 Continuations API 참조) suspend()RetryContinuation에 호출되면, 특별한 런타임 예외(RetryRequest)를 던지는데, 이는 서블릿 밖으로 전파되고 필터 체인으로 가서 SelectChannelConnector에 잡힌다. 이 예외의 결과로 클라이언트에 어떤 응답이라도 보내는 대신, 요청은 중지된 Continuation의 큐에 보유되고, HTTP 연결은 열린 상태가 된다. 이 시점에서, 요청을 제공하기 위해 사용될 수 있는 쓰레드는 ThreadPool로 리턴되고, 여기에서 또 다른 요청들을 제공하는데 이것이 사용될 수 있다.

이식성과 Continuations API

Jetty의 SelectChannelConnector를 사용하여 Continuations 기능을 실행해야 한다고 언급한 바 있다. 하지만, Continuations API는 여전히 전통적인 SocketConnector를 사용하는데, 이 경우, Jetty는 wait()/notify() 작동을 사용하는 다른 Continuation 구현에 위치하게 된다. 여러분의 코드는 여전히 컴파일 및 실행되지만, Nonblocking Continuations의 혜택은 받지 못한다. 비 Jetty 서버를 사용하는 옵션을 유지하고 싶다면 런타임 시 Jetty의 Continuations 라이브러리의 가용성 여부를 체크할 때 리플렉션을 사용하는 고유의 Continuation 래퍼를 작성해 보는 것도 좋다. DWR은 이 전략을 사용한다.

중지된 요청은 지정된 타임아웃이 종료되거나 resume() 메소드가 Continuation에 호출될 때까지 중지된 Continuation 큐에 남아있게 된다. 이러한 도 조건 중 하나라도 발생하면, 요청은 (필터 체인을 통해) 서블릿으로 다시 제출된다. 실제로, 전체 요청은 suspend()가 처음 호출되었던 지점이 될 때까지 "재생"된다. 실행이 suspend() 호출로 두 번째 도달하면, RetryRequest 예외는 던져지지 않고 실행은 정상적으로 진행된다.

Listing 5의 아웃풋은 이제 이치에 맞는다. 각 요청이 서블릿의 service() 메소드로 들어가고, start 메시지가 응답으로 보내진 다음에, Continuationsuspend() 메소드는 서블릿을 떠나서, 다음 요청을 제공할 수 있도록 쓰레드를 비운다. 다섯 개의 모든 요청들이 service() 메소드의 처음 부분을 통해 빠르게 실행되고, 중지된 상태로 들어가며, 모든 start 메시지들은 밀리초 내에 출력된다. 2초가 지난 후에, suspend() 타임아웃이 종료될 때, 첫 번째 요청은 중지된 큐에서 검색되고 ContinuationServlet으로 다시 제출된다. start 메시지는 두 번째에 출력되고, suspend()에 대한 두 번째 호출이 즉시 리턴되며, end 메시지가 응답 시 보내진다. 서블릿 코드는 대기열에 있는 다음 요청을 위해 다시 실행된다.

BlockingServletContinuationServlet 케이스 모두, 요청들은 단일 서블릿 쓰레드로의 액세스를 위해 대기열에 놓인다. 하지만, BlockingServlet에서의 2초 중지가 서블릿의 실행 쓰레드 내에서 발생하는 반면, ContinuationServlet의 중지는 SelectChannelConnector의 서블릿 밖에서 발생한다. ContinuationServlet의 전체적인 처리량은 더 높다. 이 서블릿 쓰레드는 sleep() 호출 시 대부분의 시간을 정지시키지 않기 때문이다.




위로


Continuations를 유용하게!

Continuations가 서블릿 요청이 쓰레드 소비 없이 중지되는 것에 대해 배웠으므로, 실제 상황에 Continuations를 사용하는 방법에 대해 Continuations API를 통해 자세히 알아보도록 하자.

resume() 메소드는 suspend()와 쌍을 이룬다. 이들을 표준 Object wait()/notify() 메커니즘과 동격으로 생각하면 된다. suspend()은 타임아웃이 종료하거나 또 다른 쓰레드가 resume()을 호출할 때까지 Continuation를 붙잡고 있다. (따라서 이것은 현재 메소드의 실행이 된다.) suspend()/resume() 쌍은 실제 Comet 스타일 서비스를 Continuations를 이용하여 구현하는 열쇠이다. 기본 패턴은 현재 요청에서 Continuation를 획득하고, suspend()을 호출하고, 비동기식 이벤트가 도착할 때까지 기다린다. 그리고 나서, resume()을 호출하고 응답을 만든다.

하지만, Scheme 같은 언어의 언어 레벨 Continuations나 자바의 wait()/notify() 패러다임과는 달리, Jetty의 Continuationresume()을 호출하는 것이 코드 실행으로 이것이 중지되는 장소를 정확히 짚어낸다는 것을 의미하지는 않는다. 여러분도 보다시피, 실제로 발생하는 일은, Continuation와 연관된 요청이 재생된다는 점이다. 이는 두 가지 문제를 가져온다. Listing 4ContinuationServlet에서처럼 바람직하지 못한 코드의 재실행 및 상태의 손실을 가져온다. suspend()로 호출이 일어날 때마다 범위 안에 있는 모든 것이 소실된다.

첫 번째 문제에 대한 솔루션은 isPending() 메소드이다. isPending()의 리턴 값이 true라면, suspend()이 앞서 호출되었다는 것을 의미하고, 재시도 된 요청의 실행이 아직 suspend()에 도착하지 않았다는 것을 의미한다. 다시 말해서, isPending()suspend() 호출에 앞서 코드를 만들면 요청 당 단 한번만 실행하는 것을 보장할 수 있다. suspend() 호출이 멱등이 되기 전에 애플리케이션 코드를 디자인 하는 것이 최상의 방법이기 때문에, 이것을 두 번 호출하는 것은 문제가 되지 않지만, isPending()을 사용할 수 없는 경우도 있다. Continuation는 또한 상태를 보존하는 메커니즘도 제공한다. putObject(Object)getObject() 메소드이다. 이 메소드를 사용하여 Continuation가 중지될 때 보존하고자 하는 상태로 컨텍스트 객체를 보유할 수 있다. 쓰레드들 간 이벤트 데이터를 전달하는 방식으로서 이 메커니즘을 사용할 수도 있다.




위로


Continuations 기반 애플리케이션 작성하기

실 제 시나리오에서, 기본적인 GPS 위치 추적 웹 애플리케이션을 개발하려고 한다. 불규칙한 간격으로 무작위 위도-경도 쌍을 생성할 것이다. 이렇게 생성된 좌표는 대중 교통의 위치, 마라톤 선수가 갖고 있는 GPS 장치, 자동차 랠리, 배송 중인 화물의 위치가 될 수 있다. 재미있는 부분은 좌표에 대해 브라우저에게 알려주는 방식이다. 그림 1은 GPS 트래커 애플리케이션을 위한 클래스 다이어그램이다.


그림 1. GPS 트래커 애플리케이션의 주요 컴포넌트를 보여주는 클래스 다이어그램
GPS 트래커 컴포넌트의 UML 클래스 다이어그램

먼저, 이 애플리케이션은 좌표를 생성하는 무엇인가 필요하다. 이것은 RandomWalkGenerator가 수행한다. 초기 좌표 쌍으로 시작하여, 개별 generateNextCoord() 메소드로의 호출은 그 위치에서 무작위로 제한된 단계를 취해서 GpsCoord 객체로서 새로운 위치를 리턴한다. 초기화 되면, RandomWalkGenerator는 무작위 간격으로 generateNextCoord() 메소드를 호출하고 생성된 좌표를 addListener()를 사용하여 등록했던 CoordListener로 보내는 쓰레드를 생성한다. Listing 6RandomWalkGenerator 루프의 로직이다.


Listing 6. RandomWalkGenerator의 run() 메소드
                
public void run() {

try {
while (true) {
int sleepMillis = 5000 + (int)(Math.random()*8000d);
Thread.sleep(sleepMillis);
dispatchUpdate(generateNextCoord());
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}

CoordListeneronCoord(GpsCoord coord) 메소드를 정의하는 콜백 인터페이스이다. 이 예제에서, ContinuationBasedTracker 클래스는 CoordListener를 구현한다. ContinuationBasedTracker에 대한 다른 퍼블릭 메소드는 getNextPosition(Continuation, int)이다. Listing 7은 이러한 메소드들의 구현이다.


Listing 7. ContinuationBasedTracker의 내부
                
public GpsCoord getNextPosition(Continuation continuation, int timeoutSecs) {

synchronized(this) {
if (!continuation.isPending()) {
pendingContinuations.add(continuation);
}

// Wait for next update
continuation.suspend(timeoutSecs*1000);
}

return (GpsCoord)continuation.getObject();
}


public void onCoord(GpsCoord gpsCoord) {

synchronized(this) {
for (Continuation continuation : pendingContinuations) {

continuation.setObject(gpsCoord);
continuation.resume();
}

pendingContinuations.clear();
}
}

클라이언트가 Continuation과 함께 getNextPosition()을 호출하면, isPending 메소드는 요청이 이 지점에서 재시도 되지 않는지를 검사하고, 이를 좌표를 대기하는 Continuation의 컬렉션에 추가한다. 그리고 나서, Continuation가 중지된다. 한편, 새로운 좌표가 생성될 때 호출되는 onCoord는 중지된 Continuation를 반복하고, GPS 좌표를 설정하며, 이들을 시작한다. 각각의 재시도 된 요청은 getNextPosition()의 실행을 완료하고, Continuation에서 GpsCoord를 검색한 후 이것을 콜러에게 리턴한다. 여기에서 동기화가 필요한 이유는, pendingContinuations 컬렉션의 일관성 없는 상태로부터 보호하고 새롭게 추가된 Continuation가 이것이 중지되기 전에 시작 되지 않도록 하기 위해서이다.

이 퍼즐의 마지막 조각은 서블릿 코드이다. (Listing 8)


Listing 8. GPSTrackerServlet 구현
                
public class GpsTrackerServlet extends HttpServlet {

private static final int TIMEOUT_SECS = 60;
private ContinuationBasedTracker tracker = new ContinuationBasedTracker();

public void service(HttpServletRequest req, HttpServletResponse res)
throws java.io.IOException {

Continuation c = ContinuationSupport.getContinuation(req,null);
GpsCoord position = tracker.getNextPosition(c, TIMEOUT_SECS);

String json = new Jsonifier().toJson(position);
res.getWriter().print(json);
}
}

여러분도 보듯, 이 서블릿은 아주 작은 일을 수행한다. 요청의 Continuation를 획득하고, getNextPosition()을 호출하며, GPSCoord를 JavaScript Object Notation (JSON)으로 변환한 후, 이를 작성한다. 재실행으로부터 보호해야 할 것이 아무것도 없으므로, isPending()을 체크할 필요가 없다. Listing 9GpsTrackerServlet으로의 호출 결과를 보여준다. 다섯 개의 동시 요청이 있지만 서버에는 단 하나의 쓰레드만 사용할 수 있다.


Listing 9. GPSTrackerServlet의 아웃풋
                
$ for i in 'seq 1 5' ; do lynx -dump localhost:8080/tracker & done
{ coord : { lat : 51.51122, lng : -0.08103112 } }
{ coord : { lat : 51.51122, lng : -0.08103112 } }
{ coord : { lat : 51.51122, lng : -0.08103112 } }
{ coord : { lat : 51.51122, lng : -0.08103112 } }
{ coord : { lat : 51.51122, lng : -0.08103112 } }

이 예제는 극적이지는 않지만, 개념을 명확히 하는데 도움이 된다. 요청이 제출된 후에, 좌표가 생성될 때까지 수초 간 열려있다. 이때 응답이 빠르게 생성된다. 이는 Comet 패턴의 기본이며, Continuations 덕택에 Jetty는 다섯 개의 동시 요청을 하나의 쓰레드에서 처리한다.




위로


Comet 클라이언트 생성하기

Continuations가 Nonblocking 웹 서비스를 만들기 위해 어떻게 사용되는지를 배웠다. 클라이언트 측 코드를 작성하여 이러한 기능을 활용하는 방법이 궁금할 것이다. Comet 클라이언트는,

  1. 응답을 받을 때까지 XMLHttpRequest 연결을 열어둔다.
  2. 그 응답을 적절한 JavaScript 핸들러로 파견한다.
  3. 즉시 새로운 연결을 구축한다.
더 고급의 Comet 설정은 한 개의 연결을 사용하여 여러 다른 서비스에서 브라우저로 데이터를 보낼 수 있다. 클라이언트와 서버 상에 적절한 라우팅 메커니즘을 사용한다. 한 가지 가능성은 Dojo 같은 JavaScript 라이브러리에 대한 클라이언트 측 코드를 작성하는 것인데, 이는 Comet 기반의 요청 메커니즘을 dojo.io.cometd의 형태로 제공한다.

하지만, 서버에서 자바 언어로 작업한다면, 클라이언트와 서버상에서 고급의 Comet 지원을 얻을 수 있는 좋은 방법은 DWR 2(참고자료)를 사용하는 것이다. 아직 DWR에 익숙하지 않다면, 본 시리즈의 Part 3, Ajax와 Direct Web Remoting을 참조하라. DWR은 HTTP-RPC 전송 레이어를 투명하게 제공하면서, 자바 객체를 노출하여 JavaScript 코드에서 웹을 통해 호출한다. DWR은 클라이언트 측 프록시를 생성하고, 데이터를 자동으로 마샬링/언마샬링 하며, 보안을 다루고, 유용한 클라이언트 측 유틸리티 라이브러리를 제공하며, 주요한 모든 브라우저에서 작동한다.




위로


DWR 2: Reverse Ajax

DWR 2에 새롭게 도입된 개념은 Reverse Ajax이 다. 이것은 서버 측 이벤트가 클라이언트로 보내지는 방식이다. 클라이언트 측 DWR 코드는 연결을 구축하고 응답을 파싱하는 것을 투명하게 다루기 때문에, 개발자의 관점에서 볼 때, 이벤트는 서버 측 자바 코드에서 클라이언트로 간단히 퍼블리시 될 수 있다.

DWR은 Reverse Ajax용 세 개의 다른 메커니즘을 사용하도록 설정될 수 있다. 하나는 익숙한 폴링(polling) 방식이다. 두 번째는, piggyback으 로 알려져 있으며, 서버로 어떤 연결도 생성하지 않는다. 대신, 또 다른 DWR 서비스 호출이 발생하고 임박한 이벤트를 요청의 응답으로 피기백(piggyback)할 때까지 기다린다. 이는 매우 효율적이지만, 이벤트에 대한 클라이언트 공지는 클라이언트가 관련이 없는 호출을 만들 때까지 지연된다. 마지막 메커니즘은 오래 존속하는 Comet 스타일의 연결이다. 무엇보다도, DWR은 Jetty 하에서 실행될 때 자동 탐지를 할 수 있으며, Nonblocking Comet용 Continuations를 사용하도록 변환할 수 있다.

필자는 GPS 예제를 DWR 2의 Reverse Ajax를 사용하도록 고쳤다. 이제부터는 Reverse Ajax가 어떻게 작동하는지 자세히 보자.

필 자는 더 이상 필자의 서블릿이 필요 없다. DWR은 클라이언트 요청들을 직접 자바 객체로 중개하는 컨트롤러 서블릿을 제공한다. 또한, Continuations를 다룰 필요가 없다. DWR이 이를 처리하기 때문이다. 따라서, 좌표 업데이트를 클라이언트 브라우저로 퍼블리시 하는 새로운 CoordListener 구현만 필요하다.

ServerContext를 호출하는 인터페이스는 DWR의 Reverse Ajax 매직을 제공한다. ServerContext는 모든 웹 클라이언트들이 동시에 주어진 페이지를 보고 있다는 것을 알고 있고, 서로 통신할 수 있도록 ScriptSession을 제공한다. ScriptSession은 JavaScript 조각을 자바 코드에서 클라이언트로 보내는데 사용된다. Listing 10ReverseAjaxTracker가 좌표 공지에 응답하는 방법과, 이들을 사용하여 클라이언트 측 updateCoordinate() 함수에 대한 호출을 생성하는 방법을 보여주고 있다. DWR ScriptBuffer 객체에 대한 appendData() 호출은 자동으로 자바 객체를 JSON으로 마샬링 한다.


Listing 10. ReverseAjaxTracker의 공지 콜백 메소드
                
public void onCoord(GpsCoord gpsCoord) {

// Generate JavaScript code to call client-side
// function with coord data
ScriptBuffer script = new ScriptBuffer();
script.appendScript("updateCoordinate(")
.appendData(gpsCoord)
.appendScript(");");

// Push script out to clients viewing the page
Collection<ScriptSession> sessions =
sctx.getScriptSessionsByPage(pageUrl);

for (ScriptSession session : sessions) {
session.addScript(script);
}
}

DWR은 ReverseAjaxTracker에 대해 알도록 설정되어야 한다. 큰 애플리케이션에서, DWR의 spring 통합을 활용하여 DWR에 spring에서 생성된 빈을 제공할 수 있다. 하지만, 필자는 DWR이 ReverseAjaxTracker의 새로운 인스턴스를 생성하고, 이것을 application 범위에 배치하도록 할 것이다. 모든 후속 DWR 요청은 이러한 단일 인스턴스에 액세스 할 것이다.

또한 DWR에 GpsCoord 빈에서 JSON으로 데이터를 마샬링 하는 방법을 알려줘야 한다. GpsCoord는 단순한 객체이기 때문에 DWR의 리플렉션 기반 BeanConverter로 충분하다. Listing 11ReverseAjaxTracker용 설정이다.


Listing 11. ReverseAjaxTracker용 DWR 설정
                
<dwr>
<allow>
<create creator="new" javascript="Tracker" scope="application">
<param name="class" value="developerworks.jetty6.gpstracker.ReverseAjaxTracker"/>
</create>

<convert converter="bean" match="developerworks.jetty6.gpstracker.GpsCoord"/>
</allow>
</dwr>

create 엘리먼트의 javascript 애트리뷰트는 DWR이 트래커를 JavaScript 객체로서 노출하기 위해 사용하는 이름을 지정한다. 하지만, 이 경우, 필자의 클라이언트 측 코드는 이를 사용하지 않고, 대신 트래커에서 여기로 데이터를 보낸다. 또한, Reverse Ajax용 DWR을 설정하기 위해서는 web.xml에서의 추가 설정이 필요하다. (Listing 12)


Listing 12. DwrServlet용 web.xml 설정
                
<servlet>
<servlet-name>dwr-invoker</servlet-name>
<servlet-class>
org.directwebremoting.servlet.DwrServlet
</servlet-class>
<init-param>
<param-name>activeReverseAjaxEnabled</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>initApplicationScopeCreatorsAtStartup</param-name>
<param-value>true</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>

첫 번째 서블릿 init-paramactiveReverseAjaxEnabled는 폴링과 Comet 기능을 활성화 한다. 두 번째 initApplicationScopeCreatorsAtStartup은 DWR에게 애플리케이션 시작 시 ReverseAjaxTracker를 초기화 할 것을 명령하고 있다. 이는 빈에 대한 최초의 요청이 이루어질 때 늦은 초기화의 작동을 오버라이드 한다. 이 경우에는 필수적이다. 클라이언트는 ReverseAjaxTracker에 대한 메소드를 호출하지 않기 때문이다.

마지막으로, DWR에서 호출한 클라이언트 측 JavaScript 함수를 구현해야 한다. 콜백 -- updateCoordinate() -- 은 DWR의 BeanConverter에 의해 자동 직렬화 된 GpsCoord 자바 빈의 JSON 표현으로 전달된다. 이 함수는 좌표에서 longitudelatitude 필드를 추출하고 이들을 Document Object Model (DOM) 호출을 통해 리스트에 붙인다. 이는 Listing 13에 필자의 페이지의 onload 함수와 함께 나타나 있다. onload에는 dwr.engine.setActiveReverseAjax(true)에 대한 호출이 포함되는데, 이는 DWR에게 서버로의 영속 연결을 열고 콜백을 기다리도록 명령하고 있다.


Listing 13. Reverse Ajax GPS 트래커의 클라이언트 측 구현
                
window.onload = function() {
dwr.engine.setActiveReverseAjax(true);
}

function updateCoordinate(coord) {
if (coord) {
var li = document.createElement("li");
li.appendChild(document.createTextNode(
coord.longitude + ", " + coord.latitude)
);
document.getElementById("coords").appendChild(li);
}
}

JavaScript 없이 페이지 업데이트 하기
애플리케이션에 JavaScript 코드의 양을 최소화 하려면, JavaScript 콜백을 ScriptSession으로 작성하는 방법이 있다. DWR Util 객체에 ScriptSession 인스턴스를 래핑할 수 있다. 이 클래스는 브라우저 DOM을 직접 조작할 수 있는 자바 메소드를 제공하고, 보이지 않는 곳에서 필요한 스크립트를 자동 생성한다.

이제 필자의 브라우저를 트래커 페이지로 가도록 할 수 있고, DWR은 좌표 데이터가 생성될 때 이를 클라이언트로 보낸다. 이러한 구현은 대게 생성된 좌표의 리스트를 출력한다. (그림 2)


그림 2. ReverseAjaxTracker의 아웃풋
Simple Web page listing generated coordinates

Reverse Ajax를 사용하여 이벤트 중심 Ajax 애플리케이션을 생성하기가 이토록 간단하다. Jetty Continuations의 DWR 덕분에, 어떤 쓰레드도 서버로 연결되지 않으면서, 클라이언트는 새로운 이벤트가 도착하기를 기다린다.

이제는 맵 위젯을 Yahoo!나 Google로 쉽게 통합할 수 있다. 클라이언트 측 콜백을 변경함으로써, 페이지에 직접 붙여지는 대신 맵 API로 전달될 수 있다. 그림 3은 이와 같은 매핑 컴포넌트에서 무작위로 움직이는 the DWR Reverse Ajax GPS 트래커 모습이다.


그림 3. 맵 UI를 가진 ReverseAjaxTracker
Map showing path tracing generated coordinates



위로


결론

Jetty Continuations와 Comet이 결합하여 이벤트 중심 Ajax 애플리케이션에 효율적이고 확장성 있는 솔루션을 어떻게 제공하는지를 보았다. 실제 애플리케이션에서 많은 변수들이 성능에 영향을 미치기 때문에 Continuations의 확장성은 다루지 않았다. 서버 하드웨어, OS의 선택, JVM 구현, Jetty 설정, 웹 애플리케이션의 디자인과 트래픽 프로파일 등이 로딩 중인 Jetty의 Continuations의 성능에 영향을 미친다. 하지만, Webtide의 Greg Wilkins(Jetty 개발자)는 Jetty 6에 대한 백서를 출간했다. 이 글에서 10,000개의 동시 요청을 처리할 때 Continuations를 사용할 때와 사용하지 않을 때 Comet 애플리케이션의 성능을 비교했다. (참고자료) Greg의 테스트에서, Continuations를 사용할 때 레드의 소비와 이에 동반되는 스택 메모리의 소비도 적었다.

DWR 의 Reverse Ajax 기술을 사용하여 이벤트 중심 Ajax 애플리케이션도 쉽게 구현할 수 있었다. DWR을 사용하여 클라이언트 및 서버 측 코딩을 훨씬 줄였을 뿐만 아니라, Reverse Ajax는 코드로부터 전체적인 서버-push 메커니즘을 추상화 했다. DWR의 설정을 바꿔서 Comet, 폴링 또는 피기백 메소드를 자유롭게 전환할 수 있었다. 코드에 어떤 영향도 주지 않고 애플리케이션을 위한 최상의 성능 전략을 자유롭게 시험할 수 있다.

여러분 고유의 Reverse Ajax 애플리케이션으로 시험하고 있다면, DWR 데모의 코드를 다운로드 하여 검토하기 바란다. (DWR 소스 코드 배포판의 일부, 참고자료) 이 글에 사용된 샘플 코드 역시 사용할 수 있다. (다운로드)





위로


다운로드 하십시오

설명이름크기다운로드 방식
예제 코드jetty-dwr-comet-src.tgz8KBHTTP
다운로드 방식에 대한 정보


참고자료

교육

제품 및 기술 얻기
  • Jetty: Jetty 다운로드.

  • DWR: DWR 다운로드.

토론


필자소개


Philip McCarthy는 런던을 중심으로 활동하는, 자바 및 웹 기술 전문 소프트웨어 개발 컨설턴트이다. Orange와 Hewlett Packard Labs을 위한 프로젝트 작업을 수행한 바 있다. 현재는 오픈 소스 프레임웍으로 구현된 웹 기반 금융 시스템 분야에서 일하고 있다.