개똥이도 하는 Comet

얼마 전에 Comet 기술을 사용해서 채팅방을 하나 만들었습니다. 원래는 NodeJS 라는 새로운 자바스크립트 인터프리터에 대해 소개하기 위한 예제로 작성했는데, 오히려 Comet에 대해서 관심을 보이는 분들이 많았습니다. 자바스크립트만으로도 빠르게 실시간으로 반응하는 채팅방이 가능하다는 사실에 놀라워하더군요. 그 분들께 Comet 이라고 말씀드리면 이름 자체를 처음 들어보거나 들어보기는 했지만 잘 모르는 분들이 대부분이었습니다. 그게 바로 이 연재를 시작한 이유입니다.

이 연재를 통해 Comet에 대해 알아보고, 간단하게 채팅 서버와 스크립트를 만들어 보겠습니다. 그 후, HTML5에 새롭게 추가된 WebSocket에 대해 배워보고 Comet으로 작성한 채팅 서비스를 WebSocket을 사용하도록 변환해 볼 생각입니다. 여기까지 하면 Comet이나 WebSocket에 대해서 충분히 이해하게 되지 않을까 싶습니다.

우선 이 글에서는 Comet의 개념에 대해 설명해볼까 합니다.

웹의 한계

필요는 발명의 어머니입니다. Comet 기술은 웹이 가지는 한계가 사람들의 욕구를 충족시키지 못했기 때문입니다. 바로 실시간(real-time)에 대한 욕구죠. HTTP는 원래 프로토콜 특성상 실시간을 위해 필수적인 지속되는 연결(persistent connection)을 가질 수 없습니다. 클라이언트에서 서버에 접속하면 서버가 응답하고 연결이 끊어집니다. 우리가 웹을 사용할 때는 이 동작을 수없이 반복할 뿐입니다. 효율을 위해서 선택한 방식이기는 했지만, 이것이 웹이 가지는 한계가 되기도 했습니다. HTTP가 만들어지던 당시에는 그래도 충분했었습니다. 웹의 목적은 정보의 저장과 전달이었을 뿐, 실시간일 필요는 없었기 때문입니다. 웹이 생활과 밀접하게 되면서 사람들이 더 많은 것을 바라기 전까지는 그걸로도 충분했으나 이제는 상황이 바뀌었습니다.

이쯤에서 누군가는 몇 년전쯤에 크게 주목받은 Ajax를 떠올릴 것입니다. 어쩌면 이렇게 생각할지도 모르겠습니다.

“네이버의 실시간 급상승 검색어를 보면 실시간으로 데이터를 가져오던데?”

안타깝지만 현재 웹에서 운용되는 실시간 서비스들은 대부분 실시간이 아닙니다. 대부분의 실시간 서비스들이 그렇듯 네이버의 실시간 급상승 검색어의 경우도 바로바로 데이터를 갱신하는 것이 아니라 일정한 간격을 두고 데이터를 다시 불러들이는 형태입니다. 페이지 전체를 리프레시하거나 이동해야만 변경된 데이터를 볼 수 있었던 예전에 비하면야 실시간에 가깝지만, 그렇다고 실시간이라고 부를 수 있는 것은 아닙니다. 10초 단위 혹은 30초 단위 실시간이라고 말할 수는 있을지 몰라도 실시간이라고 부르기에는 부족합니다. 사실, x초 단위 실시간이 모순적인 단어이긴 합니다.

Comet이란?

본격적으로 Comet에 대해 설명하기 전에 이 용어에 대해서 간단하게 짚고 넘어가겠습니다. Comet은 새로 개발된 혁신적인 기술을 의미하지 않습니다. Ajax와 마찬가지로 이전부터 사용되던 방법을 지칭하기 위한 통일된 용어가 필요했고 그 용어를 Comet이라 명했을 뿐입니다. 그 이전에는 이와 비슷한 방식을 두고 Reverse Ajax라고도 불렀습니다. 다른 점이 있다면 Comet은 유명한 자바스크립트 프레임웍 중 하나인 Dojo Toolkit을 개발한 알렉스 러셀(Alex Russell)이 2006년 3월에 자신의 블로그에서 이 용어를 처음 사용했다는 점이고, Reverse Ajax는 DWR이라는 자바 라이브러리의 2006년 4월경 출시된 2.0 버전에서 처음 사용됐다는 정도일 듯 합니다. 실제로는 Reverse Ajax보다 Comet이 좀 더 포괄적인 개념이라 통틀어서 Comet을 많이 사용하는 편입니다. 예를 들어, Reverse Ajax에는 iframe을 사용한 오래 끌기(long-polling) 방식이 포함되어있지 않지만, Comet은 이를 포함합니다. 참고로, Comet에 관심있다면 봤을 법한 아래 그림도 알렉스 러셀이 자신의 블로그에 올린 그림입니다. 그 외에도 Comet을 가리키는 Ajax-push, Two-way web, HTTP Streaming 등의 여러가지 이름이 있습니다.

Comet의 핵심은 위 그림에 모두 있지만, 개인적인 경험으로는 위의 그림을 아무리 봐도 이해하기가 쉽지 않았습니다(물론 제 이해력의 문제일 수 있습니다 ^^;;). 그래서, 쉽게 말로 풀어써보고자 이  글을 쓰게 된 것이지만요.

Comet 클라이언트

결론부터 말하자면 Comet에서 가장  중요한 기술은  서버와 지속적으로 연결할 수 있는 방법입니다. 그리고, Comet은 ‘지속적인 연결’이라는 목적을 이루기 위해 여러가지 방법을 사용합니다.

forever <iframe>

서버가 클라이언트의 요청에 응답할 때, Content-Length 헤더를 출력하지 않으면 클라이언트는 서버측으로부터 접속이 종료될 때까지 계속 데이터를 받아들입니다. 이러한 특성을 이용하여 서버측에서 접속을 끊지 않고 계속 유지한다면 숨겨진 <iframe>에서 아주 긴 시간 동안 서버 푸시(server-push)가 가능해집니다. 접속을 유지하는 동안 서버는 이벤트가 발생할 때 <script> 태그를 출력해서 <iframe>문서의 부모 문서에 데이터를 전달합니다.

<script>parent.exec_callback({...데이터...});</script>

이 방식의 장점은 폭넓은 호환성에 있습니다. 어지간히 오래된 브라우저에서도 <iframe>태그는 지원해주니까요. 다만, 에러를 제대로 다룰 수 없는 점, HTTP의 상태를 다룰 수 없는 점, 모바일 브라우저에서는 사용할 수 없는 점 등은 단점입니다. 또한, 웹 브라우저의 기본 보안 정책인 동일 출처 정책(Same-Origin Policy)의 영향을 받으므로 도메인이나 스키마, 포트가 다른 문서간에는 사용할 수 없습니다.

대부분의 브라우저에서는 다음과 같이 <iframe>을 생성하고 추가할 수 있습니다. Safari 브라우저에서는 display 스타일이 none으로 설정된 iframe을 폼 전송의 target으로 설정할 수 없으므로, 크기를 매우 작게 만들어 안보이는 영역에 두는 방식을 사용하는 것이 좋습니다.

var iframe = document.getElementById(iframe_id);
if (!iframe) {
    iframe = document.creatElement('iframe');
    iframe.id = iframe_id;
    document.body.appendChild(iframe);
}
iframe.style.position = 'absolute';
iframe.style.top = '-100px';
iframe.style.left = '-100px';
iframe.style.width = '1px';
iframe.style.height = '1px';
iframe.src = 'http://server/push.cgi';

IE에서는 <iframe>을 문서에 직접 추가하는 대신 ActiveX를 사용해서 깔끔하게 처리하는 방법도 있습니다. 아래는 Alex씨가 작성한 글에서 가져온 코드입니다.

// we were served from child.example.com but
// have already set document.domain to example.com
var currentDomain = "http://exmaple.com/";
var dataStreamUrl = currentDomain+"path/to/server.cgi";
var transferDoc = new ActiveXObject("htmlfile"); // !?!

// make sure it’s really scriptable
transferDoc.open();
transferDoc.write("<html>");
transferDoc.write("<script>document.domain='"+currentDomain+"';</script>");
transferDoc.write("</html>");
transferDoc.close();

// set the iframe up to call the server for data
var ifrDiv = transferDoc.createElement("div");
transferDoc.appendChild(ifrDiv);
// start communicating
ifrDiv.innerHTML = '<iframe src="'+dataStreamUrl+'"></iframe>';

<script> long-polling

두번째 방법은 <script> 태그를 사용한 오래 끌기(long-polling) 방식입니다. <script> 태그의 src 속성에 서버측에서 응답할 주소를 입력하고, 서버측에서는 응답할 내용이 있을 때까지 이 접속을 계속 유지합니다.

var script = document.createElement('script');
script.src = 'http://domain.com/path/to/server.cgi?callback=callback_function_name';

접속을 유지하다가 응답할 내용이 발생하면 서버측에서는 이름을 전달받은 콜백 함수에 데이터를 전송합니다. 콜백 함수에 전달하는 데이터의 형태는 서버와 클라이언트가 하나의 규약에 따르기만 하면 뭐든 상관없지만, JSON 데이터를 사용하는 것이 일반적이며, 확장성 측면에서도 더 유리합니다. 참고로, 이와 같이 JSON 데이터를 콜백 함수로 감싸서 응답하는 방식을 JSONP 방식이라고 합니다.

callback_function_name(  {some:data, in_any:format, you:want}  );

콜백 함수가 실행되면 전달받은 데이터를 처리함과 동시에 방금 끊어진 접속을 대체할 수 있는 새로운 <script> 태그를 생성합니다. 이런 식으로 계속 대기 + 계속 생성하는 것이 바로 <script> long-polling 방식의 핵심입니다.

이 방식은 여러 브라우저에서 사용할 수도 있으면서 포트, 도메인 등의 제약도 받지 않는 장점이 있는 반면, 에러나 HTTP의 상태를 다룰 수 없고, iframe 처럼 스트리밍 방식으로 사용할 수 없다는 단점이 있습니다. 스트리밍으로 사용할 수 없는 이유는 <script> 태그의 특성 때문입니다. <script> 태그는 src에서 지정한 리소스를 모두 읽어온 후에 해석하고 실행하기 때문에, 서버측에서 데이터를 반환했다면 반드시 접속을 종료해야 합니다. 그래야, 콜백 함수가 실행되니까요.

“응답을 계~속 기다리다가, 데이터를 받고 접속이 끊어지면 즉시 새 접속을 만든다”

이 과정을 계속 반복하는 것이 long-polling 방법입니다.

XHR long-polling

앞서 언급한 <script> long-polling과 유사하지만, 전달 매체를 <script> 가 아닌 XHR로 한다는 점에서 다릅니다. 보안 제약은 더 받지만, 에러와 HTTP 상태를 더 정교하게 다룰 수 있다는 장점이 있습니다.

Comet 서버

사실 Comet을 구현하기 위해 특별한 서버가 필요한 것은 아닙니다. 사용하는 웹 서버를 그대로 사용해도 Comet을 구현할 수 있습니다. 문제는 Comet의 특성상 접속을 오랫동안 유지해야 한다는 것인데, HTTP라는 프로토콜은 이런 목적으로 만들어진 것이 아니라서 서버측에도 부담이 심합니다.

그래서, Comet 서버는 비동기 접속을 사용하는 경우가 많습니다. 어느 정도 규모있는 서비스를 생각한다면 거의 필수라고 할 수 있지요. 그렇다고 해서 Comet 이라는 방법이 ‘비동기 소켓을 사용하는 서버’를 반드시 필요로 하는 것은 아닙니다. ‘있으면 좋은 것’일 뿐입니다.

1, 2년쯤 전에 국내에서 다섯손가락 안에 드는 대형 서비스를 운영하고 있는 모기업에 계신 개발자에게 Comet을 소개받은 적이 있습니다. 그 전에는 대충 이론으로만 알고 있었는데, 실제 사례를 들어 설명해주셔서 꽤 재미있게 들었었죠. 그 때 들은 얘기로 자신들이 구현한 채팅 서비스가 부하가 너무 심했는데, 비동기 서버로 바꾼 이후로는 동접 500명도 힘들어하던 서버가 3,000명도 거뜬히 견디게 되었다고 하더군요. 저도 실제로 그 정도 부하를 겪어볼 일은 많지 않아서 측정하기 어려웠는데, 그 분 말씀이 꽤 좋은 경험이 됐습니다.

Comet 전용 서버들은 대부분 이러한 비동기 접속을 구현하고 있습니다. Comet 전용 서버라고 해서 특별한 기능이 있는 것은 아니고, 조금 더 다듬어진 프레임웍과 Comet을 위한 간편한 설정, 사용법 등을 제공합니다. 참고로, 최근에는 일반적인 웹 서버들도 성능을 위해 epoll 이나 kqueue 등을 사용한 비동기 접속을 제공하고 있습니다.

다음 글에서는 실제로 채팅 서비스를 작성해보면서 Comet을 배워보도록 하겠습니다.

  1. comet 관련 좋은 글 감사합니다.

    톰켓 7 서블릿3.0에서 서버푸쉬 방식으로 지원한다던데 이 방식이 괜찮타고 하던데 혹시 강의할 생각은 없으신가요?

    1. 톰캣에서 지원한다는 서버 푸시 방식에 대해 아는 바가 없어서 강의도 힘들지 싶습니다. ^^;;
      남들에게 설명할만큼 잘 알게 된다면 고려해보도록 하겠습니다.

  2. 좋은정보 감사합니다.
    웹에서 이런것들이 가능했군요…
    이제서야 알게된 제가 한심합니다….ㅠ.ㅠ

  3. 가끔 보면 웹에 달린 채팅창이 있어
    구현 방법이 궁금했었습니다.

    이런 방법으로 구현을 하는군요.
    소중한 정보 공유 감사합니다. ^^

Leave a Reply