Socket.io + mediasoup SFU 서버 — 코드레벨 동작 흐름 P0코드레벨

마지막 업데이트 2026-07-22

작성일: 2026-06-14 대상: 개발자 — 서버측 미디어/시그널링 흐름 파악 핵심 파일: apps/socket/src/

개요 · 범위

apps/socket는 하나의 통합(unified) 프로세스로 두 개의 Socket.io 네임스페이스를 동시에 운영한다.

네임스페이스용도핸들러
/ (default)레거시 V1 P2P 룸 관리(시그널링 중계)socket-handler.ts
/sfuV2 mediasoup SFU — 미디어 라우팅·세션·모니터링sfu-socket/signalingHandler.ts
이 문서의 범위: 서버 부트스트랩 + /sfu 네임스페이스의 peer/room 라이프사이클과 미디어 협상(produce/consume). 녹음(#3)·Router 매니저 크로스인스턴스(#5)·Redis 인프라(#16)는 별도 문서로 분리.

서버 부트스트랩 server.ts:55–174

startServer() 초기화 순서(순서가 중요 — 자원 의존성):

  1. Redis 연결(connectRedis). REDIS_URL 비면 memory-only 모드로 skip.
  2. mediasoup 워커 초기화(workerManager.initialize) → 녹음 매니저 초기화.
  3. Express + http 서버 + Socket.io 생성.
  4. Redis 어댑터: isAdapterEnabled()면 pub/sub로 cross-instance broadcast 연결(@socket.io/redis-adapter).
  5. HTTP 메트릭 미들웨어 + Prometheus /metrics + REST 라우트 등록.
  6. io.on("connection")(레거시) + io.of("/sfu")setupSignalingHandlers.
  7. 메트릭 수집 시작 + Reaper 스케줄러(REAPER_ENABLED!=="false"면 활성, leader-elected).
  8. httpServer.listen(PORT) (기본 3001).
Graceful shutdown(SIGINT/SIGTERM): 메트릭 중지 → reaper lease 해제 → http/socket 닫기 → 모든 녹음 정지 → consumer→producer→transport→router→worker 순 정리 → Redis 종료. (자원 역순 해제)
worker died 이벤트 시 2초 후 process.exit(1)(워커 복구 불가 → 프로세스 재기동에 위임).

자원 계층 (싱글톤 매니저)

매니저책임파일
workerManagermediasoup 워커 풀(기본 CPU 코어 수), 라운드로빈 할당mediasoup/workerManager.ts
routerManagerroom별 Router(워커당 1개) 생성·소유권·guestState·peerCount. 크로스인스턴스 syncmediasoup/routerManager.ts
transportManagerWebRtcTransport(send/recv) 생성·연결·정리mediasoup/transportManager.ts
producerManagerProducer 생성·room/kind/appData 조회·정리mediasoup/producerManager.ts
consumerManagerConsumer 생성(paused)·resume/pause·layer 선택·정리mediasoup/consumerManager.ts
peers Map인메모리 PeerInfo 저장(인스턴스 로컬). 디버그 API용 snapshot/removePeer 제공sfu-socket/signalingHandler.ts:40
중요: peers Map과 mediasoup 자원은 인스턴스 메모리에만 존재한다(복구 불가). 크로스인스턴스 일관성은 Redis(room-store/peer-store)로만 유지하며, room의 모든 peer는 room pin으로 같은 인스턴스에 수렴시킨다(아래).

핸들러 등록 구조 signalingHandler.ts:107–261

setupSignalingHandlers(io)initRouterManagerSync(크로스인스턴스 room 메타 변경 구독)를 1회 init한 뒤, 접속(connection)마다 7개 핸들러군을 등록한다.

io.on("connection", (socket) => {
  setupConnectionHandlers(socket, peers, io);  // JOIN/LEAVE_ROOM, MONITOR_CONNECTED, GET_PEERS
  setupTransportHandlers(socket);            // CREATE/CONNECT_TRANSPORT
  setupMediaHandlers(socket);                // PRODUCE/CONSUME/RESUME/PAUSE/CLOSE/SET_LAYERS
  setupRoomHandlers(socket, peers, io);        // 입장승인·호스트 제어
  setupSessionHandlers(socket, peers, io);     // 세션 상태·릴레이 시그널
  setupAvatarHandlers(socket, peers, io);      // 아바타 상태 동기화
  setupMonitoringHandlers(socket, peers, io);  // 호스트 모니터링 claim
  socket.on("disconnect", ...);                  // 멀티-peer 정리(아래)
});

룸 입장 흐름 — JOIN_ROOM connection-handlers.ts:25–272

roomId 포맷은 {userId}_{lessonIndex}. 역할은 guest(아동) / host / monitor.

  1. 1중복 게스트 정리. 같은 userId의 기존 게스트가 있으면 GUEST_DUPLICATE_CONNECTION 통지 후 transport/producer/consumer 정리·peer 삭제·peerCount 감소.
  2. 2다중 인스턴스 라우팅(room pin). 모니터 제외. handshake.auth.targetInstance가 다른 인스턴스면 즉시 WRONG_INSTANCE redirect. 첫 도달이면 tryPinRoom; 실패 시 owner 인스턴스로 redirect. (room의 모든 peer를 한 인스턴스에 수렴)
  3. 3PeerInfo 저장 + socket.join(roomId). 모니터는 roomId=null(룸 미참여), 호스트는 monitoringRoomId=roomId.
  4. 4Router 확보. routerManager.getOrCreateRouter(roomId) (없으면 생성) → incrementPeerCountjoinedRoomId 기록(정확한 감소 추적용).
  5. 5게스트면 guestState 초기화 + 재접속 backstop 처리: grace 내 재접속이면 pending 타이머 취소(+이전 세션을 disconnect 시각으로 종료해 누적시간 점프 방지), grace 만료면 LESSON_SESSION_EXPIRED 발송.
  6. 6PEER_JOINED 통지 + isTest/groupId 동기화(게스트만).
  7. 7기존 producer 목록 수집(자기 것 제외) → callback으로 { success, rtpCapabilities, existingProducers, instanceId } 반환. 클라이언트는 instanceId를 자기 owner로 기록.
  8. 8broadcastRoomList(모니터 갱신), 호스트면 broadcastConnectedHosts.

입장 승인 room-handlers.ts

게스트: GUEST_REQUEST_ENTRY ──▶ (서버가 호스트 소켓에 중계) ──▶ 호스트 UI 승인 대기 호스트: HOST_APPROVE_ENTRY2 (승인) / host-deny-entry (거부) ──▶ 게스트에 결과 전달

승인 전까지 게스트는 룸에 join은 되어 있으나 세션은 시작하지 않는다. 호스트 제어 이벤트(마이크/아바타/스텝 등)도 이 핸들러군에서 처리된다(세션 릴레이 #6 참고).

미디어 협상 — transport / produce / consume

① 트랜스포트 transport-handlers.ts

② 송출(produce) media-handlers.ts:12–114

PRODUCE { transportId, kind, rtpParameters, appData, roomId, peerId }
  └─ producerManager.createProducer(...)
  └─ socket.to(roomId).emit(NEW_PRODUCER, { producerId, kind, peerId })  // 같은 룸의 다른 peer가 consume하도록
appData.type로 트랙 종류를 구분한다: mic-audio(아동 마이크), ai-audio(핑퐁이 음성), 카메라 video 등. produce 시 type에 따라 녹음 트리거가 동작:
  • mic-audio + guestState.isAiSessionActive → 최신 ai-audio producer와 함께 tryStartPendingRecording → 시작되면 RECORDING_STARTED 브로드캐스트. 이미 세션 있으면 replaceChildConsumer.
  • ai-audioreplaceAiConsumer로 녹음에 연결.
(녹음 상세는 #3 문서)

③ 수신(consume) media-handlers.ts:117–319

왜 paused로 생성? 카드뷰 모니터는 다수 세션을 동시에 보므로, 기본은 저레이어/일시정지로 두고 필요한 것만 resume·고레이어로 올려 CPU/대역폭을 아낀다.

접속 종료 정리 signalingHandler.ts:128–257

싱글톤 소켓이 여러 peer(모니터 카드뷰)를 가질 수 있어, 해당 socket.id를 가진 모든 peer를 모아 정리한다.

  1. replaced 가드: 같은 peerId를 새 소켓이 이미 점유(게스트 새로고침 재진입)했으면 stale cleanup을 skip(새 owner 세션 보호).
  2. 게스트면 guest-disconnected 통지 + guestState/manualReady 정리. pendingMetadata는 보존(재접속 녹음 재개용).
  3. DB backstop: pendingDisconnectTracker.schedule로 grace 후 미종료 lesson session 일괄 종료 예약.
  4. 녹음 중이면 stopRecordingRECORDING_STOPPED(성공/실패) 통지.
  5. peer 삭제 + transport/producer/consumer 정리 + joinedRoomId 기준 peerCount 감소 + PEER_LEFT 통지.
  6. 필요 시 broadcastRoomList / broadcastConnectedHosts.

다중 인스턴스 라우팅 (요약)

상세는 → 로드맵 #5(Router 매니저/소유권), #16(Redis 인프라/reaper). 설계 의도는 Redis 트러블슈팅 가이드 및 handover 문서.

mediasoup 설정 sfu-config.ts, packages/shared MEDIASOUP_CONFIG

항목
코덱(routerMediaCodecs)audio/opus, video/VP8, video/VP9, video/H264
워커 수MEDIASOUP_WORKER_COUNT 또는 CPU 코어 수
RTC 포트 범위40000–40099 (TRANSPORT_MIN/MAX_PORT)
초기 송신 대역폭1,000,000 bps
최대 수신 대역폭1,500,000 bps
transportUDP+TCP, preferUdp, listen/announced IP는 config.ts

REST · 디버그 API server.ts:131–158, sfu-api/

엔드포인트용도
GET /health, GET /헬스체크
GET /metricsPrometheus
GET /workers/stats워커 리소스 사용량
GET /rooms, GET /rooms/:roomId활성 룸 목록·상세
GET /router-capabilities/:roomIdRTP capabilities
GET /debug/redis-stateRedis 상태 디버그
GET/DELETE /peers/:peerId, /groups/:groupId/peerspeer/그룹 모니터 디버그·강제정리

함정 · 주의

파일 · 라인 레퍼런스

파일역할
apps/socket/src/server.ts부트스트랩·REST·네임스페이스·shutdown
sfu-socket/signalingHandler.ts/sfu 핸들러 등록·peers Map·disconnect 정리
sfu-socket/handlers/connection-handlers.tsJOIN/LEAVE_ROOM, MONITOR_CONNECTED, GET_PEERS
sfu-socket/handlers/transport-handlers.tsCREATE/CONNECT_TRANSPORT
sfu-socket/handlers/media-handlers.tsPRODUCE/CONSUME/RESUME/PAUSE/CLOSE/SET_LAYERS + 녹음 트리거
sfu-socket/handlers/room-handlers.ts입장 승인·호스트 제어
mediasoup/{worker,router,transport,producer,consumer}Manager.ts자원 매니저(싱글톤)
sfu-config.tsmediasoup 코덱·워커·포트·대역폭 설정

관련 문서