안녕하세요 브로콜리입니다.
오늘은 디베이트 타이머에서 오랫동안 개발을 이어온 신기능인 화면 공유 기능에 대해 이야기해보겠습니다.
화면 공유 기능이란?
기능의 시작은 작년 초 즈음 경희대 토론 동아리 '이감'과 유저테스트를 하다가 나온 의견에서 시작됩니다.
"사회자가 보는 화면과 토론 청중&토론자가 보는 화면이 분리되면 좋겠어요"

실제로 토론 동아리 세션 배치도를 보면 토론자는 타이머 화면이 송출되는 스크린을 등지고 서게 됩니다.
이에 따라 디스코드나 줌같은 화면 공유 프로그램을 통해 테블릿이나 노트북을 앞에 두고 타이머를 보게되는데요.
이 화면이 너무 작아 시간을 제대로 인지하지 못하거나 눈을 찡그리고 봐야한다는 의견이 있었습니다.
따라서, 사회자의 토론화면을 다른 기기(데스크톱 & 모바일)에서도 공유하여 볼 수 있도록 하는 것이
화면 공유 기능의 핵심이었습니다.
전반적인 구조

구조는 매우 간단했습니다. (정확히는 간단한 줄 알았습니다.)
1. 사회자의 세션 생성 : 사회자는 토론 시작과 동시에 server에 웹소켓 세션을 생성합니다.
2. 사회자 -> 서버 이벤트 전달 : 사회자가 타이머 조작 이벤트가 있을 때마다 서버에게 타이머 이벤트를 전달합니다.
3. 서버는 -> 청중 broadcast : server는 현재 타이머를 공유받고 있는 청중들에게 메시지를 브로드캐스트합니다.
처음에는 생각했습니다.
그냥 브로드 캐스트만 하면 되는거 아니야?
결론적으로 이 생각은 정말 큰 오판이었습니다.
오늘은 이 간단한 중계 플로우에서 어떤 예외 상황들과 세부정책들을 잡아나아갔는지 짧게 짧게 정리해보도록 하겠습니다.
**Stomp 기본 설정(하트 비트, sockjs)는 기본으로 깔고, 인증 정책은 제외했습니다.
Case1) 청중의 난입 시점 별 예외 상황 [토론 시작 전-중-후 ]
먼저 전반적으로 고려해야했던 것은 청중의 공유 시점이었습니다. 해피케이스처럼 사회자와 청중이 함께 공유를 시작하고 토론을 진행하는 경우도 있겠지만, 다음 3가지 케이스를 고려해야 했습니다.
1. 아직 사회자 세션이 없는데 청중이 먼저 공유화면에 진입하면?
-> 청중에게 아직 토론 화면 공유전이라는 안내 필요
2. 청중이 토론 중간에 들어오면?
-> 현재 사회자 세션 화면 즉시 동기화가 필요
3. 토론이 끝나고 청중이 들어오면?
-> 청중에게 토론 끝났다는 안내 필요
Case1-1) 청중이 토론 시작 전에 들어오면? -> 연결정보를 서버가 보유하고 사회자 세션이 없다는 안내
먼저 사회자 세션의 연결정보를 서버가 관리하도록 ChairmanSessionRegistry를 신설했습니다.
그리고 청중이 만약 아직 사회자 연결이 없는 토론에 들어오려 한다면,
청중 세션에 CHAIRMAN_ABSENT라는 메시지를 보내 아직 사회자 연결 대기중이라는 안내를 띄웠습니다.

공유 버튼 밑에 점이 회색 -> 붉은색 ON AIR로 들어오는 순간 화면 공유가 시작됩니다.
처음에는 '토론 대기중' 표시가 나오다가 -> 사회자 연결이 시작되고 나서는 화면이 바로 공유되는 모습을 볼 수 있습니다.
Case1-2) 청중이 토론 중간에 들어오면? -> Sync 이벤트로 즉시 동기화
토론 화면 공유는 정책 상 언제든 QR을 통해 진입이 가능했습니다.
따라서 청중이 처음부터 세션에 참여한 것이 아니라 토론 중간에 난입했을 때 상태를 빠르게 사회자 화면과 동기화해주어야 했습니다.

이를 두 가지로 해결했습니다.
1) 사회자에게 상태공유 요청이 가능한 전용 Path 신설 : 사회자에게 현재 상태 공유 요청 가능
2) SYNC 이벤트 : 현재 사회자 화면의 데이터를 이벤트로 공유

순서를 따라가보면
1. 새로운 유저가 들어오면 Server가 /chairman/{roomID} 를 경로로 사회자 세션 동기화를 위해 SYNC를 요청하고
2. 사회자는 현재 타이머 정보를 받아 SYNC 이벤트를 서버에게 발행합니다.
3. 서버는 사회자로부터 현재 타이머 동기화 정보를 받아 청중들에게 broadcast합니다.
https://youtu.be/Gx6oMde4RhQ
Case1-3) 청중이 토론이 끝나고 들어오면? -> 서버가 종료상태를 기록하고, 종료페이지 이동
이번 예외상황은 토론이 끝나고 아직 웹소켓이 정리되기 전에 청중이 세션에 참여시도를 하는 경우였습니다.
이 경우, 서버 쪽에서 인메모리로 각 Room 별 종료 상태를 기록하고 있도록 하였습니다.
이에 따라, 청중이 종료된 세션에 참여 시도를 하는지 서버가 판단하고
사회자에게 별도 동기화 요청을 안하고 바로 FINISHED 이벤트를 발송해 종료된 페이지로 이동하도록 했습니다.

인메모리로 구현하는 선택은 재배포에는 취약하지만 도메인 특성상 활용되지 않는 새벽 시간 배포라는 선택지가 있었고 트래픽이 큰 서비스가 아니라 지금으로선 충분하다 판단했습니다. 특히 Stomp 내장 Message Broker를 사용하면 Sticky Session이 필요한 상황이라 확장시에도 같은 토론 세션내의 사회자와 청중 트래픽이 같은 서버로 오는 것을 가정했습니다.
다음과 같이 종료된 이후에 공유를 시도할 경우 자동으로 토론 종료페이지로 이동되는 모습을 볼 수 있습니다.
https://youtu.be/Ebbc6PqD6g0
Case2) 사회자 탈출 시? -> 5초 간격 Sync + 15초 무응답 시 대기 화면
두 번째 예외 상황은 갑자기 사회자 연결이 끊긴 상황입니다.
이 경우 청중이나 토론자는 본인 휴대폰 & 데스크톱 화면에 있는 타이머를 절대적으로 신뢰할 수 있어
어느 정도 장애복구 회복을 위한 시간은 두되 화면을 공유받는 청중이 최대한 빠르게 공유 상태의 이상을 알 수 있도록 조치해야 합니다.

이를 위해 사회자 세션이 5초 간격으로 Sync 동기화 메시지를 보내도록 했습니다. 마치 heartbeat 처럼요.
또한 청중이 15초 간 Sync를 받지 못하면 사회자 소켓이 끊겼다고 판단하고 대기 페이지를 띄우도록 했습니다.
이러한 정책은 청중이 5초마다 사회자 세션의 상태를 받아
타이머 상태가 5초 이상 어긋나는 상황을 방지하는 효과를 가져옴과 동시에 사회자 세션이 살아있음을 빠르게 판단하는 기준이 되었습니다.
다음 영상을 통해 15초 내 사회자 연결이 복구되면 다시 사회자 타이머의 기준에 맞게 공유화면이 동기화되지만
15초 이상 사회자 연결이 부재하다고 판단되면 대기 화면을 띄우는 모습을 볼 수 있습니다.
https://m.youtube.com/watch?v=wSWudIKsTRY
Case3) 1초 이상의 네트워크 지연 시 -> 지연시간 보정
이번에는 네트워크 지연으로 이한 타이머 정확도에 대한 예외사항입니다.
서버의 시간은 각각 다르게 흐르고 청중이 얼마나 물리적으로 떨어져 있느냐에 따라 네트워크 지연도 무시할 수 없었습니다.
사회자가 바라보는 타이머와 청중 타이머를 동기화하기 위해서는 사회자 세션 발행 -> 서버 -> 브로드 캐스트를 거칩니다.
이때, 1초 이상 네트워크 지연이 발생할 경우 사회자 타이머와는 몇 초 차이가 나는 타이머가 현출될 수 있었습니다.
해결방안을 고심하다가 일전에 다루었던 NTP 시간 동기화 통신구조가 생각이 났습니다.
NTP 서버시간 동기화를 위해서는 보낸 시간과 받은 시간을 기록해 그 사이 네트워크 지연을 유추하고,
그 지연시간을 보정하여 서버 시간을 동기화하는 과정을 거칩니다.
이와 동일하게 사회자의 현재 상태를 보낸 시각과 청중들이 사회자 정보를 받은 시각 사이의 네트워크 지연시간을 계산하여 보정하면
현재 타이머를 더 정확히 사회자 타이머와 동기화할 수 있었습니다.

또한, 보정값이 1초 이상이 아니라면 현재 상태를 유지하도록 하여 5초마다 타이머가 튀는 현상이 발생하지 않도록 하였습니다.
Case4) 사회자 중복 세션 -> 최근 세션으로 공유 권한 이전
이번 예외 상황은 테스트를 하다가 발견하게 된 예외상황인데요.
A탭에서 사회자 토론 공유를 시작했고 -> B탭을 열어 같은 토론을 대상으로 공유를 시작했습니다.
그 결과, 사회자 세션이 2개가 되면서 각각 5초 간격으로 각자의 사회자 타이머 정보를 공유하기 시작했는데요.
이에 따라 청중 화면이 N초 간격으로 다른 화면을 띄우게 되는 상황이 생겼습니다.

공유되는 화면은 단일 진실원이어야 합니다. 즉 하나의 주체만 자신의 상태를 공유해야 합니다.
이에 따라 중복 사회자 세션을 핸들링하기 위한 서비스 정책을 정했습니다.
- 룸마다 활성 사회자 세션은 하나만 두고, 가장 최근에 공유를 시작한 세션만 공유 권한을 받음
- 공유 권한을 박탈 당한 탭은 '다른 곳에서 공유 중' 안내를 띄우고 '이 화면에서 다시 공유하기'로 권한을 되찾을 수 있음
그리고 이를 사회자 세션 헤더와 중복 세션 탐지 이벤트를 통해 구현하였습니다.
- X-Chariman-Session에 프론트가 UUID로 발행자 식별자를 주어 활성 세션의 이벤트만 청중에 전달
- 만약 새로운 사회자 세션이 들어오면 그곳으로 공유 권한을 이전하고 기존 세션에는 `REPLACED` 알림을 통해 공유 권한을 박탈

Case5) 백그라운드로 인한 네트워크 억제 -> 재연결 정책 도입
다섯번째 예외상황은 브라우저에서 해당 탭이 백그라운드 잡으로 넘어가는 케이스에 대한 고려였습니다.
토론을 하는 상황에서는 타이머 화면만을 송출하지는 않습니다.
대회 규정집, 혹은 토론 중간에 돌발상황이 발생하면 여러 탭을 넘나들며 토론이 진행됩니다.
즉, 토론 타이머 탭의 포커싱이 브라우저에서 벗어나 백그라운드 잡으로 넘어가는 일이 생깁니다.
문제는 브라우저가 백그라운드 프로세스의 타이머를 억제한다는 점입니다.
우리는 10초 간격으로 STOMP HeartBeat를 보내는 것을 setInterval 타이머 설정을 통해 지정해놓았고
청중/사회자는 20초 동안 PONG을 받지 못하면 서버 네트워크가 끊어졌다고 판단합니다.
그런데 크롬 문제 보고 글들을 보면
- 5분 이상 탭이 숨겨져 있고,
- webRTC를 쓰지 않으며
- chain된 타이머 동작인 5회 이상이라면
타이머를 1분에 한번 예약된 작업이 있는지 확인하여 실행합니다.
즉, heartbeat가 1분 마다 동작하게 되어 웹소켓 연결이 비정상적이라 판단해 서버 쪽에 끊는 현상이 발생하게 됩니다.
이를 위해 먼저 FE에서는 다음 작업을 진행했습니다.
- 사회자
- 탭 복귀 감지 & 웹소켓 살아 있으면 -> 즉시 SYNC를 발행
- 탭 복귀 감지 & 웹소켓 죽었으면 -> 2분 상한으로 지수적 백오프로 재연결
- 청중
- 청중 탭 복귀 감지 & 소켓 살아있으면 -> 채널을 재구독해 재동기화 요청
- 청중 탭 복귀 감지 & 소켓 죽어있으면 -> 지수적 백오프 3회로 10초 상한으로 재연결 시도
- 청중 재동기화 요청 -> 사회자 Sync 발행 -> 서버의 Sync broadcast로 재동기화
여기서 청중과 사회자의 재연결 정책에 차이를 둔 이유는 공유 기능에서의 중요성 때문입니다.
사회자 세션이 미연결 상태이면 공유 기능 자체가 동작하지 않습니다.
청중 세션 하나가 미연결 상태이면 청중 세션 하나의 미연결입니다.
따라서 사회자 세션은 2분 상한으로 적극적인 재시도를 유지하고
청중의 경우 10초 상한으로 3회 정도 재연결을 시도하되 이후에는 새로고침 버튼을 현출해 회복방안을 제시했습니다.
[사회자가 백그라운드 탭으로 이동하여 연결 끊겼을 때 -> 1분 30초 즈음 나옴]
https://youtu.be/NrCi00t82To
[청중이 백그라운드 탭으로 이동하여 연결 끊겼을 때 -> 1분 30초 즈음 나옴]
https://youtu.be/R2h_WLk-T1I
Case6) 청중 동시참여로 인한 과도한 Broadcast -> 500ms rate limit
마지막 예외상황은 브로드 캐스트의 남용에 대한 부분이었습니다.
만약 청중이 굉장히 많이 참여하게 된다면 어떻게 될까요?
실제로 디베이트 타이머는 대학 총장 투표, 혹은 세미나에서 사용시 최대 78명이 한번에 동시에 참여하여 토론을 진행하기도 하였습니다.
이런 상황에서 청중이 참여할 때마다 Sync로 사회자 상태를 동기화하는 것은 부담이 될 뿐만 아니라 비효율적입니다.

이에 따라 서버 측에서 각 토론 방 별로 연결상태와 더불어 최근 Sync 이벤트 발행에 대한 시각을 들고 있고
500ms 가 지나지 않았다면 Sync 요청을 무시하는 형태로,
즉 500ms 당 최대 하나의 Sync가 토론방 별로 이루어지도록 rate limit을 적용했습니다.
만약 Sync를 받지 못한 청중의 경우 다음 5초 틱의 Sync 이벤트 발행에 의해 자연스럽게 동기화가 되도록 하였습니다.
총 정리
그럼 예외상황과 핸들링 방식을 총 정리해보겠습니다.
Case1) 청중 난입 시점 별 예외 핸들링
1-1. 토론 시작 전 청중 진입 -> 서버가 사회자 세션 활성여부 판단 후 CHAIRMAN_ABSENT 메시지로 대기화면 현출
1-2. 토론 중 청중 진입 -> 동기화 이벤트 즉시 발행하여 바로 사회자 타이머 팔로업
1-3. 토론 후 청중 진입 -> 서버가 토론 종료 여부 판단 후, FINSIHED 메시지로 청중을 종료 화면으로 이동시킴
Case2) 사회자 탈출 시 -> 5초 간격 동기화 이벤트 발행, 15초 무응답 시 청중은 대기화면 현출
Case3) 1초 이상 네트워크 지연 시 -> 지연시간 보정
Case4) 사회자 중복 세션 -> 최근 접속 세션으로 공유 권한 이전
Case5) 탭 전환 시 백그라운드 프로세스의 웹소켓 유실 -> 재시도 정책
Case6) 청중 동시참여로 인한 과도한 Broadcast -> 토론 별 sync rate limit
느낀 점
0. 단순 중계 기능인줄 알았는데 생각보다 빡세다
이번 작업은 제가 밀려있던 공유기능 예외 상황핸들링에 대해 약 일주일간 각을 잡고 FE, BE 모든 작업을 주도했던 태스크였습니다.
물론 FE는 거의 클로드에 의존했지만 BE 설계와 정책을 함께 잡아가며 에이전틱 코딩의 속도와 파급력을 느끼는 계기가 되기도 하였습니다.

문제는 1-2개 일줄 알았던 예외상황의 범위가 알면 알수록 커졌고, 테스트를 하다가도 계속 새로운 예외상황이 발견되었습니다.
생각보다 더 많은 리소스를 쓰게된 작업이었습니다.
1. 실시간성이 도입되는 순간 유저의 액션을 최대한 자유롭게 상상해야 한다.
웹소켓은 주로 실시간성이 필요한 세션에 쓰입니다. 그만큼 문제가 생겼을 때 유저의 경험도 실시간으로 크게 저하되는 리스크를 지닙니다.
특히나 타이머 서비스의 경우, 토론 시간 조절에 큰 영향을 미치므로 그 피해범위는 복구하기 힘듭니다. 토론 현장에서는 시간을 소급하여 두 번의 발언기회를 주지 않기 떄문입니다.
그만큼 유저의 행동을 최대한 다양한 시나리오로 예측하고 예방하는 정책들이 필요했습니다. 그 중에는 청중 중도난입이라는 예측가능한 범위도 있었지만 사회자 중복 세션 생성과 같은 예측하기 힘든 시나리오도 많았습니다.
그 예측불가한 시나리오 생성에 도움을 준 건 다름아닌 토론에 전혀 배경지식이 없는 지인분이었습니다. 지인분께 dev 화면을 띄워놓고 '내가 만든 서비스 터트리려면 어떻게 할 수 있을까?'라는 한마디를 던졌더니 자동으로 QA가 되었습니다. 상상력이 많은 유저 & 제 서비스를 터트리고 싶어하는 유저의 경우 QA 엔지니어 못지 않은 Case listup에 도움이 되었습니다.
2. 소켓은 서버가 아닌 프론트적 지식도 꽤 많이 필요하다
크롬 브라우저의 백그라운드 타이머 억제에 대한 부분을 보고, 와... 소켓 통신은 서버도 서버인데 프론트가 꽤 많은 걸 고려해야하구나... 느꼈습니다. 특히 이번 글에 포함되지 않은 HeartBeat부터 인증정책과 같은 부분을 논의할 때에는 프론트 개발자와 더 많은 소통이 있었고 그만큼 기능 개발이 지연되기도 하였습니다. 서버와 프론트 모두 각자의 스펙을 잘 이해하고 적극적으로 논의하는 태도와 의지가 필요한 기능이 아니었나 싶습니다.
디베이트 타이머는 이번 기능을 출시한 이후로 타이머의 범위를 넘어 새로운 Phase의 서비스로 넘어가고자 합니다.
더 쉽고 빠른 토론 진행 -> 더 쉽고 빠른 토론으로
디베이트 타이머의 여정은 계속됩니다.
그럼 오늘도 행복하세요!
'프로젝트 > 디베이트 타이머' 카테고리의 다른 글
| [디베이트 타이머] 하늘이 무너져도 솟아날 로그는 있다 : DataDog 없이 커널 로그만으로 CPU 스파이크 원인 찾기 (0) | 2026.08.08 |
|---|---|
| [디베이트 타이머] 시간은 항상 앞으로 흐르지 않는다 - NTP가 만든 타이머 버그 (0) | 2026.07.13 |
| [디베이트 타이머] GC로 인해 우주가 잠시 꺼진 이야기 (5) | 2026.01.04 |
| [디베이트 타이머] 실 사용자 200명과 토론대회 공식 타이머까지 (2) | 2026.01.01 |
| [DB 마이그레이션] 다운타임 1초 이내로 데이터 옮기기 (6) | 2025.05.22 |