마지막 업데이트 2026-07-22
apps/socket는 하나의 통합(unified) 프로세스로 두 개의 Socket.io 네임스페이스를 동시에 운영한다.
| 네임스페이스 | 용도 | 핸들러 |
|---|---|---|
/ (default) | 레거시 V1 P2P 룸 관리(시그널링 중계) | socket-handler.ts |
/sfu | V2 mediasoup SFU — 미디어 라우팅·세션·모니터링 | sfu-socket/signalingHandler.ts |
/sfu 네임스페이스의 peer/room 라이프사이클과 미디어 협상(produce/consume). 녹음(#3)·Router 매니저 크로스인스턴스(#5)·Redis 인프라(#16)는 별도 문서로 분리.startServer() 초기화 순서(순서가 중요 — 자원 의존성):
connectRedis). REDIS_URL 비면 memory-only 모드로 skip.workerManager.initialize) → 녹음 매니저 초기화.isAdapterEnabled()면 pub/sub로 cross-instance broadcast 연결(@socket.io/redis-adapter)./metrics + REST 라우트 등록.io.on("connection")(레거시) + io.of("/sfu") → setupSignalingHandlers.REAPER_ENABLED!=="false"면 활성, leader-elected).httpServer.listen(PORT) (기본 3001).worker died 이벤트 시 2초 후 process.exit(1)(워커 복구 불가 → 프로세스 재기동에 위임).
| 매니저 | 책임 | 파일 |
|---|---|---|
workerManager | mediasoup 워커 풀(기본 CPU 코어 수), 라운드로빈 할당 | mediasoup/workerManager.ts |
routerManager | room별 Router(워커당 1개) 생성·소유권·guestState·peerCount. 크로스인스턴스 sync | mediasoup/routerManager.ts |
transportManager | WebRtcTransport(send/recv) 생성·연결·정리 | mediasoup/transportManager.ts |
producerManager | Producer 생성·room/kind/appData 조회·정리 | mediasoup/producerManager.ts |
consumerManager | Consumer 생성(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으로 같은 인스턴스에 수렴시킨다(아래).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 정리(아래) });
roomId 포맷은 {userId}_{lessonIndex}. 역할은 guest(아동) / host / monitor.
GUEST_DUPLICATE_CONNECTION 통지 후 transport/producer/consumer 정리·peer 삭제·peerCount 감소.handshake.auth.targetInstance가 다른 인스턴스면 즉시 WRONG_INSTANCE redirect. 첫 도달이면 tryPinRoom; 실패 시 owner 인스턴스로 redirect. (room의 모든 peer를 한 인스턴스에 수렴)socket.join(roomId). 모니터는 roomId=null(룸 미참여), 호스트는 monitoringRoomId=roomId.routerManager.getOrCreateRouter(roomId) (없으면 생성) → incrementPeerCount → joinedRoomId 기록(정확한 감소 추적용).LESSON_SESSION_EXPIRED 발송.{ success, rtpCapabilities, existingProducers, instanceId } 반환. 클라이언트는 instanceId를 자기 owner로 기록.승인 전까지 게스트는 룸에 join은 되어 있으나 세션은 시작하지 않는다. 호스트 제어 이벤트(마이크/아바타/스텝 등)도 이 핸들러군에서 처리된다(세션 릴레이 #6 참고).
CREATE_TRANSPORT {direction} → transportManager.createTransport → { id, iceParameters, iceCandidates, dtlsParameters } 반환. send/recv 각각 생성. recv는 setMaxIncomingBitrate 적용.CONNECT_TRANSPORT {transportId, dtlsParameters} → DTLS 핸드셰이크. transport DTLS가 closed/failed면 자동 close.PRODUCE { transportId, kind, rtpParameters, appData, roomId, peerId }
└─ producerManager.createProducer(...)
└─ socket.to(roomId).emit(NEW_PRODUCER, { producerId, kind, peerId }) // 같은 룸의 다른 peer가 consume하도록
mic-audio(아동 마이크), ai-audio(핑퐁이 음성), 카메라 video 등. produce 시 type에 따라 녹음 트리거가 동작:
mic-audio + guestState.isAiSessionActive → 최신 ai-audio producer와 함께 tryStartPendingRecording → 시작되면 RECORDING_STARTED 브로드캐스트. 이미 세션 있으면 replaceChildConsumer.ai-audio → replaceAiConsumer로 녹음에 연결.CONSUME {producerId, rtpCapabilities} → createConsumer(paused 상태로 생성 — 선택적 구독). producer의 appData.type을 consumer로 전달. consumer close 시 consumer-closed 통지(카드뷰 정확한 cleanup).RESUME_CONSUMER로 실제 수신 시작 / PAUSE_CONSUMER로 일시중지.SET_CONSUMER_LAYERS {spatialLayer, temporalLayer} → simulcast 레이어 선택(모니터 썸네일 180p ↔ 확대 720p). consumer not found는 disconnect 정상 케이스라 warn 처리.싱글톤 소켓이 여러 peer(모니터 카드뷰)를 가질 수 있어, 해당 socket.id를 가진 모든 peer를 모아 정리한다.
guest-disconnected 통지 + guestState/manualReady 정리. pendingMetadata는 보존(재접속 녹음 재개용).pendingDisconnectTracker.schedule로 grace 후 미종료 lesson session 일괄 종료 예약.stopRecording → RECORDING_STOPPED(성공/실패) 통지.joinedRoomId 기준 peerCount 감소 + PEER_LEFT 통지.tryPinRoom(Redis)으로 room을 한 인스턴스에 고정. 다른 인스턴스 도달 시 WRONG_INSTANCE + redirectInstance로 클라이언트가 owner에 재접속.initRouterManagerSync가 Redis pub/sub로 room 메타 변경을 구독 → owner 메모리 refresh + broadcastRoomList 재전송.상세는 → 로드맵 #5(Router 매니저/소유권), #16(Redis 인프라/reaper). 설계 의도는 Redis 트러블슈팅 가이드 및 handover 문서.
| 항목 | 값 |
|---|---|
| 코덱(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 |
| transport | UDP+TCP, preferUdp, listen/announced IP는 config.ts |
| 엔드포인트 | 용도 |
|---|---|
GET /health, GET / | 헬스체크 |
GET /metrics | Prometheus |
GET /workers/stats | 워커 리소스 사용량 |
GET /rooms, GET /rooms/:roomId | 활성 룸 목록·상세 |
GET /router-capabilities/:roomId | RTP capabilities |
GET /debug/redis-state | Redis 상태 디버그 |
GET/DELETE /peers/:peerId, /groups/:groupId/peers | peer/그룹 모니터 디버그·강제정리 |
socketId 일치 검사로 새로고침 race에서 새 owner를 보호한다. 이 가드를 제거하면 유령 정리로 정상 세션이 끊긴다.joinedRoomId를 써야 정확.| 파일 | 역할 |
|---|---|
| apps/socket/src/server.ts | 부트스트랩·REST·네임스페이스·shutdown |
| sfu-socket/signalingHandler.ts | /sfu 핸들러 등록·peers Map·disconnect 정리 |
| sfu-socket/handlers/connection-handlers.ts | JOIN/LEAVE_ROOM, MONITOR_CONNECTED, GET_PEERS |
| sfu-socket/handlers/transport-handlers.ts | CREATE/CONNECT_TRANSPORT |
| sfu-socket/handlers/media-handlers.ts | PRODUCE/CONSUME/RESUME/PAUSE/CLOSE/SET_LAYERS + 녹음 트리거 |
| sfu-socket/handlers/room-handlers.ts | 입장 승인·호스트 제어 |
| mediasoup/{worker,router,transport,producer,consumer}Manager.ts | 자원 매니저(싱글톤) |
| sfu-config.ts | mediasoup 코덱·워커·포트·대역폭 설정 |