개인 공부용/ㅇㅅㅇ

알림 구현(2) 웹소켓 세션 다중 클라이언트, 다중 인스턴스 환경

pon9 2025. 4. 11. 18:04

개요

실시간 알림에 사용되는 웹 소켓 세션이 현재는 다중 클라이언트와, 다중 인스턴스 환경에서 취약점을 보이고있다.

1. pc로 접속한 뒤 모바일로 접속하면 pc환경에서의 세션은 끊기고,

2. 중앙화된 세션 관리 저장소가 없어서 요청마다 세션 상태가 다르다.

 

오늘은 이 두개를 보완해보장

 

 

다중 클라이언트 허용

현재 세션은 이렇게 HashMap<Long, Session> 형태로 딱 하나만 들어가게끔 설계되어있는데, 자료구조를 좀 바꿔서

한 Id에 대해 여러 웹소켓세션을 저장하도록 하면 된다.

이렇게 thread safe한 불변 리스트를 사용하도록 바꿔주고

websocket king client에서 두 개의 클라이언트를 켜놓고 실험해보면,
같은 알림을 동시에 받는 걸 확인할 수 있다~!

 

 

다중 인스턴스 환경

현재 websocket세션은 각 인스턴스의 메모리에만 저장되는데, 다중 인스턴스 환경에서 어떻게 세션을 공유할까?

처음에는 당연히 websocket 세션 정보를 redis같은 중앙 저장소에 저장해야 할 것 같았다.

예를 들어 유저a가 인스턴스1에 연결되어있으면 그 정보를 redis에 저장하고, 알림을 보내야 할 때 모든 인스턴스가 redis에서 세션을 꺼내와서 메세지를 전송하는 방식으로..

 

하지만 그렇게 구현할 수가 없었다. websocket session객체는 직렬화 할 수 없는 상태 기반의 객체고,

TCP 연결을 포함한 실제 연결 정보는 오직 연결된 인스턴스의 메모리에만 존재하기 때문이다. 즉, 다른 인스턴스가 그 세션을 공유해서 사용할 수 없다.

그럼 다중인스턴스 환경에선 websocket 기반의 실시간 알림 시스템을 대체 어떻게 설계해야 할까..

 

라고 고민을 하면서 찾아보니까,, 이미 구현을 다 해놓은 상태였다. 해결법은 redis pub/sub 이었다

websocket 세션은 각 인스턴스가 자기 메모리에서만 관리하고,

알림 메세지는 Redis pub/sub채널을 통해 모든 인스턴스에 동시에 브로드캐스트한다.

메세지를 받은 각 인스턴스는 "이 알림의 대상 유저가 나한테 연결되어 있는지" 를 확인하고, 있으면 메세지를 전송하고, 없으면 무시한다.

uml로 표시하면 이런 느낌이다.

핵심은 websocket 세션을 공유하지 않고, 메세지만 공유하는거다. 전송은 세션을 보유한 인스턴스만 담당한다.

그럼 정말 잘 동작하는지 도커로 다중인스턴스를 띄워서 확인해보자

 

 

실험

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: app
    restart: always
    ports:
      - "6060:6060"

  app2:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: app2
    restart: always
    ports:
      - "6061:6060"

도커로 어플리케이션 서버 두 개를 띄워서 실험해보자! 나머지 redis, kafka, mysql은 로컬호스트를 사용하도록 yml에서 host.docker.internal로 설정해줬다.

그리고 로컬 카프카 기본 설정을 좀 만져줘야 한다.

path 기본값이 localhost로 되어있어서 내부적으로 계속 localhost로 연결을 시도하기 때문에.. 설정파일에서 named listener를 사용해줘야 한다.

vi $(brew --prefix)/etc/kafka/server.properties

(맥 기준) vi나 nano로 server.properties를 수정해주자.

listeners=INTERNAL://0.0.0.0:9092,EXTERNAL://0.0.0.0:19092
advertised.listeners=INTERNAL://localhost:9092,EXTERNAL://host.docker.internal:19092
listener.security.protocol.map=INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT
inter.broker.listener.name=INTERNAL

로컬에서는 9092로, 도커 컨테이너에서는 19092로 연결하도록 이 코드를 추가해주고,

도커용 yml파일에서는 카프카를 19092포트로 설정하면 된다

해당 설정은, 로컬CLI나 IDE용 포트는 9092로(internal)

docker 컨테이너용 포트는 19092로(external) 사용하도록 하는거다. 이걸 통해 kafka는 2개의 서로 다른 접속 포트를 사용할 수 있다.

 

3번째 줄은 어떤 보안 프로토콜을 사용하는지 매핑하는 설정이다. plaintext는 평문 즉 암호화 없는 기본설정이고, tls를 쓰고싶으면 internal:ssl로 하면된다.

4번째 줄은, kafka 브로커들끼리 통신할 때 어떤 리스너를 쓸지 지정하는 거다. 지금은 브로커 1개라 internal에서 더 건드릴 필요는 없다

 

아무튼 이렇게 설정하고 docker run 하면

이렇게 다중 인스턴스 환경 완성! 6060, 6061 포트 두 개에서 같은 token으로 접속했다.

댓글을 하나 달아보면,

짠!!! 이렇게 알림 메시지가 모든 인스턴스로 브로드캐스트되었다.

 

 

회고

마지막으로 정리해보자.

1. 유저a가 인스턴스1에 websocket 연결

2. 유저a가 다른 탭으로 인스턴스2에 또 연결

3. 댓글 알림이 발생 -> kafka -> redis채널에 publish

4. 1,2 모든 인스턴스가 redis 구독 중이라 메세지 수신

5. 각 인스턴스는 내가 가진 세션 중 toMemberId=유저a인 애 있는지 확인

6. 있으면 보냄 -> 그래서 양 쪽 다 메세지 수신 성공

 

처음에는 websocket을 다중 인스턴스 환경에서 사용하려면 뭔가 복잡한 분산 세션 시스템이 필요할 줄 알았다.

혼자 이상한 가정을 세우고 생각하다 보니 머리가 좀 아팠다.

특히나 websocket은 http처럼 요청->응답으로 끝나는 게 아니라 지속 연결 기반이라

프론트가 해야 할 일이 많은 듯 해서 내가 상상하면서 하기엔 좀 힘들었다

공부할게 참 많구나