본문 바로가기

.Net Technology/Comet

(1) 코멧의 소개와 활용전략

월간 마소에 2010년 6월호에 기고한 원고입니다. 

연재순서
1회 | 2010. 06 | 코멧의 소개와 활용전략
2회 | 2010. 07 | 닷넷을 활용한 코멧 구현 전략
3회 | 2010. 08 | 코멧을 이용한 웹 채팅 만들기


정말 짧은 시간 속에서 웹의 트랜드는 끊임없이 변해가고 있으며 웹의 기술들 또한 굉장히 다양하게 시도되고 있다. 이렇게 사용자의 경험을 최적화 시키기 위한 여러 움직임들 속에서 여기서 소개할 코멧이라는 기술 또한 필수적인 웹 기술로 자리잡을 것이 분명하다. 이번 회에서는 코멧이 무엇이며 어떻게 활용할 수 있는지에 대해서 살펴보도록 하겠다. 


코멧이란? 

AJAX라는 기술이 IT계에서 새로운 트렌드로 한참 이슈가 되던 5년 전을 떠올려 본다. AJAX라는 용어가 정의되기 전까지 분명 소수의 개발자들은 이 기술을 활용하고 있었다. 필자 또한 AJAX 라는 기술 용어가 정의되기 전에 “깜빡임 없이 웹 서버 갔다 오기” 라는 강좌를 작성하고는 했었다. 그렇기 때문에 어떠한 기술의 용어를 창안하고 정의한다는 것은 그 기술을 대중화시키는데 있어서 상당히 중요한 일이 아닐 수 없다. 이번에 소개할 코멧(CEOMET)이라는 기술 또한 알렉스 러셀(Alex Russell)이라는 사람으로부터 정의되었으며 이 기술 또한 Ajax 기술처럼 오래 전부터 사용되던 기술일 수도 있다. 코멧이라는 용어가 정의되기 전에 이 기술을 “리버스 AJAX” 혹은 “서버 푸쉬” 정도로 표현해왔고 지금도 여전히 이 용어를 많이 사용하고 하다. 하지만 필자는 이제부터 이러한 용어 대신에 “코멧”이라는 용어를 사용하도록 하겠다. 그렇다면 코멧이란 무엇인지 살펴보도록 하겠다. 

Ajax기술이 클라이언트에서 서버로 자신이 원하는 시간에 보내서 데이터를 가져오는 기술이라면 이 코멧이라는 기술은 서버에서 원하는 시간에 데이터를 클라이언트로 보내주게 되는 서비스라 할 수 있다. 그렇다면 이 기능이 어떻게 가능한 것일까? 웹이 TCP 통신 기반으로 서버와 통신을 한다는 것은 알고 있을 것이다. 하지만 브라우저는 원하는 시간에 데이터를 요청할 때 서버와 커넥션을 맺고 응답 즉, 데이터를 다 받게 되면 자동으로 연결을 끊어 버리게 된다. 왜냐하면 이렇게 통신 모델을 정의한 것이 바로 “HTTP 통신 프로토콜” 이기 때문이다. 때문에 웹 소켓을 지원해주는 플래쉬나 엑티브X 혹은 자바애플릿과 같은 웹 브라우저 확장 애플리케이션을 이용하는 것 외에 다른 방법을 떠오르기 힘들 것이다.

하지만 이 기사에서는 플래쉬나 엑티브X와 같은 기술을 설명하려는 것은 아니다. 코멧을 구현하기 위한 핵심기술은 바로 롱폴링(Long Polling) 기법이며 이번 회에서는 이 기술이 개념과 어떻게 효율적으로 이용할 수 있을지에 대해서 설명할 것이다. 물론, 플래쉬나 실버라이트와 같은 기술을 활용하는 것도 방법이 될 수 있겠지만 이러한 기술들은 바로 웹 접근성이라는 제약을 가지고 있다. 즉, 사용자에게 이러한 확장 애플리케이션을 설치하도록 권고해야 하고 모바일 플랫폼처럼 혹여나 지원되지 않는 플랫폼에서는 지원이 힘들 수 있다. 그렇기 때문에 구글토크나 페이스북에서 제공하는 웹 채팅의 경우 모두 순수한 HTTP 통신을 이용한 폴링과 롱폴링을 적절히 조합해서 서비스를 제공하고 있다.


폴링 기법의 소개와 그 한계

그렇다면 서버의 이벤트를 확인하는 방법은 어떤 것들이 있을지 생각해보도록 하자. 가장 먼저 떠오르는 기술이 바로 폴링 기법일 것이다. 
즉, 특정 주기 별로 서버를 계속 호출하며 새로 발생한 이벤트가 있는지 가져오는 것이다. 이 폴링기법은 구현이 굉장히 심플하고 로직 또한 간단하기 때문에 아마 웹에서 가장 많이 사용하고 있는 방법일 것이다. 다음 [그림1]은 폴링 기법을 보여주고 있다.
 

[그림1] 폴링 기법


보통 폴링 기법은 iframe을 이용하거나 자바 스크립트 타이머를 이용할 수 있을 것이다. 가장 쉽게 구현할 수 있다는 대신에 많은 단점들이 존재한다. 먼저 쓸모 없는 “요청-응답” 때문에 서버에 부하가 걸리기 쉽고 또한 많은 트래픽이 낭비된다. 사용자 수가 소수라면 크게 상관없을 수 있지만 대형 사이트에서 이러한 폴링 기법을 도입한다면 서버 부하는 상상이상으로 늘어나게 된다. 일반적으로 하나의 “요청-응답”을 처리하기 위해서 서버에서는 새로운 커넥션 즉, 소켓을 생성하게 된다. 그리고 요청을 내려주고 다시 커넥션을 끊는 작업을 진행하게 되는 것이다. 


보통 웹 사이트에서 한 페이지를 내려 받기 위해서는 이미지, 스크립트 파일 등의 여러 구성 요소를 다운받기 위해서 적어도 20개 이상의 요청-응답이 발생된다. 그렇다고 이거와 비교해서 폴링 요청이 1초에 한번씩 발생된다고 한들 상관없지 않겠냐고 생각한다면 큰 오해이다. 일반적으로 하나의 HTML 페이지를 내려주는데 있어서 브라우저에서 정의된 최대 커넥션 개수대로 커넥션을 생성한 뒤에 그 커넥션을 공유해서 사용하게 된다. 때문에 페이지에서 이미지와 같은 파일을 다운받기 위해서 100개의 요청을 보낸다고 한다 하더라도 커넥션을 생성하고 파괴하고 하는 반복되는 작업을 진행하지 않는 다는 것이다. 반면에 폴링은 단 한번의 요청에 하나의 커넥션을 열고 닫게 되기 때문에 실로 서버의 부하가 더욱더 클 수 있는 것이다.

두 번째 폴링 기법의 한계는 정밀한 시간에 데이터를 받기 힘들다는 것이다. 정말 0.5초도 용납되지 않은 주식과 같은 페이지를 폴링으로 돌리게 될 경우에 0.1초마다 서버를 호출하지 않는 이상 정확한 이벤트 타이밍을 전달하기 힘들 것이다. 그렇다고 0.1초마다 서버를 호출했다가는 서버가 수많은 클라이언트의 요청을 견디기 힘들 것이다.

그렇기 때문에 폴링을 이용하기 가장 좋은 시나리오는 사용자가 많지 않으며 이벤트 발생 시간에 크게 민감하지 않는 페이지가 가장 적당하다고 볼 수 있다. 즉, 재 요청 수가 1초에 몇 개나 일어나게 될 것인지를 동시 접속 사용자 수와 간격에 따라서 살펴봐야 할 것이다. 예를 들어 페이스 북의 웹 채팅에서는 1분에 한번씩 접속한 사용자 리스트를 갱신하기 위해서만 폴링 기법을 사용하고 있다. 이렇게 반복하는 주기가 짧지 않을 경우에는 폴링 기법을 추천하는 바이다.

폴링 기법을 위해서는 다음 [코드1]과 같이 타이머와 함께 XmlHttpRequest을 이용할 수 있을 것이다.

setTimeout(function(){xhrRequest({"foo":"bar"})}, 2000);
function xhrRequest(data){
    var xhr = new XMLHttpRequest();
    //데이터 요청 보내기
    xhr.open("get", "http://localhost/foo.php", true);
    xhr.onreadystatechange = function(){
        if(xhr.readyState == 4){
        // 서버로부터 업데이트 하기
        }
    };
    xhr.send(null);
}

[코드1] 타이머를 이용한 폴링의 구현



폴링의 대안 롱폴링

앞에서 설명한 폴링의 한계는 서버가 일어나는 정확한 타이밍에 데이터를 가져올 수 없기 때문에 주기적으로 서버를 들락날락 거릴 수 밖에 없고 그만큼 서버 부하가 걸리게 된다고 했었다. 롱폴링은 바로 이러한 폴링의 한계를 보완할 수 있는 기법으로 이 기술을 서버 푸쉬라고 부르기도 한다. 다음 [그림2]는 롱폴링의 동작 방식을 보여주고 있다.



[그림2] 롱폴링의 동작 방식


즉, 서버에서는 커넥션을 물고 기다리고 있으며 이벤트의 업데이트가 있을 경우에 클라이언트로 응답을 보내주는 경우가 될 수 있다. 즉, 특정 이벤트가 일어날 경우에만 커넥션을 끊기 때문에 쓸데 없이 요청을 만들고 끊게 되는 일은 줄일 수 있는 것이다. 그렇다면 여기서 커넥션 개수에 따른 서버 메모리의 이슈와 브라우저에서 사용하는 최대 커넥션 개수에 대해서 어떤 영향을 미치게 될지에 대해서 이야기 해볼 수 있는데 이러한 이슈들은 뒷부분에서 다룰 것이고 여기서는 어떻게 롱폴링을 구현할 수 있는지에 대한 내용을 다루도록 하겠다.

롱폴링을 구현하기 위한 기술은 다양하다. IFrame을 이용하는 방법, XmlHttpRequest를 이용하는 방법, JSONP라고 불리고 있는 자바스크립트 삽입을 이용하는 방법, 그리고 IE에서 Iframe의 한계를 보안하기 위한 HtmlFile 개체를 이용하는 방법 정도가 있다. 지금 업계에서 가장 대중적으로 사용하고 있는 것은 JSONP 방식이다. 왜냐하면 이 방법을 이용하면 크로스 도메인의 이슈를 해결할 수 있기 때문이다. 물론 각각 방식의 장단점이 존재한다. 필자도 처음에 웹 채팅 프로그램을 구현하기 위해서 이 각각의 기술들을 가지고 많은 시도를 해봤고 다음 [표1]과 같은 결론을 내릴 수 있었다. 

 

기술장점단점
iframe구현이 쉽다 로딩이 표시된다.IE에서 딸깍딸깍 소리가 난다.
크로스 도메인을 지원하지 않는다.
htmlFile크로스 도메인을 어느 정도 사용할 수 있다.
로딩바 표시나 딸깍 로딩소리를 없앨 수 있다.
Chunked인코딩을 이용해서 스트리밍 서비스를 제공할 수 있다. 
IE에서만 동작된다.
JSONP 크로스 도메인 문제를 해결할 수 있다.
로딩바 표시나 별도의 소리가 나지 않는다.
Chunked 인코딩을 이용할 수 없다.
XMLHttpRequestXmlHttpRequest 최신 브라우저에서는 스트리밍 서비스를 구현할 수 있다. 크로스 도메인을 지원하지 않는다.

[표1] 롱폴링을 위한 기술들

그럼 몇 가지 기술들의 코드를 살펴보도록 하겠다. 이번 회에서는 클라이언트 단, 즉 자바스크립트 코드만 간단히 살펴보고 다음 회에서 서버단과 같이 살펴보도록 하겠다. 왜냐하면 서버의 기술은 닷넷이나 자바 php 등 다양해 질 수 있기 때문이다. 다음 코드는 Iframe을 활용한 롱 폴링 예제를 보여주고 있다. 

//클라이언트 코드
function foreverFrame(url, callback) {
    var iframe = body.appendChild(document.createElement("iframe"));
    iframe.style.display = "none";
    iframe.src = url + "?callback=parent.foreverFrame.callback";
    this.callback = callback;
}
//아이프레임으로 서버에서 반환한 메시지
parent.foreverFrame.callback("전달할 메시지"); 


[코드2] 아이프레임을 이용한 롱폴링

즉, 포에버 프레임이라는 것을 만들고 그 프레임을 이용해서 요청을 계속 보내는 방식인 것이다. 그리고 서버에서는 특정 자바스크립트 코드를 내려줌으로써 URL을 반복하여 다시 호출하게 되는 것이다. 하지만 IE에서는 문제가 굉장히 안 좋은 사용자 경험을 가져다 주기 때문에 대안으로 구글토크에서 시도했던 방식이 바로 htmlfile이라는 ActiveX를 객체를 이용하는 것이었다. 단, IE에서만 이용이 가능하기 때문에 만약 모든 브라우저에서 서비스를 하고 싶다면 브라우저 버전 별로 다른 코드를 적용시켜 주어야 한다. 다음 [코드3]은 htmlfile ActveX객체를 이용한 예를 보여주고 있다.
 

//로딩시 호출하는 함수
function foreverFrame(url) {
//htmlfile 만들기
    var transferDoc = new ActiveXObject("htmlfile");
    transferDoc.open();
    transferDoc.write(
        "<html><script>" +
        //"document.domain='" + document.domain + "';" +
        "</script></html>");
    transferDoc.close();
    var ifrDiv = transferDoc.createElement("div");
    transferDoc.body.appendChild(ifrDiv);
    
    transferDoc.parentWindow.callback = function (msg) {
            //여기에서 콜백 함수 실행 후 다시 연결시도
    }
    ifrDiv.innerHTML = "<iframe id='ifr' src='" + url + "'></iframe>";
}
//서버에서 내려주는 코드
<script>parent.callback("서버 메시지");</script>

[코드3] htmlfile 객체의 이용


이 방법의 가장 큰 장점은 크로스 도메인을 어느 정도 해결할 수 있고, chunked 인코딩을 이용할 수 있다는 장점을 가지고 있다. 여기서 크로스 도메인을 어느 정도 해결한다는 말은 다른 도메인은 허용하지 않지만 포트가 다른 것까지는 허용한다는 것이다. 그리고 Chunked 인코딩이란 웹 서버에서 클라이언트로 데이터를 분할하여 내려 주는 방식으로 웹 서버에서 Flush 메서드를 이용하는 것이 바로 이 프로토콜을 이용하는 것이다. Chunked 인코딩 프로토콜에 대해서는 다음 회에서 보다 자세히 살펴 볼 것이다.

이제 마지막으로 롱폴링을 위한 기술로서 JSONP라고 불리고 있는 자바스크립트 요청을 보내는 것이다. 다음 [코드4]를 보고 이해해 보도록 하자.

function callbackPolling(url, callback){
    // 서버로부터 응답을 처리할 스크립트를 생성한다.
    var script = document.createElement("script");
    script.type = "text/javascript";
    script.src = url + "callback=callbackPolling.callback";
    callbackPolling.callback = function(data){
        // 서버에서 보낼 새로운 요청을 위해서 새로운 요청을 보낸다. 
        callbackPolling(url, callback);
        // call the callback
        callback(data);
    };
    // 로딩시 다음 엘리먼트를 추가한다.
    document.getElementsByTagName("head")[0].appendChild(script);
}


[코드4] JSONP를 활용하여 크로스 도메인의 해결

JQuery에서 제공하는 JSONP라는 기능이 바로 내부적으로는 이와 같은 코드를 이용해서 크로스 도메인 문제를 해결하고 있는 것이다. 롱폴링 기법을 이용하기 위한 클라이언트 단의 기술은 이와 같이 다양하고 각각의 장단점을 가지고 있다. 때문에 각 상황을 분석하여 그 상황에 가장 적절한 기술을 선택하여 사용하면 될 것이다. 


스트리밍의 구현

스트리밍은 단순 “요청-응답” 모델이 아니라 클라이언트가 원하는 시간에 데이터를 서버로 보낼 수 있고 서버 또한 클라이언트로 데이터를 푸쉬하는 모델이라 할 수 있다. 다음 [그림3]은 스트리밍 서비스의 예를 보여주고 있다.


[그림3] 스트리밍 서비스의 이해
 
그리고 다음 [그림4]는 폴링과 롱폴링 그리고 스트리밍의 차이를 한눈에 볼 수 있게 정리하여 보여주고 있다.


[그림4] 폴링, 롤폴링 그리고 스트리밍의 차이
 
그렇다면 이렇게 연결이 연결을 유지한 채로 데이터를 원하는 시간에 자유롭게 데이터를 주고 받을 수 있는 기술은 어떤 것들이 있을까? 먼저 앞에서 설명했듯이 플래쉬, 자바애플릿, 실버라이트, ActiveX와 같은 브라우저 애드온 기술을 이용할 수 있다. 그리고 지금 열심히 표준을 제정 중에 있는 HTML5 또한 웹소켓 기능을 포함할 것이라고 밝혔다. 하지만 필자의 사견으로는 국내 사용자가 99%이상이 HTML5를 지원하는 브라우저를 이용하게 될 시기가 빨라야 10년 혹은 그 이후가 될 수도 있을 정도로 굉장히 먼 이야기이기 때문에 이 기능을 기다리는 것은 결코 답이 될 수 없다. 

추가로 스트리밍을 구현하기 위한 기술로서 최신 브라우저에서만 지원하는 업그레이드 된 XHR(XmlHttpRequest)을 이용할 수도 있다. 하지만 IE8이상에서만 지원하고 파이어폭스나 사파리에서도 최신 버전에서만 지원하기 때문에 이 기술을 이용하는 것 또한 웹 접근성이 굉장히 떨어진다. 다음 [코드5]는 업그레이드 된 XHR을 이용하여 스트리밍 서비스를 구현한 예를 보여주고 있다. 다음 코드에서는 readyState 상태 변화 이벤트를 통하여 서버의 이벤트를 감지해서 데이터를 처리하는 예를 보여주고 있다.

function xhrStreaming(url, callback){
  xhr = new XMLHttpRequest();
  xhr.open('POST', url, true);
  var lastSize;
  xhr.onreadystatechange = function(){
    var newTextReceived;
      if(xhr.readyState > 2){
        // 가장 최신의 텍스트 가져오기
        newTextReceived =
        xhr.responseText.substring(lastSize);
        lastSize = xhr.responseText.length;
        callback(newTextReceived);
      }
      if(xhr.readyState == 4){
        // 만약 응답이 마칠 경우 새로운 요청을 만든다.
        xhrStreaming(url, callback);
      }
  }
  xhr.send(null);
}

[코드5] XHR을 이용한 스트리밍 서비스

현실적으로 봤을 때 순수 HTTP 통신을 이용해서 스트리밍을 구현하는 것은 불가능하다. 그렇다고 해서 특정 벤더 기술에 종속된 기술을 이용하기에는 웹 접근성에 있어서 한계가 있기 마련이다. 그렇다면 지금의 웹 채팅은 어떻게 이용하고 있는 것일까? 페이스북의 채팅의 경우 스트리밍 서비스 대신 폴링과 롱폴링을 적절히 섞어서 이용하고 있다. 다음 [그림5]는 페이스북의 채팅 예제를 보여주고 있다. 


[그림5] 페이스북 웹 채팅의 구조

채팅의 대한 자세한 내용과 구조는 다음 회에서 보다 자세히 살펴보도록 하겠다. 


커넥션 관리 이슈

롱폴링의 경우 서버에서 커넥션을 물고 있기 때문에 커넥션 관리 이슈를 빼놓고 생각해 볼 수 없다. 기존의 폴링은 커넥션을 자주 연결하고 끊기 때문에 서버 트래픽이나 서버 CPU의 영향을 주게 된다면 커넥션을 물고 있을 경우 가장먼저 생각해 봐야 하는 것이 메모리 이슈이다. 그리고 두 번째는 브라우저에서 한 사이트당 최대 커넥션 수를 제한하고 있기 때문에 잘못 했다가는 사이트가 먹통이 되어 버리는 브라우저의 이슈가 있다. 

그렇다면 먼저 메모리 이슈부터 살펴보도록 하겠다. 물론 커넥션당 하나의 스레드가 할당될 것이고 어떤 작업을 하게 되느냐에 따라서 CPU 비용을 사용하게 될 수도 있지만 적절히 스레드를 쉬어가면서 운영을 하게 되면 일반적으로 CPU 때문에 크게 이슈가 되지는 않을 것이다. 그것보다도 서버를 운영하다 보면 부딪치게 되는 이슈가 바로 메모리 이슈이다. 보통 스레드와 스택기반의 커넥션을 이루게 될 경우 한 사용자 당 약 2메가 정도를 예상해 봐야 할 것이다. 

이 수치는 약 오천 명이 동시 접속을 이루게 될 경우 10기가 정도의 메모리가 소모된다는 것이다. 하지만 웹에 오천 명이 동시에 접속한다는 것 자체가 굉장히 대단한 수치이다. 대한민국 국민의 0.01%가 모두 한 사이트에 들어와서 특정 서비스를 이용하고 있다는 것이기 때문이다. 때문에 대규모의 서비스가 아닐 경우라면 크게 걱정하지 않아도 될 것이다. 물론, 국내에서 몇 손가락 안에 드는 그런 어마어마한 사이트에서 웹 채팅을 구현한다고 한다면 각각 서버를 분할해서 구현해야 할 것이다. 하지만 이 내용은 이번 기사의 범위에서 벗어나므로 다루지 않도록 하겠다. 

두 번째로 생각해 볼 이슈는 바로 브라우저의 커넥션 제한 이슈이다. 브라우저는 각각의 최대 커넥션 개수를 가지고 있다. 즉, 최대 커넥션 개수 범위 안에서 커넥션을 공유하면서 서버와 통신을 하게 되는 것이다. 하지만 롱폴링을 이용하게 되면 하나의 커넥션을 헌납해야 한다. 다음 [표1]은 각 브라우저 별로 커넥션 수를 보여주고 있다. 


[표1] 브라우저별 커넥션 수

원래 HTTP 프로토콜에서 권고하는 도시 커넥션 수는 2개였고, 지금도 그러하다. 하지만 파이어폭스3가 발표될 때 6개로 확장을 시작하였고, 크롬도 그에 맞서 6개로 확장하여 지원하고 있다. 그에 맞서 IE8도 6개로 확장하였다. 왜냐하면 이 커넥션 숫자가 바로 웹 사이트를 다운로드 하는 속도와 아주 밀접하게 연결이 되어있기 때문이다. 하지만 조금 오래된 브라우저들은 여전히 2개의 커넥션만 허용하고 있기 때문에 웹사이트가 먹통이 되거나 굉장히 느려질 수 있기 때문에 커넥션 관리를 철저하게 해주어야 한다.

정리

이번 기사에서는 코멧을 구현하는 방법 세가지 폴링, 롱폴링, 스트리밍 이렇게 세 가지를 살펴보았다. 현 시점에서는 입맛에 딱 맞는 완벽한 스트리밍 서비스를 구현하기에는 많은 한계가 존재한다. 그렇기 때문에 많은 현재 대형 서비스 사이트에서 이용하는 것과 같이 폴링과 롱폴링을 적절히 조합하여 사용하는 것이 최선일 것이다. 이번 기사에서는 코드보다는 이론적인 내용들을 많이 설명하였다. 코멧을 구현하는데 있어서 필요한 서버를 구현하는 방법과 같이 자세한 코드나 내용들은 다음 기사에서 살펴보도록 하겠다.


참조
http://www.metabrew.com/article/a-million-user-comet-application-with-mochiweb-part-3/
http://aleccolocco.blogspot.com/2008/10/gazillion-user-comet-server-with.html
http://svn.cometd.com/trunk/bayeux/bayeux.html