태그 : java

Spring Framework 비난에 대해

블로그들을 돌아다니다 spring framework에 대해 "비난"하는 포스팅을 봤다.  "뭐 하나 고치려면 파일 뒤지는데 한 세월"이라면서 사이비가 아니냐는 말을 하고 있다.

그 포스팅을 보니 예전에 같이 일했다가 지금은 SI 쪽에서 일하고 있는 팀원이 한 말이 생각났다. 그 친구는 2004년 우리 팀에 spring framework를 도입했을 때 주도적으로 작업했던 친구다. 1.0 버전부터 최근까지 사용했던 친구라 spring framework에 대해선 아주 잘 안다 할 수 있다.  요즘은 spring framework가 꽤나 많이 알려지지 않았나 생각해서 SI 쪽에선 어떤가 물었더니 의외의 대답을 했다.

"JSP 하나로 처리할 수 있는 걸 복잡하게 여러 파일로 나눠놨다며 안쓰려고 해요. 그것 때문에 욕까지 들어먹은 적도 있는데요"

그 포스트의 말대로 "슬슬 사이비이고, 버려야 할 것이라는 움직임"이 SI 쪽에 있긴 한 거 같다.  하지만 정말로 그런 움직임이 있다면 그건 정말 슬픈 일이다.

spring framework는 참 잘 만든 프레임워크다.  나는 JSP + Bean 스타일이나 자작 Front Controller 스타일로 개발하다가 한계에 부딪친 상태에서 뭐가 뭔지 모를만큼 복잡했던 EJB는 쓰기 꺼려지던 때 spring framework를 발견했다.  그 이름처럼 산뜻한 느낌을 주는 framework였고 그때부터 지금까지 사용하면서 항상 즐거움과 신뢰를 느끼고 있다.

그런데 왜 이와 같은 말이 나오는 걸까? SI라면 나는 나쁜 기억 밖에 없다. 몇번 SI 업체에 개발 외주를 준 적이 있었고 다 별로였다. 최근 경우는 가관이었다. 구축 후 그 시스템을 우리 팀이 맡아서 유지 보수할 예정이었기 때문에 개발에 spring framework를 사용할 것을 요구했다. 그런데 그 결과물이라는 걸 검수하며 우리 팀원들은 욕을 무더기로 해댔다.

그것은 spring framework에 대한 최소한의 이해도 없는 상태에서 아니 그것보다 소프트웨어 설계에 대한 기본도 무시한 상태에서 작성한 코드였다.  데이타 접근 계층, 서비스 계층, 웹 처리 계층, 뷰 계층의 구분은 시늉만 냈을 뿐이지 아예 무시되고 뒤섞여 있었고  DI로 처리할 부분을 버젓이 singleton factory를 만들어 lookup하고 있었다.  java collection 객체들을 엉터리로 사용하는가 하면 심지어 java naming convention도 지키지 않고 클래스를 정의하고 있었다.  메서드 시그내쳐를 정의하기 귀찮았는지 Map 객체만을 인자로 받는 메서드들이 허다했고 각 메서드 호출 계층마다 인자로 넘어온 Map 객체를 마구 조작해가며 다음 메서드에 인자로 넘기고 있었다.  정말로 뭐 하나 고치려면 파일들을 찾느라 한 세월인 경우였다.

말하자면 이런 거다. "추상화니 모듈화니 재사용성이니 그런 거 몰라. 어쨌든 돌아가면 되는 거 아냐. 어차피 copy & paste인데 복잡하게 뭐하러 여러 파일로 나눠. 그냥 JSP 하나면 족하지"

글쎄, 누구 말마따나 x 한번 푸짐하게 싸놓고 나올 수 있다면 몰라도 끝까지 자신의 결과물을 책임져야 하는 인하우스 개발팀이라면 위와 같은 생각을 가질 수 없다.  설령 copy & paste로 가능하더라도 그 얼마나 재미없는 일인가?

spring framework를 비판하는데 그게 추상화와 모듈화라는 소프트웨어 개발의 기본을 망각한 가운데 나온 거라면 들어야 할 일고의 가치도 없다.  그렇지만 spring framework 자체의 단점 또는 결함을 비판하는 거라면 정말로 한번 들어보고 싶다. 과연 그런 게 나올지 의문이긴 하지만.

by Corund | 2009/01/08 14:14 | 트랙백 | 덧글(2)

자바에서 한글 처리 문제 - 아직도냐...

자바는 모든 문자열 처리를 유니코드 기반으로 한다. 초창기에 한글 지원이 제대로 구현되어 있지 않은 적도 있다 하나 지금은 한글 지원에 별 문제가 없는 걸로 알고 있다.  폰트나 IME 연동 등의 문제를 갖고 있는 데스크탑 프로그램 개발이라면 모를까 JEE 개발에서 한글 처리는 더 이상 큰 문제가 되지 않는다고 생각한다.

그렇지만 현실은 별로 그런 거 같지 않다. 우연일진 모르겠지만 결제 연동을 비롯한 여러 가지 외부 서비스 연동에서 한글 문제 관련하여 문제가 안 일어났던 적은 거의 없었다. 거의 100% 연동을 위해 제공한 라이브러리에서 한글을 잘못 처리한다.

하도 그런 경우를 많이 봐서 한글이 깨지는 경우가 발생하면 무엇 때문에 그러는지 짐작이 가게 된다.  그리고 라이브러리를 역컴파일해서 보면 거의 짐작했던 대로이다.

자바는 문자열이 유니코드로 저장되기 때문에 메서드 호출이나 문자열 조작 단계에서 결코 한글이 깨지지 않는다.  모든 문제는 문자열을 바이트 배열로, 또는 바이트 배열을 문자열로 변환할 때 발생한다.  이런 경우는 다음과 같은 때이다.
  • 문자열 객체의 getBytes() 메서드로 바이트 배열을 얻을 때
  • new String(byte[])로 바이트 배열에서 문자열 객체를 얻을 때
  • IO(파일과 Socket IO)에서 - 정확히는 Reader/Writer를 이용할 때
  • JDBC로 데이터베이스와 통신할 때
  • 외부 프로그램을 실행할 때 - java.util.Runtime 객체의 exec 메서드를 호출할 때
  • 프로퍼티 파일을 읽을 때 즉 java.util.Properties 객체의 load 메서드를 호출할 때
문자열과 바이트 배열간 변환에서 반드시 문자셋(charset)이 개입된다. 문자셋을 명시적으로 정할 수 있느냐 아니면 묵시적으로 정해져 있느냐의 차이가 있을 뿐 모든 경우에 문자셋에 따라 변환이 일어나게 된다.

따라서 이런 변환이 있을 것으로 예상되는 부분에 문자셋을 지정하는 부분이 없다면 어떤 문자셋이 묵시적으로 지정되는지를 알아봐야 한다.  만약 이것이 실행하는 환경에 따라 바뀐다면(많은 경우 그렇다) 윈도우 데스크탑인 개발 머신에서는 문제 없던 코드가 리눅스 운영 체제인 실 서버에서 한글이 깨지는 등의 문제가 발생되는 경우가 되는 거다.

실제로 한 결제 모듈에서는 대략 다음과 같은 식으로 서버와 통신을 하는 코드가 있었다.

....

private OutputStream out;

public void send(String message, ...) {
    ....
    byte[] buf = message.getBytes();
    out.write(buf);
    ....
}

위 코드의 문제점은 getBytes() 메서드가 문자열을 바이트 배열로 변환할 때 file.encoding 이라고 하는 시스템 프로퍼티에 지정된 문자셋으로 변환한다는 점이다. 보통 file.encoding 프로퍼티는 실행 환경에 따라 결정된다. 한글 윈도우에서는 cp949(또는 ms949)이며 리눅스 머신에서는 LANG 변수를 어떻게 지정하느냐에 따라 달라진다.  콘솔 환경을 선호하는 SE들은 posix 또는 ascii로 지정하기도 하고 en_US.8859_1으로 지정하는 경우도 있다. 한글 환경으로 설정하더라도 ko_KR.euc-kr인 경우도 있고 ko_KR.utf-8인 경우도 있다.

그래서 이렇게 구현된 코드는 사용 환경에 따라 다르게 작동한다.  따라서 한글 윈도우에서는 문제없던 코드가 리눅스 머신에서는 문제를 일으키는 것이다.  여기에 문제를 해결한답시고 다음과 같은 메서드를 쓰면 문제를 더욱 복잡하게 만들게 된다.

public static String toKor(String src) {
    byte[] b = src.getBytes("8859_1");
    return new String(b, "EUC-KR");
}

위와 같은 식의 처치법은 지금도 검색하면 꽤 해결책이라고 나온다. 그렇지만 이런 코드를 이용하여 한 곳에서 제대로 돌아가게 땜빵을 하면 어김없이 다른 곳에서 문제를 일으키게 된다.

위의 경우 자바를 기동할 때 -D 옵션으로 file.encoding 을 지정하는 방법으로 해결할 수 있다.  그렇지만 이런 해결책은 제공하는 라이브러리를 개발할 때 사용해서는 안 된다.  만약 두 개의 서로 다른 라이브러리가 각기 다른 file.encoding 설정을 요구한다면 어떻게 되겠는가.

많은 경우 문자셋은 직접 지정할 수 있다. 따라서 다음과 같은 규칙을 지키는 것이 좋다.
  • String 객체의 getBytes() 를 쓰지 않고 getBytes(String charsetName)을 사용
  • new String(byte[] b) 가 아니라 new String(byte[] b, String charsetName)을 사용
  • 파일 입출력을 할 때에는 FileReader, FileWriter 클래스를 사용하지 말고 FileInputStream, FileOutputStream 으로 파일을 연 후 InputStreamReader, OutputStreamWriter 를 이용하여 charset을 명시하여 Reader/Writer로 변환할 것.
  • JDBC로 데이터베이스를 연결하는 경우 데이터베이스마다 문자셋을 지정하는 경우가 다르므로 관련 매뉴얼을 참조할 것. 그러나 반드시 어떤 방식으로든 문자셋 지정이 포함된다는 것은 잊지 말 것.
  • java.lang.Runtime.exec 의 경우 문자셋을 외부에서 지정할 수 있는 방법이 없음. C로 되어 있는 외부 프로그램인 경우 핵심 기능이 구현된 라이브러리가 있다면 JNI를 통해 호출할 수 있는지 알아보고 그것이 불가능하다면 한글 처리에 신경을 써야 한다(한글을 base64로 인코딩하여 보내고 외부 프로그램에서 이를 디코딩한다던지)
  • java.util.Properties의 경우 프로퍼티 파일은 무조건 iso-8859-1 문자셋으로 읽게 된다. 한글은 native2ascii 유틸리티를 이용하여 변환해야 한다.
  • Servlet이나 JSP의 경우 좀더 복잡하지만 결국 원리는 같다. IO가 일어나는 부분에서 어떤 문자셋이 지정되는지에만 신경을 쓰면 된다.
그렇지만 아직도 많은 코드들이 실행 환경에 따라 한글이 깨지게 구현되어 돌아가고 있다.  그러면서 이런 말을 하기도 한다. 자바의 "Write Once Run AnyWhere"는 거짓말이라고. 하지만 그 정도 수준의 개발자가 그런 말 할 자격은 없지 않을까.

나는 어떻게 해결하고 있을까? 라이브러리를 역컴파일하여 문제점을 찾아서 버그 패치를 해도 반영해 주는 곳을 보지 못했다.  나의 경우 아예 한글을 쓰지 않는 방법으로 문제를 해결하고 있다.

by Corund | 2008/11/26 16:24 | 트랙백 | 덧글(0)

자바 서블릿 컨테이너의 Comet 지원 3 - Resin

이전글

Resin 은 버전 3.1에서 Comet 모델을 지원한다.  Tomcat 과 마찬가지로 javax.servlet.Servlet 을 확장한 인터페이스를 제공하는 방식이다.  com.caucho.servlet.comet.CometServlet 이 그것인데 이 인터페이스에는 다음과 같은 메서드들이 있다.

public interface CometServlet extends Servlet {
  boolean service(ServletRequest request, ServletResponse response,
                            CometController controller) throws ServletException, IOException;
  boolean resume(ServletRequest request, ServletResponse response,
                             CometController controller) throws ServletException, IOException;
}


Tomcat 과 마찬가지 방식으로 이 인터페이스를 구현한 서블릿은 요청을 처리할 때 기존 서블릿의 service 메서드를 호출하지 않고 이 인터페이스의 service 메서드를 호출한다.  service 메서드의 호출 결과가 true 가 되면 요청 처리는 suspend 되고 스레드는 풀로 되돌아간다.  CometController 의 wake 메서드를 호출하면 요청 처리는 재개되어 resume 메서드가 실행된다.  resume 메서드가 true 를 반환하면 또 다시 suspend 되어 wake 를 기다리게 되고 false 를 반환하면 요청 처리가 끝나게 된다.

service, resume 메서드에 인자로 들어오는 CometController 객체는 service, resume 그리고 다른 이벤트 대기 스레드 사이에서 공유하여 신호를 보낼 수 있고(wake 메서드) 서로간 데이타 교환을 할 수도 있다(setAttribute, getAttribute).

Jetty 나 Tomcat 과는 어떻게 다른지 같은 채팅 예제로 실제 사용법을 알아보겠다. ChatServlet 과 chat.jsp 는 Jetty 의 경우와 같고 BroadcasterServlet 은 다음과 같다.

BroadcasterServlet.java
...
@Override
public boolean service(ServletRequest request, ServletResponse response,
                            CometController controller) throws ServletException, IOException {
    HttpServletResponse res = (HttpServletResponse) response;
    res.setContentType("text/html; charset=utf-8");
    messageSender.addSession(controller);
    return true;
}

@Override
public boolean resume(ServletRequest request, ServletResponse response,
                             CometController controller) throws ServletException, IOException {
    messageSender.removeSession(controller);
    String message = (String) controller.getAttribute("message");
    HttpServletResponse res = (HttpServletResponse) response;
    PrintWriter out = res.getWriter();
    out.println(message);
    res.flushBuffer();
    return false;
}
...


service 메서드에서 최초 요청 처리를 처리하면서 인자로 넘어온 CometController 객체를 messageSender 객체에 저장하여 이벤트 처리 스레드로 넘긴다.  이후 CometController 의 wake 메서드가 호출되면 resume 이 실행되고 여기서 실제 데이타를 전송한다.  전송할 데이타는 이벤트 처리 스레드에서 CometController.setAttribute 으로 넘기고 여기서는 getAttribute 로 받는다.

다음은 MessageSender 클래스이다.

MessageSender.java
public class MessageSender implements Runnable {
  private final BlockingQueue<String> messages =
                         new LinkedBlockingQueue<String>();
  private final Set<CometController> sessions =
                         new CopyOnWriteArraySet<CometController>();

  private volatile boolean running = true;

  public void stop() {
    this.running = false;
  }

  public void sendMessage(String message) {
    try {
      messages.put(message);
    } catch (InterruptedException ignore) {
      // ignore
    }
  }

  public void addSession(CometController controller) {
    sessions.add(controller);
  }

  public void removeSession(CometController controller) {
    sessions.remove(controller);
  }

  @Override
  public void run() {
    while (running) {
      String message = null;
      try {
        message = messages.take();
      } catch (InterruptedException ignore) {
        // ignore
      }
      for (CometController controller : controllers) {
        controller.setAttribute("message", message);
        controller.wake();
      }
    }
  }
}


MessageSender 는 Jetty 의 경우와 비슷하다.  CometController 들을 sessions 에 저장해 두고 채팅 메시지가 들어오면 이를 CometController 에 저장(setAttribute) 한 후 wake 를 호출해서 요청 처리가 resume 되게 한다.

Resin 의 Comet 지원은 Jetty 와 비교하면 직관적으로 이해하기 쉽다.  또한 Tomcat 처럼 번거롭지도 않다.  세 서블릿 컨테이너의 Comet 지원 중에서 가장 사용하기가 쉬운 것 같다.

by Corund | 2008/08/01 09:35 | 트랙백 | 덧글(0)

자바 서블릿 컨테이너의 Comet 지원 2 - Tomcat

이전글

Tomcat 에서는 버전 6.0의 Advanced IO 지원을 통해 Comet 모델을 효과적으로 구현할 수 있게 되었다. Tomcat 6.0 에서는 Jetty 와는 달리 서블릿을 확장하는 형태로 지원하고 있다.

javax.servlet.Servlet 인터페이스를 확장한 org.apache.catalina.CometProcessor 가 그것인데 이 인터페이스는 Servlet 인터페이스를 상속한 것이다. 이 인터페이스에는 event(CometEvent) 메서드가 있는데 이 인터페이스를 구현한 클래스가 서블릿으로 등록되어 있으면 Tomcat 은 요청을 처리할 때 기존 서블릿의 service(...) 메서드를 호출하지 않고 이 event(...) 메서드를 호출하게 된다. 이 메서드는 한 요청 처리 트랜잭션에서 여러번 호출될 수 있으며 이벤트 정보가 인자로 넘어오게 된다. 이 인자로 넘어온 객체를 통해 여러 정보를 알 수 있고 IO도 처리할 수 있다. 입력(input)은 이 이벤트 처리 메서드 안에서 이루어져야 하지만, 출력(output)은 이 이벤트 처리 메서드 밖에서도 처리할 수 있다.

CometProcessor 를 사용하기 위해서는 Tomcat 설정을 조금 손봐야 한다. CometProcessor 는 Tomcat connector 가 Native 또는 NIO connector 여야 한다. 자바 1.4 버전 이상이라면 NIO 를 쓰면 되며 Tomcat 설정 파일을 다음과 같이 수정하면 된다.

<!-- 기존 설정. 아래와 같이 바꾼다.
<connector connectiontimeout="20000" port="8080" protocol="HTTP/1.1" redirectport="8443"/>
-->
<connector connectiontimeout="20000" port="8000">
     protocol="org.apache.coyote.http11.Http11NioProtocol" useComet="true" redirectPort="8443"/>


event 메서드는 BEGIN, READ, END, ERROR 이벤트가 발생할 때 호출되는데 이때 인자로 org.apache.catalina.CometEvent 객체가 넘어온다. 이 객체에는 위의 Event Type 을 얻는 메서드와 HttpServletRequest, HttpServletResponse 객체를 얻는 메서드 등이 있다.

이전 Jetty 의 경우와 마찬가지로 채팅 예제로 실제 사용법을 알아본다. 먼저 ChatServlet 과 chat.jsp 는 이전 jetty 의 경우와 같다. BroadcasterServlet 은 CometProcessor 를 사용해야 하는데 코드는 다음과 같다.

public class BroadcasterServlet extends HttpServlet implements CometProcessor {
...
  @Override
  public void event(CometEvent event) throws ServletException, IOException {
    HttpServletRequest request = event.getHttpServletRequest();
    HttpServletResponse response = event.getHttpServletResponse();
    String sessionId = request.getSessionId();

    if (CometEvent.EventType.BEGIN == event.getEventType()) {
      // 요청을 최초로 처리할 때 호출됨.
      response.setContentType("text/html; charset=utf-8");
      messageSender.addSession(sessionId, event);
    } else if (CometEvent.EventType.ERROR == event.getEventType()) {
      // IO 에러가 발생했을 때.
      messageSender.removeSession(sessionId);
      event.close(); // 요청 처리 완료.
    } else if (CometEvent.EventType.END == event.getEventType()) {
      // 요청 처리가 완료되었을 때
      log("End event");
    } else if (CometEvent.EventType.READ == event.getEventType()) {
      log("Read event");
    }
  }
...
}


요청을 최초로 처리할 때 BEGIN 이벤트가 발생하며 이때 Request 정보를 읽는 작업을 한다. 여기서는 채팅 메시지가 있을 때 메시지를 보낼 수 있도록 event 객체를 messageSender 객체에 저장해 놓는 작업도 한다.

요청에서 읽을 데이타가 준비되었을 때 (즉 request.getInputStream() 등) READ 이벤트가 발생하며, 에러가 발생했을 때 ERROR 이벤트가 요청 처리가 완료되면 END 이벤트가 발생한다. 여기서는 ERROR 이벤트가 발생하였을 때 관련 정보를 messageSender 에서 삭제하고 요청을 마치는 작업을 한다.

MessageSender 클래스는 다음과 같다. Jetty 의 경우와 조금 다른데 메시지를 실제로 보내는 작업도 여기서 처리하고 있다. CometProcessor 에 원하는 때 이벤트를 발생시킬 수 없기 때문이다. 따라서 이벤트가 발생할 때 출력을 하기 위해 미리 CometEvent 객체를 저장하여 HttpServletResponse 객체를 얻어 출력을 한다.

MessageSender.java
public class MessageSender implements Runnable {
  private volatile boolean running = true;
  private final BlockingQueue<String> messages =
                                          new LinkedBlockingQueue<String>();
  private final Map<String, CometEvent> sessions =
                                          new ConcurrentHashMap<String, CometEvent>();
  private final ExecutorService executor = Executors.newFixedThreadPool(5);

  public void send(String message) {
    try {
      messages.put(message);
    } catch (InterruptedException ignore) {
      // ignore
    }
  }

  public void addSession(String id, CometEvent event) {
    sessions.put(id, event);
  }

  public void removeSession(String id) {
    sessions.remove(id);
  }

  public void stop() {
    this.running = false;
    this.executor.shutdown();
  }

  @Override
  public void run() {
    while (running) {
      String message = null;
      try {
        message = messages.take();
      } catch (InterruptedException ignore) {
        // ignore
      }
      for (String id : sessions.keySet()) {
        executor.submit(new Task(id, message));
      }
    }
  }

  private class Task implements Runnable {
    private String sessionId;
    private String message;

    public Task(String id, String msg) {
      sessionId = id;
      message = msg;
    }

    public void run() {
      CometEvent event = sessions.get(sessionId);
      if (null == event) {
        return;
      }
      HttpServletResponse response = event.getHttpServletResponse();
      PrintWriter out = null;
      try {
        out = response.getWriter();
        out.println(message);
        out.flush();
        response.flushBuffer();
      } catch (IOException naive) {
        naive.printStackTrace();
      } finally {
        try { out.close(); } catch (Exception ignore) {}
        try { event.close(); } catch (Exception ignore) {}
        sessions.remove(sessionId);
      }
    }
  }
}


Jetty 의 경우와 달리 Tomcat 은 Servlet 인터페이스를 확장하여 웹 요청 단계에 따라 이벤트를 발생시키는 방식으로 작동한다. 네트워크 서버가 많이 취하고 있는 방식이라 쉽게 이해할 수 있다. 그러나 실제 프로그래밍을 하려면 번거로운 점이 있다. Jetty 처럼 원하는 때 resume 을 시킬 수 없기 때문에 long polling 처럼 원하는 때 출력을 하려면 서블릿 코드 밖에서 출력을 해야 하며 따라서 직접 스레드를 관리할 필요가 생긴다. 또한 Error 시에 적절히 자원을 정리하는 코드도 필요하다.

정리하자면 Tomcat 의 Comet 지원은 일반 네트워크 서버 스타일에 가깝다고 볼 수 있다. 좀더 웹 프로그래밍 스타일에 가깝게 만들었으면 좋지 않았을까 하는 생각이 든다.

by Corund | 2008/07/30 19:31 | 트랙백(1) | 핑백(1) | 덧글(1)

자바 서블릿 컨테이너의 Comet 지원 1 - Jetty

이전글: Comet 에 대하여

Jetty 는 버전 6.0에서부터 Continuation 이라는 것을 도입해서 Comet 스타일의 웹 프로그래밍을 효과적으로 구현할 수 있게 되었다.  jetty의 Continuation 을 이용하면 웹 요청을 처리를 중지(suspend)시켜서 대기 상태로 만들었다가 후에 필요할 때 다시 재개(resume)시킬 수 있다.  웹 요청 처리를 중지(suspend)시키면 요청을 처리하던 스레드는 다시 스레드 풀(Thread Pool)로 되돌아가 다른 요청을 처리할 수 있게 된다.  이로서 장시간 유지되는 HTTP 연결을 더 적은 수의 스레드로 효과적으로 처리할 수 있게 된다.

Continuation 은 scheme 등의 언어에서 지원하던 개념인데, jetty 에서는 이와 비슷하긴 하지만 꼭 같지는 않다.  요청 처리를 중지(suspend)시킨 후 재개(resume)하면 바로 중지시킨 그 지점에서 다시 진행하는 것이 아니라 요청 처리 체인(FilterChain)을 다시 처음부터 진행한다. 이것 때문에 프로그래밍에 약간 신경써야 할 것들이 있다. 

실제 Jetty 의 Contiuation 을 이용하여 채팅을 구현한 예로 설명을 진행하겠다.

먼저 채팅 페이지를 보여주고 채팅 메시지를 받는 서블릿이다.

ChatServlet.java
...
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ... {
  request.getRequestDispatcher("/WEB-INF/jsp/chat.jsp").forward(request, response);
}

public void doPost(HttpServletRequest request, HttpServletResponse response) throws ... {
  String name = request.getParameter("name");
  String text = request.getParameter("text");

  String message = "<b>" + name + "</b>: " + text + "<br>";
  messageSender.sendMessage(message);

  response.setContentType("text/html");
  response.getWriter().println("OK");
}
...


채팅 페이지는 다음과 같다.

chat.jsp
...
<script type="text/javascript">
    function pollMessage() {
        new Ajax.Updater({ success: 'panel' }, '/broadcaster', {
            method: 'get',
            insertion: 'bottom',
            onComplete: pollMessage
        });
    }
    function sendMessage() {
        new Ajax.Request('/chat', {
            method: 'post',
            parameters: { nick: $F(name'), text: $F('text') },
        });
        $('text').clear();
        return false;
    }
</script>
</head>
<body onload="pollMessage()">
<h1>채팅 페이지</h1>
<div id="panel"></div>
<form action="" method="POST" onsubmit="return sendMessage()">
    Nick <input type="text" name="name" id="name" size="10">
    <input type="text" name="text" id="text" size="40">
    <input type="submit" value="보내기">
</form>
...


ajax 를 이용해서 채팅 메시지를 ChatServlet 으로 보낸다.  그리고 BroadcasterServlet 에 long polling 으로 접속해서 메시지를 받아 이를 패널에 출력한다.  pollMessage 함수가 Comet long polling 을 하는 코드이다.

ChatServlet 은 chat 메시지를 받으면 이를 messageSender 객체로 보낸다. messageSender 객체는 MessageSender 클래스의 인스턴스로 미리 ServletContext 에 저장해 놓고 최초 context 기동시에 기동시킨다.

다음은 Long Polling 으로 접속된 클라이언트에 채팅 메시지를 뿌려주는 BroadcasterServlet 이다. 이부분에 jetty의 Continuation 이 사용되었다.

BroadcasterServlet.java
...
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ... {
    Continuation continuation = ContinuationSupport.getContinuation(request, null);
    if (! continuation.isPending()) {
        // new session start
        response.setContentType("text/html; charset=utf-8");
        messageSender.addSession(continuation);
    }
    continuation.suspend(0); // request suspend
    // resumed
    String message = (String) continuation.getObject();
    PrintWriter out = response.getWriter();
    out.println(message);
    out.flush();
    response.flushBuffer();
}
...


여기서 messageSender 객체는 위의 ChatServlet 의 messageSender 객체와 같은 객체이다.

최초로 요청이 처리될 때에는 continuation.isPending() 메서드가 false 가 된다.  그리고 Contiunation 이 suspend 되었다가 resume 되어 다시 요청 처리 체인이 진행될 때에는 isPending() 메서드가 true 가 된다(다시 suspend 메서드가 호출될 때까지).  이를 이용하여 suspend 호출 이후 부분만을 다시 실행시키는 것처럼 할 수 있다.

continuation.suspend() 가 호출되면 이 요청 처리는 여기서 끝나게 된다.  그렇지만 HTTP 커넥션은 그대로 유지되고 있으며 request, response 객체도 여전히 유효한 상태이다.  이후 continuation.resume() 이 호출되면 이 요청 처리 체인은 다시 실행된다(resume 메서드는 messageSender 에서 호출한다).  이 때 continuation 객체는 이전과 같은 객체이며, 다시 suspend() 호출을 만날 때까지 isPending() 메서드는 true 가 된다(반면 최초 suspend() 호출 이전에 isPending() 메서드는 false 이다). 이를 이용하여 suspend() 이후를 진행시키는 것과 같은 효과를 낼 수 있는 것이다.

MessageSender 클래스는 다음과 같다.

MessageSender.java
public class MessageSender implements Runnable {
  private volatile boolean running = true;
  private final BlockingQueue<String> messages =
                             new LinkedBlockingQueue<String>();
  private final Set<Continuation> sessions =
                             new CopyOnWriteArraySet<Continuation>();

  public void stop() {
    this.running = false;
  }

  public void addSession(Continuation cont) {
    sessions.add(cont);
  }

  public void sendMessage(String message) {
    try {
      messages.put(message);
    } catch (InterruptedException ignore) {
      // 종료 처리를 해야 하지만 생략
    }
  }

  public void run() {
    while (running) {
      String message = null;
      try {
        message = messages.take();
      } catch (InterruptedException ignore) {
        // 종료 처리를 해야 하지만 생략
      }
      for (Continuation continuation : sessions) {
        continuation.setObject(message);
        continuation.resume();
        sessions.remove(continuation);
      }
    }
  }
}


이 클래스는 ServletContextListener 에서 최초 context 가 기동될 때 인스턴스를 생성하여 새 스레드로 시작한다.

ChatServlet 에서 sendMessage 를 호출하면 메시지가 Queue 에 쌓이게 되고 이 메시지는 메시지 처리 스레드에서 받아 continuation 객체에 저장한 다음 continuation 을 resume 한다.  그러면 BroadcasterServlet 에서 다시 실행이 되는 것이다.  continuation suspend() 호출과 resume 호출 그리고 resume 후의 진행은 모두 다른 스레드에서 진행된다.  따라서 이 사이의 데이타 교환은 Continuation 객체의 setObject 와 getObject 메서드를 통해서만 해야 한다.

Jetty 의 Continuation 방식의 Comet 지원은 기존 Servlet API 가 크게 바뀌지 않아도 된다는 장점이 있다.  그래서인지 Servlet 3.0 의 Comet 지원도 이와 비슷한 방식 - ServletRequest 를 suspend, resume 하는 방식 - 으로 논의되고 있는 것 같다.  그렇지만 약간은 억지같은 Continuation 방식으로 최초 suspend 시 이곳에서 처리가 끝난다는 점이 직관적이지 못하며 resume 을 위해 isPending 과 같은 메서드로 체크를 해야 한다는 번거로움이 있다.

Tomcat 6.0, Resin 3.1 의 방식은 훨씬 이해하기 쉬운 방식인데 이는 다음 번에 다루겠다.

by Corund | 2008/07/30 16:52 | 트랙백(1) | 핑백(2) | 덧글(0)

◀ 이전 페이지다음 페이지 ▶