Websocket을 활용한 채팅 서비스 설계

Websocket을 활용한 채팅 서비스 설계

1. 들어가며

저는 여러 사람이 하나의 대화방에 모여 메시지를 주고받고 사진을 공유하는 실시간 채팅 서비스를 개발하고 있습니다. 흔히 사용하시는 메신저처럼, 새로고침을 누르지 않아도 상대방이 보낸 메시지가 즉시 내 화면에 나타나는 그런 서비스입니다.

실시간 채팅을 만들 때 가장 먼저 부딪히는 고민은 “누군가 메시지를 보냈을 때, 같은 방에 있는 나머지 사람들에게 어떻게 즉시 알려줄 것인가”입니다. 한 방에 열 명이 있고 그중 한 명이 메시지를 보내면, 나머지 아홉 명의 화면에는 거의 동시에 그 메시지가 나타나야 합니다.

가장 단순하게 떠올릴 수 있는 방법은 “메시지 본문을 그대로 아홉 명에게 실시간으로 쏘아주는 것”입니다. 하지만 저희는 이 방식을 선택하지 않았습니다. 대신 메시지 본문 대신 “무슨 일이 일어났다”는 가벼운 알림만 보내고, 실제 내용은 클라이언트가 다시 조회하도록 설계했습니다.

이 글에서는 왜 이런 선택을 했는지, 어떻게 적용했는지, 그 과정에서 어떤 문제를 만나고 해결했는지를 공유하고자 합니다.

2. 그 전에, 웹소켓이란 무엇일까요

본격적인 이야기에 앞서, 실시간 통신의 바탕이 되는 웹소켓(WebSocket)을 잠깐 짚고 넘어가겠습니다.

우리가 평소에 웹사이트를 사용할 때 쓰는 일반적인 통신 방식은 편지를 주고받는 것과 비슷합니다. 내가 궁금한 것이 있을 때마다 서버에게 편지, 즉 요청을 보내고, 서버는 그에 대한 답장, 즉 응답을 보내줍니다. 그리고 한 번 주고받으면 그 대화는 거기서 끝납니다. 다음에 또 궁금한 것이 생기면 새로 편지를 써서 보내야 합니다.

이 방식의 한계는 서버가 먼저 나에게 말을 걸 수 없다는 점입니다. 내가 묻지 않으면 서버는 새로운 소식이 생겨도 알려줄 방법이 없습니다.

그래서 실시간 채팅에서 이 방식을 쓰려면, 클라이언트가 “새 메시지 있나요?”라고 끊임없이 물어봐야 합니다. 마치 택배를 기다리며 1분마다 현관문을 열어보는 것과 같습니다. 새 메시지가 없어도 계속 물어봐야 하니 낭비가 크고, 물어보는 간격만큼 지연도 생깁니다.

반면 웹소켓은 전화 통화에 가깝습니다. 한 번 전화를 걸어 연결되면, 끊기 전까지는 그 통화가 계속 유지됩니다. 그 상태에서는 내가 먼저 말할 수도 있고, 상대방이 먼저 말할 수도 있습니다. 즉 서버가 새로운 소식이 생겼을 때 클라이언트가 묻지 않아도 먼저 알려줄 수 있는 것입니다.

이렇게 연결을 계속 열어둔 채 양쪽이 자유롭게 데이터를 주고받을 수 있는 통신 방식이 바로 웹소켓이며, 실시간 채팅처럼 “서버가 먼저 사용자에게 알려줘야 하는” 서비스에 잘 맞습니다.

저희는 이 웹소켓 위에, “어떤 주소로 보내고 어떤 주소를 구독한다”는 약속을 표준화한 STOMP라는 메시징 규약을 얹어 사용했습니다. 전화선이 깔려 있다면, STOMP는 “누구에게 어떤 내용을 어떻게 전달할지”에 대한 통화 예절 같은 것이라고 생각하시면 됩니다.

3. 기술 선택: 본문 대신 “알림”만 보냅니다

3.1. 연결할 때 신원부터 확인합니다

채팅 내용은 민감한 정보일 수 있으므로, 아무나 연결하게 둘 수는 없습니다. 그래서 웹소켓 연결이 처음 맺어지는 순간에 사용자의 토큰을 검사하도록 했습니다. 토큰이 없거나 형식이 올바르지 않거나, 인증 서버를 통한 검증을 통과하지 못하면 그 연결은 곧바로 차단됩니다.

여기서 한 가지 설계상의 선택이 있었습니다. 일반적인 요청은 보통 서버 앞단의 보안 필터에서 인증을 막습니다. 하지만 웹소켓은 한 번 연결되면 계속 유지되는 통로이기 때문에, 매 순간 검사하기보다는 연결을 맺는 바로 그 순간에 한 번 확실히 검사하는 편이 더 자연스럽고 효율적이었습니다.

앞서 비유한 전화 통화로 치면, 통화를 시작할 때 상대가 누구인지 확인하고 연결하는 것과 같습니다.

3.2. 핵심 결정: 본문이 아니라 “알림”을 보냅니다

이제 가장 중요한 설계 결정입니다. 누군가 메시지를 보냈을 때, 저희는 그 메시지 본문 전체를 같은 방 사람들에게 뿌리지 않습니다. 대신 “이 방에 새 메시지가 등록되었다”는 신호만 보냅니다. 그 신호를 받은 클라이언트는 그제서야 평소처럼 일반 조회 방식으로 새 메시지를 가져와 화면에 표시합니다.

비유하자면 일반적인 메신저의 푸시 알림과 비슷합니다. 푸시 알림 자체에는 “○○님이 메시지를 보냈습니다”라는 짧은 신호만 담겨 있고, 우리가 앱을 열면 그때 실제 대화 내용을 불러오는 것과 같은 방식입니다.

처음에는 “이왕 실시간으로 보낼 거면 본문까지 같이 보내면 한 번에 끝나지 않을까?”라고 생각할 수 있습니다. 저 역시 직관적으로는 그 편이 단순해 보였습니다. 하지만 알림만 보내는 방식에는 분명한 장점이 있었고, 검토 끝에 이 방식을 택했습니다.

첫째, 소켓으로 흐르는 데이터가 가볍습니다. 사진이 첨부된 큰 메시지든 긴 텍스트든, 소켓으로는 “새 메시지 있음”이라는 작은 신호만 흐릅니다. 덕분에 많은 사람이 동시에 대화할 때도 실시간 통로에 부담이 적습니다.

둘째, 데이터의 정확성과 권한 처리를 한 경로로 모을 수 있습니다. 실제 데이터는 항상 검증된 조회 경로를 통해서만 내려갑니다. 만약 소켓으로도 본문을 내려보낸다면, 소켓 경로와 일반 조회 경로 두 곳에서 서로 다른 형태의 데이터가 나갈 수 있어 관리가 복잡해지고, 권한 검사 같은 로직도 두 군데에서 중복으로 신경 써야 합니다.

셋째, 클라이언트가 필요한 것만 가져갈 수 있습니다. 알림에는 “무엇이 바뀌었는지”만 담기므로, 클라이언트는 자신이 현재 보고 있는 화면 상태에 맞춰 꼭 필요한 데이터만 다시 조회하면 됩니다. 예컨대 그 방을 보고 있지 않다면 메시지 본문은 가져오지 않고 “안 읽은 표시”만 갱신할 수도 있습니다.

4. 적용 과정: 사건의 종류를 정의하다

4.1. 알림에는 “무엇이 일어났는지”만 담습니다

알림으로 주고받는 데이터에는 메시지 본문 전체가 들어가지 않습니다. 대신 어느 방에서, 어떤 대상에 대해, 무슨 일이 일어났는지 정도의 정보만 담깁니다. 구체적으로는 보낸 사람, 방 식별자, 영향을 받은 대상의 식별자, 그리고 사건의 종류 정도입니다.

여기서 핵심은 “사건의 종류”입니다. 저희는 채팅에서 발생할 수 있는 사건들을 미리 명확하게 분류해 두었습니다. 예를 들면 다음과 같은 것들입니다.

  • 새 메시지가 등록되는 경우
  • 메시지를 읽은 경우
  • 메시지를 수정하거나 삭제하는 경우
  • 메시지를 고정하거나, 답글을 달거나, 이모지로 반응하는 경우
  • 방에 초대하거나, 방을 만들거나 삭제하거나, 방을 나가는 경우

이렇게 사건의 종류를 십여 가지로 나누어 정해진 값으로만 표현하도록 했습니다. 이런 방식의 장점은, 서버와 클라이언트가 “이런 사건이 일어나면 이렇게 처리한다”는 약속을 명확하게 공유할 수 있다는 점입니다.

알림에 임의의 문자열을 담는 대신 미리 정의된 사건 종류만 사용하므로, 오타나 누락으로 인한 혼선을 줄일 수 있습니다.

대표적인 흐름은 이렇습니다. 한 사용자가 메시지를 보내면, 서버는 같은 방의 사람들에게 “새 메시지가 등록되었다”는 사건 종류와 해당 방 식별자, 새 메시지 식별자만 담아 알림을 보냅니다. 알림을 받은 클라이언트는 “아, 이 방에 새 메시지가 생겼구나” 하고 그 방의 메시지를 조회해 화면을 갱신합니다.

4.2. “읽음” 처리도 같은 알림으로 흐릅니다

이 알림 구조의 장점은 새 메시지뿐 아니라 다른 상태 변화에도 똑같이 적용된다는 점입니다. 예를 들어 사용자가 메시지를 읽으면, 서버는 그 사용자가 마지막으로 읽은 위치를 갱신한 뒤, 같은 방에 “메시지를 읽었다”는 알림을 보냅니다. 그러면 다른 사람들의 화면에서 “읽음” 표시가 같은 메커니즘으로 실시간 갱신됩니다.

여기서 한 가지 신경 쓴 부분은, 이미 읽은 위치보다 더 뒤를 읽었을 때만 갱신하고 알림을 보낸다는 점입니다. 사용자가 이전 메시지를 다시 보더라도 읽음 위치가 거꾸로 돌아가지 않도록 하기 위함입니다.

결과적으로 새 메시지든, 메시지 수정이든, 읽음이든, 모든 변화가 “알림을 보내고 → 클라이언트가 다시 조회한다”는 하나의 일관된 패턴으로 통일되었습니다. 새로운 종류의 사건이 생기더라도 같은 틀에 끼워 넣기만 하면 되므로, 기능을 넓혀 가기에도 수월했습니다.

5. 온라인 사용자에게만 보냅니다

소켓 알림은 지금 접속해서 연결을 열어둔 사람에게만 의미가 있습니다. 앞서 비유한 전화 통화로 치면, 전화를 끊어 둔 사람에게는 아무리 말을 걸어도 들리지 않는 것과 같습니다. 그래서 알림을 보내기 직전에, 대상이 되는 각 사용자가 현재 접속 중인지를 먼저 확인합니다.

서버는 누가 현재 연결되어 있는지에 대한 정보를 가지고 있어서, 알림을 보낼 때 그 목록을 조회해 접속 중인 사용자에게만 신호를 전달합니다. 접속하지 않은 사용자에게는 소켓으로 보내봐야 받을 사람이 없으므로, 불필요하게 신호를 흘리지 않도록 거르는 것입니다.

이렇게 “보낼 가치가 있는 대상”을 미리 추려내는 과정은, 사람이 많은 방일수록 효과가 큽니다. 한 방에 사람이 많아도 실제로 접속 중인 사람은 일부일 수 있는데, 접속자만 골라 보냄으로써 헛된 전송을 줄일 수 있기 때문입니다.

6. 겪은 문제와 해결: 방을 나갈 때는 누구에게 알릴 것인가

알림 구조를 적용하면서 실제로 부딪혔던 문제 하나를 공유하겠습니다.

대부분의 알림은 “이 방의 현재 멤버 전원”에게 보내면 됩니다. 그래서 처음에는 알림을 보낼 때마다 방 식별자로 현재 멤버 목록을 조회해서 그들에게 보내도록 만들었습니다. 새 메시지, 메시지 수정, 읽음 처리 등은 모두 이 방식으로 잘 동작했습니다.

그런데 방을 삭제하거나 방을 나가는 경우에는 문제가 생겼습니다. 방이 삭제되거나 한사용자가 방을 나가면, 그 처리가 끝난 시점에는 이미 멤버 정보가 정리되어 사라진 뒤입니다. 그 상태에서 “현재 멤버 목록”을 조회하면, 정작 “당신이 방에서 나갔습니다”, “이 방이 삭제되었습니다”를 알려줘야 할 대상을 찾지 못하는 상황이 발생했습니다.

알림을 보내려고 보니 받을 사람이 이미 목록에서 빠져 있었던 것입니다.

원인을 따져 보니, 알림 대상을 조회하는 시점과 멤버 정보가 사라지는 시점이 어긋난 것이 문제였습니다. 평소에는 “지금 이 방에 누가 있는가”를 그때그때 조회해도 충분했지만, 방을 떠나거나 없애는 사건은 그 조회 결과 자체를 바꿔 버리는 사건이었기 때문입니다.

해결을 위해, 사건의 종류에 따라 알림 대상을 정하는 방식을 둘로 나눴습니다. 방 삭제나 방 나가기처럼 멤버 정보가 정리되는 사건의 경우에는, 멤버가 사라지기 전에 미리 확보해 둔 대상 목록을 그대로 사용해 알림을 보내도록 했습니다. 반대로 새 메시지나 수정 같은 일반적인 사건은 기존처럼 현재 멤버를 조회해서 보냅니다.

즉 “현재 상태를 조회해서 보낼 것인가, 사라지기 전에 붙잡아 둔 대상에게 보낼 것인가”를 사건의 성격에 따라 갈라 준 것입니다.

이 경험을 통해, “알림을 누구에게 보낼지”를 결정하는 일이 단순히 현재 상태만 보는 것으로는 부족하다는 점을 배웠습니다. 사건의 성격에 따라, 데이터가 사라지기 전과 후 중 어느 시점의 정보를 써야 하는지를 함께 고려해야 한다는 것입니다.

앞서 사건의 종류를 명확하게 분류해 둔 덕분에, 이렇게 사건별로 다른 처리를 깔끔하게 구분할 수 있었던 점도 큰 도움이 되었습니다.

7. 결과와 회고

이 설계를 적용한 결과, 소켓으로는 “무슨 일이 일어났다”는 가벼운 신호만 흐르고, 실제 데이터의 정확성은 검증된 조회 경로가 책임지는 구조로 정리되었습니다. 알림의 종류가 늘어나도, 새로운 사건을 미리 정의해 둔 분류에 추가하고 같은 전송 방식을 재사용하면 되므로 확장이 단순합니다.

가장 크게 배운 점은, “실시간 = 모든 데이터를 실시간으로 밀어 넣기”라는 생각이 항상 옳은 것은 아니라는 사실입니다. 오히려 “변화는 빠르게 알리되, 데이터는 신뢰할 수 있는 경로로 당겨오게 한다”는 접근이 더 단순하고 견고했습니다. 실시간 통로와 일반 조회 경로가 각자 잘하는 일에 집중하도록 역할을 나눈 셈입니다.

물론 아직 개선할 부분도 남아 있습니다. 예를 들어 현재 접속자 정보를 판단하는 방식이 한 대의 서버를 전제로 하고 있어, 서버를 여러 대로 늘려 운영할 경우에는 접속 정보를 서버 간에 공유하는 구조가 추가로 필요합니다.

실시간 채팅이라는, 겉보기에는 단순해 보이는 기능 하나에도 “무엇을, 언제, 누구에게 보낼 것인가”라는 결정들이 촘촘하게 얽혀 있다는 것을 직접 만들어 보며 체감할 수 있었습니다. 이 글이 비슷한 고민을 하시는 분들께 작은 참고가 되었으면 합니다.

messi

Site footer