웹 기반 채팅방
동아리에는 현재 대충 그지같이 만들어놓은 채팅방이 있다. 내가 만들었다 orz
코드 라인별 분석은 당연히 의미가 없을 것 같고, 핵심적인 부분에 대해 간략히 이야기해보려고 한다.
채팅방을 만들기 위해서 고민해야할 것은 다음과 같다.
- 메세지 구조
- 동기화 방법
동기 or 비동기
웹 프로그래밍은 다른 네트워크 프로그래밍과 약간은 다르게, 서버와 클라이언트가 통신하는 메세지 구조에 대해 고민하지 않는 경우가 많다. 왜냐하면 어차피 웹 서버는 HTML을 만들어서 클라이언트에게 반환하면, 클라이언트는 이를 rendering해서 보여주기만 하니까.
하지만 Ajax라는게 나오고, 조금이나마 웹이란 세상에서도 메세지를 신경써야되는 시대가 왔다. 전체 페이지를 HTML로 다같이(동기적으로) 갱신하는게 아니라, 부분적 정보만을 요청하여 받은 데이터로 클라이언트 영역의 부분만을 갱신하는, 즉 전체 페이지가 각 영역별로 따로(비동기적으로) 갱신하는 구조가 된 것이다.
전자의 방식에서는 클라이언트 FORM 페이지에서 웹 서버로 데이터를 보내는 POST 단계와, 서버의 스크립트 언어로 FORM DATA를 처리한 후 다시 클라이언트에게 HTML을 보내주는 BACK 단계를 합쳐서 POSTBACK이란 용어가 나오게 되었다.
브라우저의 현재 URL 값을 변경하여 페이지 전체의 데이터를 보내는 경우에는 딱히 고민하지 않아도 될 것이 메세지 구조였다. 어차피 서버에서는 HTML을 반환시키면 그만이니까. 우리에게 친숙한 언어의 함수로보면 아래와 같다.
HTML process_in_server (FORMDATA data) {
return make_html (process_formdata (data));
}
void request_in_client (void) {
render_in_client (process_in_server (get_form_data ()));
}
그렇다면 비동기적인 방법으로 갱신하는 건 어떤 것일까?
MESSAGE process_in_server_async (FORMDATA data) {
return make_message (process_formdata (data));
}
void request_in_client (UPDATEAREA area) {
MESSAGE msg = process_in_server (get_form_data ());
HTML html = translate_message_result (msg);
update_in_client_area (html);
}
서버의 코드는 크게 변한게 없다. 다만 HTML을 반환하는게 아니라 message 형식으로 반환한다는 것이다.
- Ajax(Asynchronous Javascript and XML)를 예를 들어본다면 이름에서 알 수 있듯이 message 형식을 XML으로도 할 수 있는 것이다.
- 아니면 요즘 대세인 작고 가벼운 JSON(Javascript Object Notation)을 쓰는 것도 하나의 방법이고,
- 직접 Serializer와 Deserializer(Parser)를 구현한다면 자신이 정의한 message 형식으로 반환해도 된다.
- 아예 서버 쪽에서 비동기 요청에 대해 HTML을 만들어서 반환하면, 클라이언트에서는 그 HTML을 바로 특정 영역(update area)에 반영시켜도 좋을 것이다.
요약하면,
- 동기적 갱신에 대해 클라이언트는 어차피 전체 페이지가 모두 변경되니 변경될 지역을 신경 쓸 필요가 없지만,
- 비동기적 갱신에 대해서는 갱신해야할 부분을 지정해서, 서버가 반환한 값으로부터 HTML 을 구성하여 직접 갱신해주어야 하는 작업이 추가적으로 필요하다는 것.
따라서 전자의 방식이 단순 Server-Side 프로그래밍만으로 해결될 문제라면, 후자의 방식은 클라이언트에서 화면 갱신을 위한 코드가 필요하기 때문에 Client-Side 프로그래밍이 반드시 필요하다는 것이다.
ajax를 왜 쓸까
데이터 통신량이 아까워서 하는 것도 맞기는 한데, 데스크탑의 wired 환경에서는 정답은 아닌 것 같다.
전체 화면을 100이라고 했을 때 특정 동작으로 갱신되어야 하는 화면이 대략 10 정도만을 차지한다고 하자. 그 요청을 할 때마다 화면이 전체 갱신되어 깜빡거린다면 사용자로써는 그 대기 시간 기다리는 것도 짜증나고 여러 모로 좋지 않은 경험을 하게 될 것이다. 따라서 보다 미려한 환경을 구축하기 위해 쓴다고 보면 되겠다.
덕분에 페이지에 Ajax 방법을 도입할 때는 신경써줘야 할 것들이 무지하게 많아지지만 본 글의 목적에서 벗어나므로 이 글에서 서술하지는 않겠다.
메세지 구조에는 XML과 JSON이 있다고 하자. 다른 것도 많겠지만 생각하기 귀찮으니까.
- XML은 사실상(defecto) 업계상 표준이라고 한다. 따라서 다양한 플랫폼으로의 이식성을 고려한다면 XML을 쓰는게 좋을지도 모르겠지만, 다 옛날 말이 된 것 같다.
- 표기법이 간단하고 쓰기도 쉬운 JSON이 대세가 된 것 같다. JSON parser가 없는 플랫폼이 없을 정도고, 직접 Parser 만드는 것도 어렵지 않으니까.
웹 기반으로 채팅방을 구성함에 있어 가장 곤란한 것은 비동기 알림(Server Push)이다.
위에서 동기네 비동기네 했던 이야기는 모두 클라이언트의 이야기.
웹은 그 구조상 클라이언트가 요청을 하면 서버가 그 결과를 반환하게 되어있다. HTTP 프로토콜 자체가 그리 구성되어있는 것이고, 보안상 내가 요청하지도 않았는데 서버가 알아서 알려줄 수도(비동기 알림) 없는 것이다. (그러면 서버가 클라이언트를 해킹하는 개념이 되겠지)
채팅의 경우는 네트워크 프로그래밍의 기초 예제로 많이 나온다. 클라이언트에서 서버로, 서버에서 클라이언트로 쌍방 데이터를 주고 받는 가장 간단한 구조이기 때문이다.
웹은 태생부터 저 구조에 부합되지 못한다. 클라이언트에서 서버로 요청하는 것은 문제가 없지만, 서버에서 클라이언트로 데이터를 보낼 방법이 없기 때문이다.
(내가 채팅 메세지를 보낼 때만 요청 반환 값으로 다른 사람이 이야기한 것을 받아올 수 있다. 테트리스로 치자면, 내가 키를 누르기 전까지는 블록이 아래로 내려오지 않는 것과 같다)
이걸 해결하기 위한 가장 간단한 방법은,
- 무한 새로고침 (동기적 해결법)
- Ajax polling (비동기적 해결법)
- long-held HTTP Request (COMET)
- Web Socket
1번은 설명 안해도 알 것이고, 3, 4번은 여기서 설명할 내용은 아니다. 이 글에서는 2번에 대해서 설명하겠다.
polling
클라이언트에서 일정 시간 간격으로 서버에 ajax로 지속적인 요청을 한다. 서버는 해당 클라이언트가 갖고 있는 메세지 이후부터 새로 받은 메세지를 반환한다.
(물론 웹 서버는 Stateless 하니까 SESSION 등으로 각 클라이언트를 식별할 정보를 잘 갖고 있어야겠다.)
사용자 A와 B가 있다. A는 B의 말을 들어주는 중이고, B는 열심히 지 할 말을 하고 있다.
- B 컴퓨터의 웹 클라이언트는 떠드는 메세지를 서버에게 지속적으로 보낼(Request) 것이다. 이 메세지들은 서버에 차곡차곡 쌓아놔야한다.
- 그리고 A 컴퓨터의 웹 클라이언트가 이걸 받아가야한다. 이왕이면 봤던 것 이후로, 즉 내가 읽은 것 말고, 그 다음부터 B 가 떠든 내용을 받아오고 싶을 것이다. 따라서 A 클라이언트는 서버에게 내가 아까 본 이후로 B 가 얼마나 떠들었나를 요청(Request) 할 것이고, 서버는 그에 대한 응답 메시지를 A 클라이언트에게 보내면 A 클라이언트는 그걸 받아서 화면을 갱신할 것이다.
서버가 클라이언트에게 비동기 알림이 안되니까 매번 클라이언트가 직접 물어보고 가져오는 건데, 그 동안 서버에 얼마나 데이터가 쌓일지 모르니까, 서버는 클라이언트에게 보낼 데이터를 다 저장해두고 있어야 한다는 것. 이 부분이 이러한 방식의 프로그래밍을 어렵게 하는 요소다.
동아리 채팅방에서는 매우 단순 무식한 구조를 사용하고 있다.
- 각 클라이언트가 채팅 메세지를 서버로 보내면 서버는 무조건 모든 데이터를 다 데이터베이스에 집어 넣는다.
- 그리고 클라이언트가 서버에게 지금까지 쌓인 메세지를 보내달라고 요청하면, 그 데이터베이스에서 무조건 20개를 꺼내서 그걸 HTML 로 만들어서 반환한다. 그러면 클라이언트는 그 HTML 로 채팅 영역을 update 한다.
사람이 많이 들어오면 답이 없는 구조다. DB가 엄청난 병목 지점이 되기 때문이다.
(물론 데이터베이스에서 해당 채팅 메세지를 저장하는 테이블을 Memory Table 로 작성해놨지만 그래도 느리다)
그리고 클라이언트가 항상 최근 20개를 받아와서 자기 영역을 갱신한다.
바꿔 말하면 채팅방에 아무런 대화가 없어도 클라이언트는 계속해서 채팅 영역을 교체(update) 하면서 CPU 를 소모한다는 것이다.
(그나마 이 부분은 채팅 내용이 완전히 동일할 경우 클라이언트에서 교체하지 않도록 코드를 작성해놨지만, 그래도 네트워크 자원 소모는 지속적으로 이루어진다)
추후 누군가의 요청에 의해 동아리 친구가 스크롤바 기능을 구현해놨는데, 이게 메세지 구조를 뜯어 고친게 아니라 일단 서버로부터 20개를 HTML로 받아온 뒤 그걸 Parsing 해서 새로 추가된 부분만 채팅 영역에 삽입하는 방식으로 작성된 것이라 문제가 발생할 가능성이 많다.
(자바 스크립트의 문자열 비교 기능이 개판인건 아닌데 여기에 htmlentities가 끼면 제대로 동등 비교가 안되어서 채팅방에 계속 중복된 메세지가 추가되는 경우가 있다.)
근본적으로 저 문제를 해결하려면 각 메세지에 AUTO_INCREMENT
한 ID 값을 박아넣고, 요청할 때 최근에 받은 ID 값을 서버로 보내서 그 다음 데이터부터 받아와서 해당 영역에 추가(appendChild
) 해주는 방법을 사용하면 될 것이다.
그런데 저 방법은 내가 해봤는데 지금 우리 채팅방에 쓰면 안 될거다. 제대로 확인은 안해봤지만 글을 동시에 대량으로 보낼 경우 ID 값이 꼬이는지 제대로 클라이언트간 동기화가 안 되었던 것으로 기억한다.
따라서 해당 ID 값에 보정치를 고려하여 클라이언트에서 JSON 형태로 메시지를 받아서 ID 값에 대해 현재 클라이언트에 없는 부분만 삽입해주는 방법으로 구현하면 제일 깔끔할 것이고,
현재 채팅방의 가장 고질적인 문제인, 계속 켜놓으면 메모리 점유율이 너무 높아진다도 추가하는 채팅 로그 개수가 일정량이 넘어갔을 때 위에서부터 제거하면 해결될 문제다.
생각해보면, 채팅방을 Ajax 로 구현한다는 것 자체가 문제겠지. 당연히 WebSocket으로 개발하는게 맞겠지만 일부 브라우저 (특히 군대) 에서는 WebSocket 이 지원되지 않을 수 있기 때문에 WebSocket과 Ajax 방식 둘 다 유지해야한다는 이야기가 있어서 귀찮아서 그만 두었다.
게다가 WebSocket으로 작성하려면 WebSocket 서버와 웹 서버간의 SESSION 공유도 해주어야 하잖아?
[doodoori2]님의 이야기로는 데이터베이스에 SESSION을 넣어두고 WebSocket 서버에게 DB 접근 권한을 주어서 공유하라고 하는데, 그렇게되면 SESSION의 갱신에 대해 WebSocket이 능동적으로 알아챌 수 있는 방법이 없기 때문에 결국 DB Polling을 해야하는 사태가 벌어질 수도 있을 것 같다.
여러가지 해결책이 있겠지만 귀찮다는 핑계로 고민 중이다.
정리
본 글에서는 기존의 웹 통신 기반의 채팅방을 어떻게 구성하면 좋을지에 대해 써봤다. 코드가 없다고 아쉬워하는 사람이 있을지는 모르겠는데,
- 웹 서버 쪽 프로그래밍이야 Server-Side Script 로 각 클라이언트간 동기화 로직만 잘 짜주면 되니까 이건 그냥 코딩이고,
- 클라이언트 쪽 프로그래밍이라면 Ajax나 WebSocket 관련 라이브러리를 가져다 쓰는게 좋겠다.
난 전통적인 prototypejs 유저라서 잘 모르겠지만, 대세가 jQuery 라고 하니 그 쪽을 공부해보는 것도 좋겠다.