코멧(Comet) #3 – Ajax 롱폴링(Ajax Long polling) 채팅방 예제로 배우기

코멧을 구현하는 두 번째 방법은 롱폴링(Long Polling)이라고 하는 기법으로, 그 중에서도 금번 포스팅에서는 Ajax를 기초로 한 롱폴링 기법을 소개합니다. 이것은 지난 포스팅에서 소개한 Ajax 폴링 기법에서 일부분을 변형하여서, HTTP 통신이 일어나는 빈도를 줄인 것입니다. Ajax 폴링과 이 기법 사이의 결정적인 차이점은 서버측에서 특정한 상태값이 변하기 전까지는 응답을 미루는 것이라고 할 수 있습니다.

금번 포스팅에서는 지난 포스팅에서 Ajax 폴링으로 구현한 채팅방 예제를 롱폴링으로 수정하여, Ajax 폴링과 대조되는 롱폴링의 작동원리를 살펴볼 것입니다. 따라서 지난 포스팅을 읽어보지 않은 분께서는 금번 포스팅을 읽기에 앞서 일독을 권합니다.

 

롱폴링 채팅방 예제코드 (다운로드)

금번에 소개하는 예제는 지난 포스팅에서 소개한 Ajax 폴링 채팅방 예제를 롱폴링으로 수정한 것입니다. 이전 예제와 동일하게 zip 파일 1개로 압축되어 있으며, 아래와 같이 6개의 파일이 있습니다. 이 중에서 지난 포스팅과 소스코드가 달라진 파일은 단 2개, proc.php과 chat.js 뿐입니다. 소스코드의 작동환경은 여전히 자바스크립트, PHP, MySQL입니다.

  • index.html : 채팅방 메인 페이지
  • proc.php : GET 파라메터로 주어진 날짜 이후의 채팅내용을 가져와 JSON 포맷으로 출력하는 페이지
  • write.php : 닉네임과 채팅내용을 받아 데이터베이스에 입력하는 페이지
  • chat.js : 자바스크립트 파일
  • chat.css : 스타일시트 파일
  • chat.sql : 데이터베이스 테이블을 생성하기 위한 sql문 (MySQL 기준)

본 예제를 여러분의 서버에 업로드하여 실행하실 때에는, 먼저 MySQL 상에서 chat.sql 파일 안의 sql문을 실행하여 DB 테이블을 생성하셔야 합니다. 또한 proc.php와 write.php 파일에는 DB 연결하는 코드가 있으므로, 이 부분을 여러분이 사용하는 MySQL의 계정과 비밀번호, 데이터베이스 이름으로 바꾸셔야 합니다.

 

자바스크립트 예제코드 – chat.js

Ajax 폴링 예제의 자바스크립트 코드와 비교했을 때, 롱폴링 예제의 자바스크립트 코드는 단지 3군데 만이 변경되었습니다. 새롭게 추가된 코드가 2군데 있으며, 제거된 코드가 1군데 있는데요. 명확한 설명을 위해서 금번에도 소스코드의 일부분은 생략하였습니다.

var chatManager = new function(){
	var interval	= 500;
	var xmlHttp		= new XMLHttpRequest();
	var finalDate	= '';

	// Ajax Setting
	xmlHttp.onreadystatechange = function()
	{
		if (xmlHttp.readyState == 4 && xmlHttp.status == 200)
		{
			// JSON 포맷으로 Parsing
			res = JSON.parse(xmlHttp.responseText);
			finalDate = res.date;
			
			// 채팅내용 보여주기
			chatManager.show(res.data);

			// 채팅내용 가져오기 [추가]
			chatManager.proc();
		}
	}

	// 채팅내용 가져오기
	this.proc = function()
	{
		// Ajax 통신
		xmlHttp.open("GET", "proc.php?date=" + finalDate, true);
		xmlHttp.send();
	}

	// 채팅내용 보여주기
	this.show = function(data)
	{
		var o = document.getElementById('list');
		var dt, dd;

		// 채팅내용 추가
		for(var i=0; i<data.length; i++)
		{
			dt = document.createElement('dt');
			dt.appendChild(document.createTextNode(data[i].name));
			o.appendChild(dt);

			dd = document.createElement('dd');
			dd.appendChild(document.createTextNode(data[i].msg));
			o.appendChild(dd);
		}
	}

	// interval에서 지정한 시간마다 실행 - [제거]
	// setInterval(this.proc, interval);
}

// 페이지 로딩을 끝마치면 채팅내용 가져오기 시작 [추가]
window.onload = function()
{
	chatManager.proc();
}

이전 장의 Ajax 폴링 코드에서는 setInterval() 함수를 통하여, 정해진 시간 간격으로 Ajax 통신을 하여 채팅 데이터를 가져오는 proc() 함수를 실행하였습니다. 그 흔적이 위 코드의 51번줄에 남아있는 것을 볼 수 있습니다.

그러나 롱폴링 코드에서는 이 부분이 제거되어 있습니다. 더이상 일정한 시간 간격으로 통신을 하지 않는다는 것입니다. 대신에 55번줄에 의해, 페이지 로딩을 마치면 Ajax 통신으로 채팅 데이터를 가져오는 proc() 함수를 실행합니다. 그렇게 실행된 데이터 통신과 그 처리가 모두 끝나고 나면, 18번줄의 proc() 함수에 의해서 Ajax 통신을 새롭게 실행합니다. 이를 통해 통신이 끝나면 다음 통신을 시작하고, 그 통신이 끝나면 다시 통신이 시작되는 꼬리물기 형태의 로직이 완성됩니다.

 

PHP 예제코드 – proc.php

이제 Ajax 통신을 통해 호출되는 proc.php를 살펴볼 차례입니다. 아래 롱폴링 예제의 proc.php는 이전 장의 예제와 유사한 형태를 가지고 있지만, 쿼리실행이 무한루프 안에서 이루어진다는 점을 유심히 봐두셔야 합니다. 이 무한루프가 바로 롱폴링의 핵심적인 아이디어이기 때문입니다.

$data = array();
$date = $_GET['date'];

$db = new mysqli('localhost', 'epiloum', 'thatkxkd86!', 'test');
$db->query('SET NAMES utf8');

while(1)
{
 	$res = $db->query('SELECT * FROM chat WHERE date > "' .$date. '"');

	if($res->num_rows > 0)
	{		
		while($v = $res->fetch_array(MYSQLI_ASSOC))
		{
			$data[] = $v;
			$date = $v['date'];
		}
		
		break;
	}
	
	usleep(250000);
}

echo json_encode(array('date' => $date, 'data' => $data));

Ajax 폴링의 예제코드는 단순히 데이터베이스에 접근하여 인자로 받은 날짜 이후에 입력된 채팅내용들을 가져와 JSON 포맷으로 출력해주는 역할을 하였습니다. 그러나 롱폴링의 예제코드는 이 쿼리를 무한루프 안에서 실행합니다. 쿼리의 결과로 가져온 레코드가 없을 경우, 250ms 후에 같은 쿼리를 다시 실행합니다. 그러나 쿼리의 결과가 1행 이상일 경우에는 데이터를 배열에 담고 무한루프를 종료합니다.

이 무한루프는 인자로 받은 날짜 이후에 어떠한 채팅도 입력되지 않았을 경우의 처리방법을 다르게 합니다. 반복문이 없는 이전 장의 Ajax 폴링 예제는 이 경우 빈 배열을 JSON 포맷으로 출력하고 통신을 마쳤을 것입니다. 그러나 위 롱폴링 예제는 새로운 채팅이 입력될 때까지 연결을 끊지 않고 기다립니다.

 

롱폴링의 장단점

롱폴링의 핵심적인 장점은 HTTP 통신의 빈도가 비교적 낮다는 것입니다. 데이터의 변경이 있을 때까지는 통신을 끊지 않기 때문입니다. HTTP는 헤더가 크고 응답 후에 연결이 끊어지는 특징 때문에 코스트가 높은 프로토콜에 속합니다. 때문에 HTTP 통신의 빈도를 줄이는 일은 서버에 가해지는 부하를 줄이는 효과를 가져옵니다.

이를 통해 파생되는 롱폴링의 또다른 장점은 매우 짧은 시간간격으로 데이터 변경을 확인할 수 있다는 점입니다. 만약 Ajax 폴링 기법으로 데이터 변경을 1초에 10회 확인하겠다면, HTTP 통신 또한 1초에 10회 이루어질 것입니다. 이 정도면 소형 서비스에서도 부담이 될만한 통신빈도입니다. 그러나 데이터 변경이 될 때까지는 통신을 끊지 않고 기다리는 롱폴링 기법이라면, 데이터 변경을 1초에 10회 확인하더라도 실제 HTTP 통신의 횟수는 줄어들게 됩니다.

그러나 이러한 롱폴링에도 한계는 분명히 존재합니다.

첫 번째로 롱폴링은 데이터 변경이 빈번한 경우에는 오히려 통신횟수가 늘어날 수 있는 문제가 있습니다. 롱폴링은 데이터의 변경이 없으면 데이터가 변경될 때까지 기다리는 한편, 일단 데이터가 변경되면 즉각적으로 그 데이터를 반환합니다. 때문에 데이터가 변경되는 빈도가 데이터 변경을 확인하는 빈도보다 짧아진다면, 오히려 일정한 시간간격을 두고 통신을 하는 Ajax 폴링이 더 나은 선택일 수도 있고, 굳이 롱폴링 기법을 고수한다면 소스코드 상에서 충분한 안전장치를 마련해야 합니다.

두 번째로 롱폴링은 데이터가 변경되는 순간에 연결이 집중되는 문제가 있습니다. 만약 n명의 임직원이 동시에 접속해있는 사내 ERP 도구에서 실시간 공지기능을 롱폴링으로 구현했다고 가정해봅시다. 만약 공지사항 1건이 새로 등록된다면, 등록되는 순간 n개의 HTTP 응답과 n개의 HTTP 요청이 매우 짧은 시간에 집중될 것입니다. 이 숫자가 100명이 되고 1000명이 되고 그 이상이 된다면, 집중된 HTTP 통신이 서버를 위험에 빠뜨릴 수도 있습니다. 이 때문에 하나의 데이터를 많은 사람들이 바라보고 있는 서비스에서는 롱폴링을 도입하기 어렵습니다.

마지막으로 롱폴링은 데이터 변경까지는 연결이 유지되는 특성 때문에 동시접속수가 크다는 문제가 있습니다. 일반적으로 웹서버에는 최대요청한계(Max Request Limit)가 있기 때문에, 동시접속한 클라이언트가 늘어나면 문제가 됩니다. 이 포스팅에서 제공하고 있는 예제파일에는 이 부분을 완화하고자, proc.php의 무한루프 부분이 for문으로 대신 구현되어 있습니다. 이를 통해 예제코드는 for문이 80회 반복되면 새로 등록된 채팅내용이 없더라도 연결이 종료됩니다.

 

롱폴링을 도입하기에 적합한 상황

롱폴링은 실시간에 가까운 매우 짧은 응답을 구현할 수 있지만, 데이터 변경빈도나 동시접속수에 있어서는 제한이 있습니다.

이러한 기술적 특징에 가장 부합하는 기능이 바로 그룹채팅입니다. 채팅은 일반적으로 실시간에 가까운 빠른 반응이 매우 중요한 반면, 채팅이 매우 빠르게 진행되더라도 데이터 변경 간의 시간이 수 초에 이르며, 채팅에 참여하는 인원 또한 적게는 2명에서 많아도 수십명에 그칠 가능성이 높습니다. 때문에 웹 환경에서 채팅을 구현하는 경우에는 롱폴링 기법을 선택하는 것이 일반적입니다.

그러나 반대로 같은 데이터를 수십명에서 수천명이 바라보아야 하는 스포츠 문자중계나, 데이터의 갱신이 실시간으로 이루어지는 센서 모니터링 등의 기능에는 도입하기 어려울 것입니다.

2 thoughts on “코멧(Comet) #3 – Ajax 롱폴링(Ajax Long polling) 채팅방 예제로 배우기

  1. 설명해 주신 내용 너무 잘 보았습니다.
    한가지 궁금한 점이 있는데요,
    while이나 for문 안에서 새로운 데이터 변화가 감지될 때까지 계속해서 DB에 쿼리를 날리게 되는데
    예제로 드신것처럼 1client가 250ms간격의 속도로 같은 쿼리를 빠르게 날리는 것은
    DB에서는 크게 부담이 가지 않는 부분인가요?
    클라이언트들이 붙을수록 http 통신도 문제지만 DB단에서의 부하도 가중되는게 아닐까 싶어서요.
    100클라이언트면 초당400회의 db 조회인데 물론 단순한 select쿼리 이긴 하지만
    DB에 걸리는건 그래도 좀 감내가 가능한 수준이고 http통신에 걸리는 부하가 더 많이 문제 되는 것인지 궁금합니다.

    • 김가별님, 안녕하세요. 관심 가져주시고 질문해주셔서 감사합니다. 위 포스팅에서는 롱폴링 기법의 개념을 전달하기 위해 MySQL 기반으로 코드를 작성하였습니다만, 김가별님께서 짐작하고 계신대로 이처럼 잦은 통신이 일어나는 서비스를 실무에서 구축할 때는 DB 부하가 이슈가 됩니다.

      그렇다고 해도 “사용자에게 실시간에 가깝게 DB에 저장된 정보를 제공해야 한다”는 요구사항이 변하지 않는 이상, DB 쿼리의 빈번함은 구현방법을 바꾸더라도 필연적으로 발생할 수 밖에 없는 이슈입니다. 따라서 롱폴링 이외의 다른 기법을 찾아보는 것은 의미가 없고, 일반적인 웹서비스를 구축할 때와는 DB 구성을 다르게 하여 문제를 해결하는 것이 최선이라고 생각합니다. 이에 대한 아이디어는 매우 다양하게 생각해볼 수 있겠습니다만, 일단 제가 쉽게 떠올릴 수 있는 부분은 아래 정도인 것 같습니다.

      1. 새로운 DBMS의 검토
      우리에게 익숙한 MySQL의 경우, 병렬처리를 지원하지 않는다는 것이 가장 치명적인 부분입니다. 따라서 병렬처리로 쿼리 부담을 줄이고자 한다면 Oracle를 비롯한 다른 DBMS를 고려할 수도 있습니다. 한편 SELECT 쿼리 성능을 극대화한 서드파티 솔루션의 도입을 검토할 수도 있습니다. 잘 구성된 서드파티 솔루션은 초당 수십만건의 단순쿼리 처리성능을 보이기도 합니다.

      2. 열람용 데이터와 보관용 데이터를 분리
      롱폴링의 경우, 갱신할 데이터의 유무를 확인하는 쿼리가 빈번하게 발생하기 때문에, 이 쿼리의 실행속도가 보틀넥이 됩니다. 만약 데이터를 열람할 테이블과 보관할 테이블을 분리하고, 이 쿼리가 실행되는 테이블을 최소한의 레코드 개수로 유지한다면, 쿼리속도의 향상을 기대할 수 있습니다. 채팅과 같이 사용자가 과거 데이터를 열람할 일이 거의 없는 경우에는 이러한 해법이 도움이 됩니다. 더 나아가 열람용 테이블에 대해서는 MongoDB 도입을 고려해볼 수 있는데, MongoDB는 인덱스와 최근에 접근한 데이터들을 자동으로 메모리에 캐시하여 성능향상을 기대할 수 있기 때문입니다.

      3. 데이터 갱신일시를 별도 보관
      갱신 데이터의 유무 여부를 따로 확인할 수 있는 테이블을 만드는 방법도 생각해볼 수 있습니다. 채팅의 사례를 예시로 들면, 채팅방이 생성될 때마다 이에 해당하는 Key값을 생성하고, 각 Key값의 채팅방에서 마지막으로 INSERT가 일어난 날짜를 별도 테이블에 보관하는 것입니다. 단순 Key값으로 접근하는 쿼리는 상당히 빠르며, 적은 레코드 개수와 컴펙트한 스키마가 그 속도를 더욱 높일 것입니다. 좀더 욕심을 부리면 이 갱신일시 테이블을 MySQL에서 분리하여, Redis 등을 이용해 메모리 영역에서 확인할 수 있게 한다면 더욱 강력한 해결책이 될 것입니다.

김가별 에 응답 남기기 응답 취소

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.

*

다음의 HTML 태그와 속성을 사용할 수 있습니다: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>