BackEnd/Spring

JWT + WebSocket + Stateless 환경에서 실시간 채팅 구현 시 알아야 할 핵심 정리

bora-ovo 2025. 11. 25. 00:33

 

실시간 채팅 기능은 일반적으로 HTTP 통신만으로는 사용성이 떨어지기 때문에 WebSocket을 사용한다.

하지만 WebSocket은 기본적으로 statefule 프로토콜이기 때문에 JWT 기반의 SessionStateLess 서버 구조에서는 주의해야한다.

이번 글에서는 WebSocket의 기본, HTTP와의 차이, 그리고 JWT 기반 Stateless를 어떻게 유지할지까지 실제 개발에 들어가기 전 확인할 이론을 정리한다.

 

 

 

1. WebSocket이란? 

 

서버와 클라이언트가 하나의 연결을 지속적으로 유지한 상태로, 양방향 데이터를 실시간으로 주고받을 수 있는 프로토콜이다.

보통 채팅이나 알림, 증권 시세나 게임 서버와 같이 실시간성이 중요한 작업에서 주로 사용한다.

 

여기서 사용자가 웹소켓을 사용하려고 할 때, 처음부터 웹소켓으로 요청이 시작되는 것은 아니고 일반 HTTP 요청을 보낸 후 WebSocket 연결로 변경하게 된다.

이 때 서버는 사용자에게 HTTP 요청을 받고 웹소켓이 가능하다면 연결을 웹소켓으로 변경하는 것이다.

이후부터는 같은 TCP 연결 위에서 웹소켓 전용 프레임의 형식으로 데이터를 주고 받는다.

따라서 물리적으로 연결된 TCP 소켓은 그대로 유지하되, 그 위에서 돌아가는 프로토콜만 HTTP에서 웹소켓으로 변경한다.

 

이처럼 처음엔 HTTP로 요청을 보낸 후 WebSocket으로 바꾸는 과정을 Handshake라고 한다.

 

 

2. WebSocket을 사용하는 이유

그 이유는 HTTP 기반 요청 및 응답은 실시간으로 이루어지는 작업에 있어 비교적 약하기 때문이다.

 

HTTP는 요청이 있어야만 응답이 가능하다는 사실은 다들 알고 있을 것이다.

그래서 HTTP로 실시간 채팅을 구현한다면 매번 사용자가 서버로 요청을 날려야하는데, 이 때 서버 및 네트워크는 지연이 발생하기 쉽고, 또한 과부하의 위험도 큰 편이다.

 

하지만 WebSocket의 경우 HTTP 처럼 연결이 요청 이후에 종료되는 것이 아니라 한 번의 HTTP 요청으로 연결이 지속된다. 

따라서 필요 시에 즉시 발신 및 수신이 가능하여 실시간 채팅에 적합하다.

 

 

3. HTTP와 WebSocket의 차이 한 눈에 보기

항목 HTTP WebSocket
연결방식  요청 후에 종료되기 떄문에 매번 요청을 해야한다. 1번의 HTTP 요청으로 연결이 지속된다.
방향성 단방향적 (요청을 보내야만 응답이 올 수 있기 때문) 양방향적
상태 Stateless Stateful 
사용목적 문서 및 데이터 요청 실시간 이벤트 및 데이터

 

* Stateful (상태 기반) 

: 서버가 사용자의 상태를 기억하고 유지하는 방식

: 다음 요청이 오면 이전 상태를 기억한 상태로 참고하여 처리. (ex. 전통적인 로그인 세션)

 

** Stateless (상태 없음)

: 서버가 사용자의 상태를 저장하지 않는 방식

: 요청마다 필요한 정보를 모두 보내야 하며 서버에서는 모두 이를 새로운 요청으로 취급한다. (ex. JWT 기반 인증)

 

 

4. WebSocket의 특징

1) 초기 연결은 HTTP로 시작한 다음 WebSocket으로 업그레이드 한다.

2) 업그레이드 후에는 별도의 연결을 계속해서 유지한다.

3) stateful한 특성 때문에 연결이 끊겼을 경우 대응이 필요하다

4) HTTP와 동일한 포트(80/443를 사용할 수 있다.

 

 

5. WebSocket 사용할 때 고려사항

1) 연결이 유지되기 떄문에 서버 부담이 증가할 수 있다.

WebSocket은 HTTP와 달리 연결이 끊이지 않고 지속적으로 유지하기 때문에

사용자가 늘어날 수록 서버는 더 많은 소켓을 연결해야하고, 더 많은 메모리를 사용해야하며, 더 많은 스레드 관리를 해야한다.

 

만약 동시에 10,000명의 사용자가 접속해있다면 서버는 10,000개의 WebSocket을 모두 열린 상태로 유지해야하며,

이 때 연결하면서 필요한 버퍼, 컨텍스트, 스레드가 쌓이면서 메모리 사용량이 증가하게 된다.

 

 

2) 연결이 끊길 수 있음을 미리 감지하고 대응할 수 있어야 한다.

WebSocket은 다양한 이유로 연결이 끊어질 수 있다.

더보기

사용자의 인터넷이 잠시 꺼지거나, 인터넷 종류(휴대폰 LTE, WIFI)가 전화되거나, 사용자가 앱을 백그라운드로 보내버렸거나, 서버를 재배포 하면서 연결이 재설정 되었다거나...

이러한 상황을 대비하기 위해 아래 두 가지 전략은 필수적이다.

 

ping/pong 프레임을 통해 연결상태 확인하기

  • 서버 또는 사용자가 일정 주기로 ping을 보내고, 상대가 pong으로 응답하는 방식.
  • 응답이 없으면 연결이 끊어진 것으로 판단한 후 정리한다.

 

사용자의 재연결을 위한 로직을 만들어두기

  • 연결이 끊어졌을 때 자동으로 재연결을 시도하고, 백오프(재시도하는 시간을 점점 증가시킴)를 하며, 재연결 시 마지막으로 받은 메시지 이후부터 다시 불러오는 이러한 기능이 없다면 실제로 사용하는 환경에서는 채팅이 자주 끊기면서 심지어는 메시지가 누락되는 문제까지 생길 수 있다.

 

또한 WebSocket 연결은 보통 하나의 서버에서 지속되지만,

서버 과부화 등의 이유로 인해 서버를 둘로 나누었다면, 메시지 브로커가 필수이다.

메시지 브로커 없이 여러 서버를 띄우면 A서버의 이용자와 B서버의 이용자는 서로 연결될 수 없기 때문이다.

 

 

6. Spring에서의 WebSocket 주요 컴포넌트

  • @EnableWebSocket : WebSocket 설정 클래스임을 명시
  • WebSocketConfigurer.configureWebSocketHandlers() : 핸들러 등록
  • TextWebSocketHandler : 텍스트 메시지를 처리할 핸들러 구현
  • afterConnectionEstablished : 연결 성공 시 실행
  • handleTextMessage : 메시지 수신 시 실행
  • handleTransportError : 오류 발생 시 실행

이 부분에 대해서는 다음 것에서 더 자세히 다룰 것이다. 

단일 서버일 때 어떻게 해야하는지와 함께..

 

 

7. WebSocket에서 JWT 인증이 필요한 이유

WebSocket은 연결시점에서 사용자가 인증된 사용자인지 확인해야한다.

왜냐하면 HTTP 처럼 매 요청마다 자신의 인증정보를 보내지 않고, 이후엔 인증절차가 없이 연결되어있기 때문이다. like. 에버랜드 입장권

 

따라서 연결한 사용자 정보를 미리미리 관리하지 않으면 누가 메시지를 보내는 지 알 수 없기 때문에 보안 문제가 생길 수 있다.

 

 

8. WebSocket + JWT 인증 방식

1. WebSocket에서 JWT는 필수이다.

그 이유는 HTTP는 요청마다 Authorization Header를 넣기 떄문에 매 요청마다 인증 상태를 확인할 수 있지만,WebSocket은 한 번 연결되면 요청 및 응답에서의 구조가 없고 연결이 계속 유지되기 때문에 Handshake 단계에서 인증 과정을 확실하게 거쳐야 하기 떄문이다.

 

2. WebSocket을 쓰면서도 JWT를 쓰려면 결국 JWT의 기반인 Stateless 환경이 되어야 한다.

그렇다면 WebSocket에서 인증된 사용자 정보를 WebSocketSession에 저장하게 한다면 어떻게 될까?

 

3. WebSocketSession에 로그인 상태를 저장해서는 안된다.

로그인 상태를 WebSocketSesssion에 저장하게 되면, 사용자 상태를 서버에 저장하지 않는 Stateless에서 벗어나게 되어 JWT 인증의 본질이 무너지기 때문에 stateful 서버가 되어서 확장성이 떨어진다.

 

4. 그래서 인증과정은 첫 요청인 HandShake 과정 속에서 한 번만 진행을 하고,

거기서 유효성이 검증이 된다면 JWT 안에 있는 User의 ID나 name같은 식별값을 꺼낸다.

그 식별값을 WebSocket의 Principal로 설정하게 된다.

 

5. 이후 WebSocket 연결이 성공하고 나면 이후 메시지 통신단계에서는 Authorization 헤더가 없기 떄문에

메시지 라우팅, 누가 어떤 방에 들어왔는지, 누가 메시지를 보냈는지 이러한 판단을 할 때,

세션ID가 아닌 Principal.getName() 혹은 JWT에서 꺼낸 UserId로 사용자를 식별한다.

 

 

9. WebSocket 연결 중에 JWT가 만료된다면?

WebSocket은 계속 연결되어있다보니 비교적 장시간 연결되어있다.

따라서 토큰이 만료되는 상황이 올 수 있다.

 

이 때는 아래 두 가지 방법으로 해결한다.

 

1) 서버가 재연결을 강제한다.

: access token이 만료되면 서버가 연결을 끊어버리고, 사용자가 refresh token으로 재발급을 받은 후 다시 연결한다.

  • 장점
    • 서버 입장에서는 구조가 단순하기 때문에 쉽게 구현할 수 있다.
    • 유효하지 않은 토큰으로 계속 통신하는 상황을 막을 수 있다,
  • 단점
    • 토큰이 만료 될 떄 마다 연결이 끊기기 떄문에 사용자가 불편함을 겪을 수 있음(채팅방 복원, 재연결 로직 등)

2) 메시지 안에 JWT를 포함하여 매번 검증한다.

: WebSocket 메시지 속 하나하나에 access token을 같이 넣어서 보내는 방식이다.

서버는 메시지를 받을 떄마다 토큰을 검증하고, 만료 및 위조 여부를 체크하며 유효하면 메시지를 전송하고, 아니면 거부한다.

 

WebSocket을 쓰지만 HTTP처럼 요청마다 인증을 하는 느낌이다.

 

  • 장점
    • 이 방식이 오히려 완전한 Stateless에 더 가깝다. 
    • 토큰이 만료되거나 블랙리스트에 올라가면 그 즉시 차단이 가능하다.
    • 서버입장에서 보면 HTTP 인증방식과 거의 동일한 방식이니 통일성이 있다.
  • 단점
    • 모든 메시지에 토큰이 붙기 때문에 payload가 커진다.
    • 채팅 메시지가 짧든 길든 JWT 문자열이 길기 때문에 상대적으로 효율이 떨어진다
    • 메시지마다 인증과 파싱, 그리고 검증과정이 있어 짧은 시간에 메시지나 요청이 극한으로 발생할 떈 성능에 영향을 줄 수 있다.

 

 

 

 

'BackEnd > Spring' 카테고리의 다른 글

서블릿 컨테이너 심화 (멀티스레딩, 부가기능)  (0) 2025.07.07
서블릿 / 서블릿 컨테이너  (0) 2025.06.30
[Java] Interface  (0) 2025.03.26