[ApartTime] PING/PONG 기반 WebSocket 세션 관리 도입기

고뭉남·2025년 7월 2일

ApartTime

목록 보기
4/6
post-thumbnail

아파트타임에는 서버에서 클라이언트로 전달되는 단방향 실시간 알림 기능이 있습니다.

해당 알림은 로그인 한 사용자에 한해서만 확인이 가능해야 하기에, 클라이언트에서 사용자가 로그인 시 자동으로 서버와 WebSocket 세션 생성을 해주고 있습니다.

관리자 시스템 인증 흐름: [ApartTime] 관리자 시스템 인증 흐름

이러한 인증 기반의 WebSocket 세션을 안정적으로 유지하기 위해 가장 먼저 해결해야 할 과제는 Access Token(AT)의 만료 문제였습니다.


1차 접근: API 요청 기반 AT 갱신

최초에 고려했던 방식은 일반적인 API 요청의 생명주기에 AT 갱신을 의존하는 것이었습니다.

  1. 클라이언트에서 API 호출

  2. 서버가 Authorization 헤더의 AT 검증

  3. AT가 만료된 경우 401 Unauthorized 반환

  4. 클라이언트가 401 Unauthorized 감지하면 /api/auth/reissue로 재발급 요청

이 방법은 간단했지만 API 요청이 없으면 AT의 갱신도 없다는 한계가 있었습니다.

session_management_flow_v1

이로 인해 몇 가지 문제가 드러났습니다.

1. 인증 무효화 이후에도 살아있는 WebSocket 세션

현재 방식에서는 AT가 만료돼도 서버는 WebSocket 세션의 상태를 모릅니다.

즉, 만료된 세션이 그대로 살아 서버 리소스를 잡아먹고, 더 나아가서는 인증되지 않은 사용자에게도 알림이 계속 전송될 수 있습니다.

2. 서버 주도의 WebSocket 세션 관리 불가

서버는 클라이언트의 활동을 통해서만 AT 상태를 파악할 수 있습니다.

실시간 서비스에 필수적인 능동적 세션 관리 및 모니터링이 불가능했습니다.


2차 접근: 클라이언트 타이머 기반 AT 갱신

첫 번째 방법의 한계를 보완하기 위해 클라이언트 측에서 AT를 선제적으로 갱신하는 방법을 적용해봤습니다.

  1. 사용자 로그인 성공

  2. 서버에서 AT, RT 발급

  3. 클라이언트에서 받은 AT를 디코딩해서 만료 시간 계산

  4. setTimeout()으로 만료 1분 전 /api/auth/reissue 호출 예약

이 방식은 API 호출이 전무해도 만료 직전에 AT를 갱신할 수 있어서 1차 접근 방식에서의 문제를 해결할 수 있었습니다.

session_management_flow_v2

하지만, 클라이언트 타이머 기반 방식도 완벽하지는 않았습니다.

브라우저 스로틀링

Chrome, Safari 등의 최신 브라우저는 비활성 탭이나 백그라운드 상태에서 AT 재발급에 사용되는 setTimeout()이나 setInterval()을 최대 1분 단위로 묶어 실행합니다.

따라서 만료 1분 전에 예약한 콜백이 수 분 뒤에 실행될 수도 있습니다.

(Visibility API나 Web Worker 등을 이용해 우회하는 방법도 살펴봤지만, 프론트엔드 지식이 얕다 보니 완전히 이해하기가 어려워 도입하지 않았습니다.)

이 스로틀링 문제는 브라우저가 언제 타이머를 깨워줄지 예측할 수 없다는 점에서 치명적이라고 판단했습니다.


중간 정리

API 요청 기반 접근과 타이머 기반 접근, 두 방식을 직접 적용해보니 클라이언트 환경에 의존하여 WebSocket 세션을 관리하는 방식의 불안정함을 느낄 수 있었습니다.

결과적으로 WebSocket 세션의 생명주기는 서버가 직접 책임지는 편이 훨씬 안전하고 안정적이라는 결론에 도달했고, 성능 부담이 걱정돼 미뤄두었던 Ping/Pong 기반 서버 주도 WebSocket 세션 관리를 도입했습니다.


3차 접근: 서버 주도 Ping/Pong

WebSocket 세션 관리를 클라이언트에만 의존하는 방식의 구조적 한계를 느끼고, 서버가 주기적으로 Ping을 보내고 클라이언트가 Pong으로 응답하는 heartbeat 패턴을 적용했습니다.

  1. 서버에서 주기(25초)적으로 클라이언트에 Ping 전송 (PingScheduler)

  2. Ping을 받은 클라이언트는 브라우저 로컬 스토리지의 AT decode하여 만료 시간 확인

  3. 현재 시간과 만료 시간 비교

    • 남은 유효 시간이 60초 미만이라면 (만료 임박)

      • 서버로 AT 재발급 요청
        • 재발급 성공 시: 로컬 스토리지에 새로운 AT로 교체
        • 재발급 실패 시 (RT 만료 등): 모든 인증 정보 삭제, WebSocket 세션 연결 종료 후 사용자를 로그인 페이지로 이동
    • 남은 유효 시간이 60초 이상이라면 (유효)

      • 별도의 작업 없이 다음 단계로 이동
  4. 클라이언트에서 서버로 Pong 전송

  5. 서버의 비활성 WebSocket 세션 정리

    • 서버는 Pong을 받을 때마다 해당 WebSocket 세션의 lastPongTime 갱신
    • SessionWatchdogScheduler에서 주기(10초)적으로 WebSocket 세션들을 확인하여 lastPongTime이 60초를 초과한 WebSocket 세션을 찾아 session.close() 실행

session_management_flow_v3

이 구조는 이전 접근들에서 발생하던 문제를 해결할 수 있었습니다.

1. 높은 신뢰성

서버에서 주기적으로 WebSocket 세션을 통해 연결된 클라이언트들에 Ping을 보내는 흐름이다보니, 클라이언트의 상태로부터 독립적입니다.

기존 방식들은 클라이언트 단에서 API 요청이 발생하지 않거나 브라우저의 스로틀링으로 인해 타이머가 늦게 동작하면 인증 상태 유지에 차질이 빚어졌습니다.

하지만 Ping/Pong 방식은 서버가 보내는 Ping이 트리거로 작용하여 클라이언트가 자신의 현재 상태에 구애받지 않고 주기적으로 AT의 유효성을 점검하고 갱신할 수 있는 확실한 기회를 제공합니다.

2. 서버 주도의 능동적 WebSocket 세션 관리

더 이상 인증이 만료된 유령 세션을 방치하지 않습니다.

서버는 Pong 응답의 유무를 통해 클라이언트의 활성 상태를 직접 감지하고 주기적으로 동작하는 SessionWatchdogScheduler가 응답이 없는 WebSocket 세션을 정리합니다.

이를 통해 불필요하게 낭비되던 서버 리소스를 빠르게 회수하고 인증이 만료된 사용자에게 알림이 전송될 수 있는 보안 문제까지 해결하며 안정성을 확보할 수 있게 되었습니다.


하지만, 완벽한 해결책은 없다

Ping/Pong 기반 설계는 앞선 두 방법의 구조적 한계를 거의 모두 해소하지만, heartbeat 모니터링 자체가 야기하는 서버 리소스 부하 문제를 무시하긴 어려울 것 같았습니다.

추후에 부하 테스트를 진행하고 결과를 바탕으로 스케줄러들의 주기, Redis I/O 최적화 등의 성능 튜닝을 시도해 볼 생각입니다.

profile
개발자 고뭉남입니다.

0개의 댓글