코멧을 구현하는 두 번째 방법은 롱폴링(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명에서 많아도 수십명에 그칠 가능성이 높습니다. 때문에 웹 환경에서 채팅을 구현하는 경우에는 롱폴링 기법을 선택하는 것이 일반적입니다.
그러나 반대로 같은 데이터를 수십명에서 수천명이 바라보아야 하는 스포츠 문자중계나, 데이터의 갱신이 실시간으로 이루어지는 센서 모니터링 등의 기능에는 도입하기 어려울 것입니다.
설명해 주신 내용 너무 잘 보았습니다.
한가지 궁금한 점이 있는데요,
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. 열람용 데이터와 보관용 데이터를 분리
롱폴링의 경우, 갱신할 데이터의 유무를 확인하는 쿼리가 빈번하게 발생하기 때문에, 이 쿼리의 실행속도가 보틀넥이 됩니다. 만약 데이터를 열람할 테이블과 보관할 테이블을 분리하고, 이 쿼리가 실행되는 테이블을 최소한의 레코드 개수로 유지한다면, 쿼리속도의 향상을 기대할 수 있습니다. 채팅과 같이 사용자가 과거 데이터를 열람할 일이 거의 없는 경우에는 이러한 해법이 도움이 됩니다. 더 나아가 열람용 테이블에 대해서 Redis와 같은 메모리 기반의 NoSQL로 속도를 크게 개선하는 것도 검토해볼 수 있습니다.
3. 데이터 갱신일시를 별도 보관
갱신 데이터의 유무 여부를 따로 확인할 수 있는 테이블을 만드는 방법도 생각해볼 수 있습니다. 채팅의 사례를 예시로 들면, 채팅방이 생성될 때마다 이에 해당하는 Key값을 생성하고, 각 Key값의 채팅방에서 마지막으로 INSERT가 일어난 날짜를 별도 테이블에 보관하는 것입니다. 단순 Key값으로 접근하는 쿼리는 상당히 빠르며, 적은 레코드 개수와 컴펙트한 스키마가 그 속도를 더욱 높일 것입니다. 좀더 욕심을 부리면 이 갱신일시 테이블을 MySQL에서 분리하여, Redis 등을 이용해 메모리 영역에서 확인할 수 있게 한다면 더욱 강력한 해결책이 될 것입니다.
안녕하세요. 우선 이렇게 좋은 포스트를 작성해주심에 감사의 인사를 드립니다.
카톡처럼 1:1 채팅을 하고, 채팅방이 여러개일 경우에도 롱폴링을 써야하는지에 대해 궁금증이 생겨 질문드립니다.
1:1 이라 응답할경우 1개의 응답이 발생하겠지만, 채팅방 100곳에서 1개씩 친다면 연결에 집중되는 문제가 생기지는 않을까하는 생각에 질문을 드립니다
안녕하세요. 답을 드리는 것이 많이 늦어 죄송합니다. 글 남겨주셔서 감사합니다.
글에서 소개하여 드렸던 것과 같이 롱폴링이 기술적으로 문제가 되는 상황은 ① 데이터 변경이 빈번한 경우, ② 하나의 데이터 변경이 동시에 여러 사용자에게 영향을 미치는 경우, ③ 동시접속자가 많은 경우의 3가지로 정리해볼 수 있습니다.
말씀해주신 100개의 채팅방에서 1:1 채팅이 이루어지는 상황은 ①과 ②에 있어서는 문제가 되지 않습니다. ① 채팅은 사람이 타이핑을 하는 속도가 있기 때문에 데이터 변경의 간격이 수 초로 충분히 길고, ② 1:1이라는 특징상 한 채팅방에서 데이터 변경이 일어나도 영향을 주는 사용자는 1:1 채팅중인 2명에 한정되기 때문입니다.
문제가 된다면 ③으로 동시접속자가 200명이라는 점인데, 사실 동시접속수의 문제는 롱폴링 외에 웹소켓이나 폴링 방식을 취하여도 근본적으로 회피할 수 있는 문제는 아닙니다. 그러므로 예상수요에 맞추어 서버 스펙 및 대수를 결정하고 DBMS나 웹서버의 최대접속수(Maximum # of Connection) 설정에 유의하여 구현하여 운용하시는 것이 어떨까 싶습니다.
안녕하세요 php 초보 개발자입니다.
저는 대시보드를 만들면서 롱폴링을 쓰고자하는데
위에 기입된 소스로 구현을 해보았는데
새로고침을 시에 한참 후에야 새로고침이 되거나,
페이지 이동시에도 한참 후에야 되는것을 발견하였습니다.
이 경우, 브라우저에 캐시가 쌓여서 그런것일까요?
저와 같은 경우에 롱폴링이 맞는지 사실 아직 초보라 확신이 서지 않네요..
젼이님, 안녕하세요. 먼저 덧글 남겨주셔서 감사합니다. 기재해주신 내용만 토대로 보면 페이지의 이동이나 새로고침 시에 페이지가 웹브라우저에 표시될 때까지 오랜 시간이 걸린다는 점이 문제로 생각됩니다.
이 때는 웹브라우저에 내장된 개발자 도구(크롬이나 엣지의 경우, 단축키 F12로 표시)에서 네트워크(Network) 탭에 들어가 1차적인 진단을 하실 수 있습니다. 이 곳에서는 어떤 웹 페이지, JS, CSS, 이미지 등이 로딩되었는지, 또 그러한 로딩에 얼마나 오랜 시간이 걸렸는지 확인할 수 있습니다.
크게 2가지 중의 하나의 현상이 보이리라고 생각됩니다.
1. 하나 이상의 항목을 로딩하는 데 대단히 긴 시간이 소요되는 경우
2. 동일한 항목 여러 개가 매우 빠르게 반복되어 로딩되는 경우
1번에 해당하신다면 긴 로딩이 소요된 항목의 Name, Type, Size, Time 값을 기재해주시고, 2번에 해당하신다면 반복되어 로딩되는 항목의 Name, Type, Size 값만 기재해주시면 조언 드리겠습니다.
안녕하세요~ php공부중인 학생입니다. 잘 따라해 봤는데용
혹시 f5로 새로고침을 하게 되면 채팅 내용이 다 사라지는데, 새로고침을 해도 내용이 남아있게 하려면 어느 부분을 어떻게 수정해 주면 되는지 궁금합니다.
라라님, 안녕하세요.
제가 말씀하신 내용을 구현한다면, 변수 finalDate를 초기화할 때 현재 시각의 n분 전 정도로 설정할 것 같습니다. 해당 채팅방에 주고받았던 내용 전체를 다 남기게 할 수도 있겠습니다만, 그렇게 되면 시간이 흐르면서 화면에 표시될 내용은 점점 많아지게 되어 끝내는 브라우저가 처리하기 버거운 정도의 양이 되겠지요.
안녕하십니까? 좋은 글을 남겨주셔서 정말 감사한 마음으로 글을 보고 있습니다.
작성해주신 글을 보면서 작업중 궁금한 사항이 있어 죄송한 마음으로 댓글을 남겨봅니다.
php로 롱폴링 작업시에 동시에 2개의 request가 반영되지 않는 문제가 발생하고 있습니다.
즉, 한개의 무한루프가 실행하고 있을때, 작성 글을 남기면 작성글은 입력이 되지 않고, 루프가 끝난 다음에야 작성글이 입력되는 현상입니다.
위 댓글중 2020년 12월 08일에 작성하신 분도 저와 같은 현상이 아닐까 싶습니다.
즉, 새로고침을 해도 무한 루프가 중단되지 않고, 무한 루프 중에는 다른 request가 있어도 무한루프만 계속 실행되는 현상입니다.
while문을 for문으로 바꾸어 10초 정도만 loop를 실행시키면 10초가 끝난 다음에야 그 사이에 있었던 request를 실행시키는 상황입니다.
개발자 도구에서 확인해보아도, 다른 문제라기 보다는 loop를 돌리고 있는 페이지만 pending 상태이고, 그 다음에는 어떠한 request를 해도 해당 loop가 끝난 상태에만 다음 request를 실행하는 것입니다.(그래서 무한루프로 실행하면 새로고침을 해도 계속 무한루프가 돌아가서 페이지가 실행되지 않는 것입니다.)
set_time_limit(0); 을 해도 문제는 동일하고요.
현재 nginx 서버를 사용하고 있는데, 서버의 문제일까요? 아니면 다른 문제가 있는 것일까요?
1주일 가까이 해당 문제때문에 잠을 못이루고 있습니다. 혹시 알고 계신 부분이 있으시면 아주 작은 힌트라도 주시면 너무 감사하겠습니다.
초면에 이렇게 부탁 드려서 너무 죄송합니다.
델리카토님, 안녕하세요. 글 남겨주셔서 감사합니다.
기재해주신 내용만을 토대로 이해한다면 proc.php로의 Ajax 통신이 시작되어 response가 돌아오기 전에는, 글을 작성해도 write.php으로의 Ajax 통신이 시작하지 않는 것으로 풀이됩니다.
이처럼 Ajax 통신이 시작조차 되지 않는 경우라면, 대게는 프론트엔드의 문제를 의심하는 것이 순리입니다. 백엔드에 문제의 원인이 있다면, write.php로의 Ajax 통신 자체는 시작되고, 통신 이후의 처리에서 문제가 발생하여야만 하기 때문입니다.
그러나 제 경우에는 문제재현에 실패하였습니다. 제가 소스코드와 DB를 세팅하여, 익스플로러, 크롬, 오페라, 웨일 브라우저 4개를 띄워서 테스트해본 결과로는, 어느 브라우저에서도 말씀하신 문제를 재현하지는 못하였습니다.
혹시 Github 저장소 등에 올려놓으신 소스파일이 있다면, 살펴볼 수 있도록 하겠습니다. 즉각적으로 도움이 되지 못하여 죄송합니다.
1대1 채팅을 구현하고자 합니다.
실시간일 필요는 없고, 제가 텍스트를 쳤을 때, 접속중이면 그때만 실시간 처럼 보여도 되거든요. 약간 챗봇 같은 느낌으로? 텍스트나, 파일첨부 등을 할 수 있는 채팅을 만드려고 하는데
내용을 읽어봤을 때, 롱폴링이 괜찮아 보이는데 글쓴이께서는 어떻게 생각하시는지 궁금합니다.
bathingape님, 안녕하세요. 글 남겨주셔서 감사합니다.
먼저 말씀하신 상황에서는 롱폴링이 적합한 선택으로 보여집니다. 챗봇도 사용자의 행동에 대한 반응시간은 짧은 것이 사용성에 큰 영향을 주기 때문에, 실시간성이 떨어지는 Ajax 폴링보다는 롱폴링이 적합하다고 생각됩니다.
다만 이 글은 2017년도에 쓰인 글이고, 지금은 HTML5 스펙 중 하나인 웹소켓이라는 좋은 대안도 있습니다. 만약 웹소켓 서버를 별도로 구축가능하시다면 웹소켓을 고려해보시는 것도 좋습니다. 다만 웹서버 외에 별도 서버구축은 어려운 상황이시라면, 여전히 롱폴링도 대안이 될 수 있다고 생각되니 참고하여 주시면 감사드리겠습니다.
혹시 이거 롤폴링 채팅 쓰고 있는데요~ 새로고침 누르면 채팅기록이 사라지는데 db에 저장하면서 채팅이 안지워지게 하던지 시간이 지나면 사라지게 하려면 어떻게 해야하나요??