소켓 응용 4/5 · SCALE

서버가 두 대면
방도 둘로 갈라집니다.

각 서버는 자신에게 연결된 사용자만 압니다. 브로커로 실시간 이벤트를 퍼뜨리고, 저장소로 놓친 이벤트를 복구하는 역할 분리를 봅니다.

이 편의 핵심 질문다른 서버에 붙은 사용자에게 어떻게 전달할까?브로커로 퍼뜨리고
저장소로 복구

나쁜 결과와 안전한 결과를 비교하세요

한 채팅방을 서버 A와 B에 걸쳐 연결합니다

보는 순서서버 분리 선택A에서 발행B 수신 확인

  1. 1분산 연결
  2. 2발행
  3. 3단절
  4. 4결론
실험 시작을 누르면 첫 장면부터 움직입니다방이 둘로 분리
민지Server A 연결
준호Server B 연결
INSTANCE A소켓 서버 Aroom-7 · 민지
공유 경로연결 없음각 서버 메모리만 사용
INSTANCE B소켓 서버 Broom-7 · 준호

room-7A ┆ 끊어진 방 ┆ BLOCAL ONLY

서버끼리 분리 시나리오입니다. 실험 시작을 눌러 네 장면을 따라가세요.

시작 전

화면에서 코드로 옮길 때

이 편의 원리 세 가지

01연결 목록은 로컬 상태입니다.

서버 A의 메모리는 서버 B에 연결된 사용자를 알지 못합니다.

02브로커는 지금 퍼뜨립니다.

온라인 서버 인스턴스에 토픽 이벤트를 fan-out합니다.

03저장소는 나중을 책임집니다.

끊긴 사용자가 마지막 offset 이후 이벤트를 다시 읽게 합니다.

구현 체크리스트

최소 네 조각으로 시작하세요.

데모의 시각 요소를 실제 프로토콜 필드와 런타임 동작으로 옮기면 됩니다.

  1. 1공통 room ID모든 서버가 같은 규칙으로 만드는 토픽 이름
  2. 2Pub/Sub 브로커현재 살아 있는 서버에 빠르게 이벤트 전달
  3. 3내구성 저장소고유 event ID와 순서를 유지하는 로그
  4. 4중복 허용 소비자재생 이벤트가 겹쳐도 한 번만 화면에 반영
이 데모는 네트워크 동작을 단계별로 보여주는 개념 시뮬레이션입니다.

Redis Pub/Sub과 같은 실시간 브로커만으로는 오프라인 구독자의 누락을 복구하지 못합니다. 저장과 재생은 별도로 설계하세요.

이 데모에 대하여

한 채팅방을 서버 두 대에 걸쳐 놓습니다

서버가 한 대일 때는 방에 있는 사람 목록이 그 서버의 메모리에 다 있습니다. 그런데 서버를 한 대 더 늘리는 순간, 각 서버는 자기에게 붙은 사람만 알게 됩니다. 이 화면은 그 상태에서 한쪽 서버에 붙은 사람이 보낸 메시지가 다른 쪽 서버에 붙은 사람에게 닿지 않는 상황을 재현하고, 그것을 잇는 두 가지 장치가 각각 무엇을 담당하는지 나눠 보여줍니다.

  1. 서버가 분리된 설계를 고르고 A 쪽에서 메시지를 보냅니다.
  2. A 에 붙은 사람은 받고 B 에 붙은 사람은 못 받는지 확인합니다.
  3. 메시지가 어디까지 갔다가 멈추는지 경로 표시를 봅니다.
  4. 브로커를 켠 설계로 바꿔 같은 메시지를 보냅니다.
  5. 메시지가 브로커를 거쳐 반대쪽 서버로 퍼지는 단계를 따라갑니다.
  6. 이번에는 한쪽 사용자를 끊어 두고 메시지를 보냅니다.
  7. 다시 접속했을 때 브로커만으로 복구되는지 확인합니다.
  8. 저장소가 관여하는 단계가 따로 있는지 봅니다.
  9. 브로커만 켰을 때와 저장소까지 켰을 때의 결과를 비교합니다.
  10. 메시지가 지나가는 지점을 순서대로 적어 봅니다.
  11. 두 서버가 서로를 직접 부르는 단계가 있는지 확인합니다.

두 장치의 역할이 다릅니다

01

연결 목록은 서버마다 따로입니다.

각 서버는 자기에게 연결된 사용자만 알고 있습니다. 이것은 설계 실수가 아니라 연결이라는 것이 특정 서버와 맺어지는 관계이기 때문에 생기는 당연한 결과입니다. 서버를 늘리는 순간 방이 갈라지는 문제는 규모를 키울 때 반드시 만나게 되며, 서버 한 대로 잘 돌던 코드가 두 대에서 깨지는 대표적인 이유입니다.

02

브로커는 지금 있는 사람에게 퍼뜨립니다.

브로커를 켜면 한쪽 서버가 받은 메시지를 다른 서버들에게 전달하고, 각 서버가 자기에게 붙은 사람에게 내보냅니다. 이 방식은 빠르지만 그 순간 연결돼 있는 대상에게만 닿습니다. 지금 듣고 있지 않은 사람을 위해 메시지를 들고 있어 주지는 않는다는 점이 이 장치의 성격입니다.

03

저장소는 나중에 온 사람을 담당합니다.

끊겨 있던 사용자가 다시 붙었을 때 그동안의 메시지를 받으려면 어딘가에 남아 있어야 합니다. 그 역할은 브로커가 아니라 저장소가 맡습니다. 화면에서 두 장치를 따로 켜고 끌 수 있게 만든 이유는, 하나만으로는 해결되지 않는다는 점을 직접 확인하게 하기 위해서입니다.

04

실시간 전달과 나중 복구는 다른 문제입니다.

많이 쓰는 브로커를 붙여 놓고 메시지 유실이 해결됐다고 생각하기 쉬운데, 그 둘은 다루는 시점이 다릅니다. 지금 퍼뜨리는 일과 나중에 다시 읽을 수 있게 하는 일을 같은 장치에 맡기려 하면 어느 쪽도 제대로 되지 않습니다. 이 구분이 이 화면에서 가장 중요한 내용입니다.

05

같은 방 이름을 모든 서버가 알아야 합니다.

서버들이 메시지를 주고받으려면 어느 방에 대한 이야기인지 같은 규칙으로 이름을 만들어야 합니다. 서버마다 다른 방식으로 이름을 지으면 브로커를 붙여도 서로의 메시지를 알아보지 못합니다. 화면에서는 이 부분을 이미 맞춰진 것으로 두고 있습니다.

06

다시 읽으면 겹칠 수 있습니다.

저장소에서 놓친 메시지를 다시 읽어 올 때 이미 받은 것이 섞여 들어올 수 있습니다. 그래서 받는 쪽은 같은 메시지가 두 번 와도 화면에 한 번만 반영되도록 만들어야 합니다. 메시지마다 고유한 번호가 필요한 이유이고, 그 번호는 전달을 다룬 첫 편에서 본 것과 같은 역할을 합니다.

07

경로를 위치로 표시합니다.

메시지가 지금 어느 지점에 있는지를 값으로 들고 있어서, 실패한 경우에도 어디까지 갔다가 멈췄는지 화면에 남습니다. 전달 실패를 결과로만 표시하면 무엇을 고쳐야 할지 알 수 없는데, 멈춘 지점이 곧 빠진 장치를 가리킵니다.

08

서버 한 대에서는 드러나지 않습니다.

이 문제의 성가신 점은 개발 환경에서 재현되지 않는다는 것입니다. 서버를 한 대만 띄우고 시험하면 모든 사용자가 같은 메모리를 공유하므로 아무 장치 없이도 잘 동작합니다. 실제로 서버를 늘린 뒤에야 드러나고, 그 시점에는 이미 코드가 그 전제 위에 쌓여 있습니다. 규모를 키울 계획이 있다면 처음부터 서버 간 전달을 가정하고 짜는 편이 나은 이유입니다.

09

붙는 서버를 고정하는 방법도 있습니다.

같은 방의 사용자를 항상 같은 서버로 보내면 이 문제를 피할 수 있습니다. 다만 그러면 특정 서버에 부하가 몰리고, 그 서버가 죽었을 때 그 방 전체가 영향을 받습니다. 문제를 없애는 대신 다른 제약을 얻는 선택이라, 방의 크기와 서버 구성에 따라 어느 쪽이 나은지 달라집니다. 이 화면은 고정하지 않는 쪽을 전제로 합니다.

구조를 보여주는 최소 모델입니다.

브라우저는 어떤 서버나 브로커에도 연결하지 않고 정해 둔 장면을 재생합니다. 서버 두 대와 사용자 둘은 문제가 드러나는 최소 크기이며 실제 규모와는 무관합니다. 실제 설계에서는 여기 없는 것이 많은데, 저장소에 메시지를 얼마나 오래 둘지, 사용자마다 어디까지 읽었는지를 어떻게 기록할지, 브로커 자체가 죽었을 때 어떻게 할지, 서버가 늘어나고 줄어들 때 연결을 어떻게 재배치할지, 방 하나에 사람이 아주 많을 때 퍼뜨리는 비용을 어떻게 감당할지가 모두 별도의 문제입니다. 또한 브로커와 저장소를 각각 무엇으로 고를지에 따라 보장되는 성질이 크게 다르므로, 이 화면의 두 상자를 특정 제품과 바로 대응시켜 생각하면 안 됩니다. 같은 제품이 상황에 따라 두 역할을 모두 할 수도 있고 어느 쪽도 제대로 못 할 수도 있어서, 도구 이름보다 그 도구가 무엇을 보장하는지를 확인하는 편이 안전합니다. 특히 메시지를 잃지 않는다는 설명이 어느 조건에서 성립하는지, 서버가 갑자기 죽었을 때도 유지되는지를 따로 확인해야 합니다.

참고 자료Redis — Pub/Sub 문서RFC 6455 — WebSocket 프로토콜

실험을 마쳤다면