diff --git a/src/main/java/com/back/global/websocket/webrtc/controller/WebRTCSignalingController.java b/src/main/java/com/back/global/websocket/webrtc/controller/WebRTCSignalingController.java index 3e20473f..c5602e1a 100644 --- a/src/main/java/com/back/global/websocket/webrtc/controller/WebRTCSignalingController.java +++ b/src/main/java/com/back/global/websocket/webrtc/controller/WebRTCSignalingController.java @@ -6,6 +6,7 @@ import com.back.global.websocket.dto.WebSocketSessionInfo; import com.back.global.websocket.service.WebSocketSessionManager; import com.back.global.websocket.util.WebSocketAuthHelper; +import com.back.global.websocket.webrtc.dto.WebRTCErrorResponse; import com.back.global.websocket.webrtc.dto.media.WebRTCMediaStateResponse; import com.back.global.websocket.webrtc.dto.media.WebRTCMediaToggleRequest; import com.back.global.websocket.webrtc.dto.signal.*; @@ -16,7 +17,6 @@ import org.springframework.messaging.handler.annotation.MessageExceptionHandler; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.Payload; -import org.springframework.messaging.simp.SimpMessageHeaderAccessor; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.stereotype.Controller; import org.springframework.validation.annotation.Validated; @@ -35,13 +35,10 @@ public class WebRTCSignalingController { // WebRTC Offer 메시지 처리 @MessageMapping("/webrtc/offer") - public void handleOffer(@Validated @Payload WebRTCOfferRequest request, - SimpMessageHeaderAccessor headerAccessor, - Principal principal) { + public void handleOffer(@Validated @Payload WebRTCOfferRequest request, Principal principal) { CustomUserDetails userDetails = WebSocketAuthHelper.extractUserDetails(principal); if (userDetails == null) { - errorHelper.sendUnauthorizedError(headerAccessor.getSessionId()); - return; + throw new CustomException(ErrorCode.UNAUTHORIZED); } Long fromUserId = userDetails.getUserId(); @@ -66,13 +63,10 @@ public void handleOffer(@Validated @Payload WebRTCOfferRequest request, // WebRTC Answer 메시지 처리 @MessageMapping("/webrtc/answer") - public void handleAnswer(@Validated @Payload WebRTCAnswerRequest request, - SimpMessageHeaderAccessor headerAccessor, - Principal principal) { + public void handleAnswer(@Validated @Payload WebRTCAnswerRequest request, Principal principal) { CustomUserDetails userDetails = WebSocketAuthHelper.extractUserDetails(principal); if (userDetails == null) { - errorHelper.sendUnauthorizedError(headerAccessor.getSessionId()); - return; + throw new CustomException(ErrorCode.UNAUTHORIZED); } Long fromUserId = userDetails.getUserId(); @@ -97,13 +91,10 @@ public void handleAnswer(@Validated @Payload WebRTCAnswerRequest request, // ICE Candidate 메시지 처리 @MessageMapping("/webrtc/ice-candidate") - public void handleIceCandidate(@Validated @Payload WebRTCIceCandidateRequest request, - SimpMessageHeaderAccessor headerAccessor, - Principal principal) { + public void handleIceCandidate(@Validated @Payload WebRTCIceCandidateRequest request, Principal principal) { CustomUserDetails userDetails = WebSocketAuthHelper.extractUserDetails(principal); if (userDetails == null) { - errorHelper.sendUnauthorizedError(headerAccessor.getSessionId()); - return; + throw new CustomException(ErrorCode.UNAUTHORIZED); } Long fromUserId = userDetails.getUserId(); @@ -127,13 +118,10 @@ public void handleIceCandidate(@Validated @Payload WebRTCIceCandidateRequest req // 미디어 상태 토글 처리 (방 전체에 상태 공유) @MessageMapping("/webrtc/media/toggle") - public void handleMediaToggle(@Validated @Payload WebRTCMediaToggleRequest request, - SimpMessageHeaderAccessor headerAccessor, - Principal principal) { + public void handleMediaToggle(@Validated @Payload WebRTCMediaToggleRequest request, Principal principal) { CustomUserDetails userDetails = WebSocketAuthHelper.extractUserDetails(principal); if (userDetails == null) { - errorHelper.sendUnauthorizedError(headerAccessor.getSessionId()); - return; + throw new CustomException(ErrorCode.UNAUTHORIZED); } Long userId = userDetails.getUserId(); @@ -150,15 +138,41 @@ public void handleMediaToggle(@Validated @Payload WebRTCMediaToggleRequest reque // WebRTC 시그널링 처리 중 발생하는 CustomException 처리 @MessageExceptionHandler(CustomException.class) - public void handleCustomException(CustomException e, SimpMessageHeaderAccessor headerAccessor) { - log.warn("WebRTC 시그널링 오류 발생: {}", e.getMessage()); - errorHelper.sendCustomExceptionToUser(headerAccessor.getSessionId(), e); + public void handleCustomException(CustomException e, Principal principal) { + if (principal == null) { + log.warn("인증 정보 없는 사용자의 WebRTC 오류: {}", e.getMessage()); + return; + } + + log.warn("WebRTC 시그널링 오류 발생 (to {}): {}", principal.getName(), e.getMessage()); + + WebRTCErrorResponse errorResponse = WebRTCErrorResponse.from(e); + + messagingTemplate.convertAndSendToUser( + principal.getName(), // 에러를 발생시킨 사람의 username + "/queue/webrtc", + errorResponse + ); } // 예상치 못한 모든 Exception 처리 @MessageExceptionHandler(Exception.class) - public void handleGeneralException(Exception e, SimpMessageHeaderAccessor headerAccessor) { - log.error("WebRTC 시그널링 처리 중 예상치 못한 오류 발생", e); - errorHelper.sendGenericErrorToUser(headerAccessor.getSessionId(), e, "시그널링 처리 중 오류가 발생했습니다."); + public void handleGeneralException(Exception e, Principal principal) { // headerAccessor -> Principal + if (principal == null) { + log.error("WebRTC 처리 중 인증 정보 없는 사용자의 예외 발생", e); + return; + } + + log.error("WebRTC 시그널링 처리 중 예상치 못한 오류 발생 (to {})", principal.getName(), e); + + // CustomException으로 감싸서 일관된 형식의 에러 DTO를 생성 + CustomException customException = new CustomException(ErrorCode.WS_INTERNAL_ERROR); + WebRTCErrorResponse errorResponse = WebRTCErrorResponse.from(customException); + + messagingTemplate.convertAndSendToUser( + principal.getName(), + "/queue/webrtc", + errorResponse + ); } } \ No newline at end of file diff --git a/src/main/java/com/back/global/websocket/webrtc/dto/WebRTCErrorResponse.java b/src/main/java/com/back/global/websocket/webrtc/dto/WebRTCErrorResponse.java new file mode 100644 index 00000000..9f872930 --- /dev/null +++ b/src/main/java/com/back/global/websocket/webrtc/dto/WebRTCErrorResponse.java @@ -0,0 +1,20 @@ +package com.back.global.websocket.webrtc.dto; + +import com.back.global.exception.CustomException; +import java.time.LocalDateTime; + +public record WebRTCErrorResponse( + String type, + ErrorPayload error, + LocalDateTime timestamp +) { + public record ErrorPayload(String code, String message) {} + + public static WebRTCErrorResponse from(CustomException e) { + return new WebRTCErrorResponse( + "ERROR", + new ErrorPayload(e.getErrorCode().getCode(), e.getMessage()), + LocalDateTime.now() + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/back/global/websocket/webrtc/dto/signal/SdpData.java b/src/main/java/com/back/global/websocket/webrtc/dto/signal/SdpData.java new file mode 100644 index 00000000..a7543e45 --- /dev/null +++ b/src/main/java/com/back/global/websocket/webrtc/dto/signal/SdpData.java @@ -0,0 +1,3 @@ +package com.back.global.websocket.webrtc.dto.signal; + +public record SdpData(String type, String sdp) {} \ No newline at end of file diff --git a/src/main/java/com/back/global/websocket/webrtc/dto/signal/WebRTCAnswerRequest.java b/src/main/java/com/back/global/websocket/webrtc/dto/signal/WebRTCAnswerRequest.java index ca3d7cfe..f8499727 100644 --- a/src/main/java/com/back/global/websocket/webrtc/dto/signal/WebRTCAnswerRequest.java +++ b/src/main/java/com/back/global/websocket/webrtc/dto/signal/WebRTCAnswerRequest.java @@ -6,7 +6,7 @@ public record WebRTCAnswerRequest( @NotNull Long roomId, @NotNull Long targetUserId, - @NotNull String sdp, + @NotNull SdpData sdp, @NotNull WebRTCMediaType mediaType ) { } \ No newline at end of file diff --git a/src/main/java/com/back/global/websocket/webrtc/dto/signal/WebRTCOfferRequest.java b/src/main/java/com/back/global/websocket/webrtc/dto/signal/WebRTCOfferRequest.java index 93fe4108..1bfe6ddc 100644 --- a/src/main/java/com/back/global/websocket/webrtc/dto/signal/WebRTCOfferRequest.java +++ b/src/main/java/com/back/global/websocket/webrtc/dto/signal/WebRTCOfferRequest.java @@ -6,7 +6,7 @@ public record WebRTCOfferRequest( @NotNull Long roomId, @NotNull Long targetUserId, - @NotNull String sdp, + @NotNull SdpData sdp, @NotNull WebRTCMediaType mediaType ) { } \ No newline at end of file diff --git a/src/main/java/com/back/global/websocket/webrtc/dto/signal/WebRTCSignalResponse.java b/src/main/java/com/back/global/websocket/webrtc/dto/signal/WebRTCSignalResponse.java index 59f8d2b4..f077ca24 100644 --- a/src/main/java/com/back/global/websocket/webrtc/dto/signal/WebRTCSignalResponse.java +++ b/src/main/java/com/back/global/websocket/webrtc/dto/signal/WebRTCSignalResponse.java @@ -9,7 +9,7 @@ public record WebRTCSignalResponse( Long fromUserId, Long targetUserId, Long roomId, - String sdp, + SdpData sdp, WebRTCMediaType mediaType, String candidate, String sdpMid, @@ -23,7 +23,7 @@ public static WebRTCSignalResponse offerOrAnswer( Long fromUserId, Long targetUserId, Long roomId, - String sdp, + SdpData sdp, WebRTCMediaType mediaType ) { return new WebRTCSignalResponse( diff --git a/src/test/java/com/back/global/websocket/webrtc/controller/WebRTCSignalingControllerTest.java b/src/test/java/com/back/global/websocket/webrtc/controller/WebRTCSignalingControllerTest.java index 4485077e..d006c4f1 100644 --- a/src/test/java/com/back/global/websocket/webrtc/controller/WebRTCSignalingControllerTest.java +++ b/src/test/java/com/back/global/websocket/webrtc/controller/WebRTCSignalingControllerTest.java @@ -10,7 +10,6 @@ import com.back.global.websocket.webrtc.dto.media.WebRTCMediaType; import com.back.global.websocket.webrtc.dto.signal.*; import com.back.global.websocket.webrtc.service.WebRTCSignalValidator; -import com.back.global.websocket.util.WebSocketErrorHelper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -19,7 +18,6 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.messaging.simp.SimpMessageHeaderAccessor; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; @@ -37,28 +35,20 @@ class WebRTCSignalingControllerTest { @Mock private SimpMessagingTemplate messagingTemplate; - - @Mock - private WebSocketErrorHelper errorHelper; - @Mock private WebRTCSignalValidator validator; - @Mock private WebSocketSessionManager sessionManager; @InjectMocks private WebRTCSignalingController controller; - private SimpMessageHeaderAccessor headerAccessor; private Authentication authentication; private Long roomId; private Long fromUserId; private String fromUsername; private Long targetUserId; private String targetUsername; - private String fromSessionId; - private String targetSessionId; @BeforeEach void setUp() { @@ -67,16 +57,11 @@ void setUp() { fromUsername = "userA"; targetUserId = 20L; targetUsername = "userB"; - fromSessionId = "from-session-id"; - targetSessionId = "target-session-id"; CustomUserDetails userDetails = mock(CustomUserDetails.class); lenient().when(userDetails.getUserId()).thenReturn(fromUserId); lenient().when(userDetails.getUsername()).thenReturn(fromUsername); authentication = new UsernamePasswordAuthenticationToken(userDetails, null, null); - - headerAccessor = mock(SimpMessageHeaderAccessor.class); - lenient().when(headerAccessor.getSessionId()).thenReturn(fromSessionId); } @Nested @@ -87,180 +72,107 @@ class HandleOfferTest { @DisplayName("성공 - Offer 메시지를 특정 사용자에게 전송") void t1() { // given - WebRTCOfferRequest request = new WebRTCOfferRequest(roomId, targetUserId, "sdp", WebRTCMediaType.AUDIO); - WebSocketSessionInfo targetSession = new WebSocketSessionInfo(targetUserId, targetUsername, targetSessionId, LocalDateTime.now(), LocalDateTime.now(), null); + SdpData sdpData = new SdpData("offer", "sdp-content"); + WebRTCOfferRequest request = new WebRTCOfferRequest(roomId, targetUserId, sdpData, WebRTCMediaType.AUDIO); + WebSocketSessionInfo targetSession = new WebSocketSessionInfo(targetUserId, targetUsername, "session-id", LocalDateTime.now(), LocalDateTime.now(), null); when(sessionManager.getSessionInfo(targetUserId)).thenReturn(targetSession); // when - controller.handleOffer(request, headerAccessor, authentication); + controller.handleOffer(request, authentication); // then verify(validator).validateSignal(roomId, fromUserId, targetUserId); verify(messagingTemplate).convertAndSendToUser(eq(targetUsername), eq("/queue/webrtc"), any(WebRTCSignalResponse.class)); - verifyNoInteractions(errorHelper); } @Test @DisplayName("실패 - 대상 사용자가 오프라인일 때 예외를 던짐") void t2() { // given - WebRTCOfferRequest request = new WebRTCOfferRequest(roomId, targetUserId, "sdp", WebRTCMediaType.AUDIO); + SdpData sdpData = new SdpData("offer", "sdp-content"); + WebRTCOfferRequest request = new WebRTCOfferRequest(roomId, targetUserId, sdpData, WebRTCMediaType.AUDIO); when(sessionManager.getSessionInfo(targetUserId)).thenReturn(null); // when & then CustomException exception = assertThrows(CustomException.class, () -> - controller.handleOffer(request, headerAccessor, authentication) + controller.handleOffer(request, authentication) ); - assertThat(exception.getErrorCode()).isEqualTo(ErrorCode.WS_TARGET_OFFLINE); - - verify(validator).validateSignal(roomId, fromUserId, targetUserId); - verifyNoInteractions(errorHelper); verify(messagingTemplate, never()).convertAndSendToUser(anyString(), anyString(), any()); } @Test - @DisplayName("실패 - 인증 정보 없음") + @DisplayName("실패 - 인증 정보가 없을 때 Unauthorized 예외를 던짐") void t3() { // given - WebRTCOfferRequest request = new WebRTCOfferRequest(roomId, targetUserId, "sdp", WebRTCMediaType.AUDIO); - - // when - controller.handleOffer(request, headerAccessor, null); - - // then - verify(errorHelper).sendUnauthorizedError(fromSessionId); - verify(validator, never()).validateSignal(any(), any(), any()); - verify(messagingTemplate, never()).convertAndSendToUser(anyString(), anyString(), any()); - } - - @Test - @DisplayName("실패 - 검증 오류 시 예외를 던짐") - void t4() { - // given - WebRTCOfferRequest request = new WebRTCOfferRequest(roomId, targetUserId, "sdp", WebRTCMediaType.VIDEO); - doThrow(new CustomException(ErrorCode.BAD_REQUEST)).when(validator).validateSignal(roomId, fromUserId, targetUserId); + SdpData sdpData = new SdpData("offer", "sdp-content"); + WebRTCOfferRequest request = new WebRTCOfferRequest(roomId, targetUserId, sdpData, WebRTCMediaType.AUDIO); // when & then - assertThrows(CustomException.class, () -> controller.handleOffer(request, headerAccessor, authentication)); - verify(validator).validateSignal(roomId, fromUserId, targetUserId); - verifyNoInteractions(errorHelper); - verify(messagingTemplate, never()).convertAndSendToUser(anyString(), anyString(), any()); + CustomException exception = assertThrows(CustomException.class, () -> + controller.handleOffer(request, null) // principal = null + ); + assertThat(exception.getErrorCode()).isEqualTo(ErrorCode.UNAUTHORIZED); + verify(validator, never()).validateSignal(any(), any(), any()); } } @Nested @DisplayName("Answer 처리") class HandleAnswerTest { - @Test @DisplayName("성공 - Answer 메시지를 특정 사용자에게 전송") void t1() { // given - WebRTCAnswerRequest request = new WebRTCAnswerRequest(roomId, targetUserId, "sdp", WebRTCMediaType.AUDIO); - WebSocketSessionInfo targetSession = new WebSocketSessionInfo(targetUserId, targetUsername, targetSessionId, LocalDateTime.now(), LocalDateTime.now(), null); + SdpData sdpData = new SdpData("answer", "sdp-content"); + WebRTCAnswerRequest request = new WebRTCAnswerRequest(roomId, targetUserId, sdpData, WebRTCMediaType.AUDIO); + WebSocketSessionInfo targetSession = new WebSocketSessionInfo(targetUserId, targetUsername, "session-id", LocalDateTime.now(), LocalDateTime.now(), null); when(sessionManager.getSessionInfo(targetUserId)).thenReturn(targetSession); // when - controller.handleAnswer(request, headerAccessor, authentication); + controller.handleAnswer(request, authentication); // then verify(validator).validateSignal(roomId, fromUserId, targetUserId); verify(messagingTemplate).convertAndSendToUser(eq(targetUsername), eq("/queue/webrtc"), any(WebRTCSignalResponse.class)); - verifyNoInteractions(errorHelper); - } - - @Test - @DisplayName("실패 - 대상 사용자가 오프라인일 때 예외를 던짐") - void t2() { - // given - WebRTCAnswerRequest request = new WebRTCAnswerRequest(roomId, targetUserId, "sdp", WebRTCMediaType.AUDIO); - when(sessionManager.getSessionInfo(targetUserId)).thenReturn(null); - - // when & then - CustomException exception = assertThrows(CustomException.class, () -> - controller.handleAnswer(request, headerAccessor, authentication) - ); - - assertThat(exception.getErrorCode()).isEqualTo(ErrorCode.WS_TARGET_OFFLINE); - - verify(validator).validateSignal(roomId, fromUserId, targetUserId); - verifyNoInteractions(errorHelper); - verify(messagingTemplate, never()).convertAndSendToUser(anyString(), anyString(), any()); } } @Nested @DisplayName("ICE Candidate 처리") class HandleIceCandidateTest { - @Test @DisplayName("성공 - ICE Candidate를 특정 사용자에게 전송") void t1() { // given WebRTCIceCandidateRequest request = new WebRTCIceCandidateRequest(roomId, targetUserId, "candidate", "audio", 0); - WebSocketSessionInfo targetSession = new WebSocketSessionInfo(targetUserId, targetUsername, targetSessionId, LocalDateTime.now(), LocalDateTime.now(), null); + WebSocketSessionInfo targetSession = new WebSocketSessionInfo(targetUserId, targetUsername, "session-id", LocalDateTime.now(), LocalDateTime.now(), null); when(sessionManager.getSessionInfo(targetUserId)).thenReturn(targetSession); // when - controller.handleIceCandidate(request, headerAccessor, authentication); + controller.handleIceCandidate(request, authentication); // then verify(validator).validateSignal(roomId, fromUserId, targetUserId); verify(messagingTemplate).convertAndSendToUser(eq(targetUsername), eq("/queue/webrtc"), any(WebRTCSignalResponse.class)); - verifyNoInteractions(errorHelper); - } - - @Test - @DisplayName("성공 - 대상 사용자가 오프라인이어도 조용히 무시") - void t2() { - // given - WebRTCIceCandidateRequest request = new WebRTCIceCandidateRequest(roomId, targetUserId, "candidate", "audio", 0); - when(sessionManager.getSessionInfo(targetUserId)).thenReturn(null); - - // when - controller.handleIceCandidate(request, headerAccessor, authentication); - - // then - verify(validator).validateSignal(roomId, fromUserId, targetUserId); - verifyNoInteractions(errorHelper); - verify(messagingTemplate, never()).convertAndSendToUser(anyString(), anyString(), any()); } } @Nested @DisplayName("미디어 상태 토글") class HandleMediaToggleTest { - @Test @DisplayName("성공 - 미디어 상태를 방 전체에 브로드캐스트") void t1() { // given WebRTCMediaToggleRequest request = new WebRTCMediaToggleRequest(roomId, WebRTCMediaType.AUDIO, true); - doNothing().when(validator).validateMediaStateChange(roomId, fromUserId); // when - controller.handleMediaToggle(request, headerAccessor, authentication); + controller.handleMediaToggle(request, authentication); // then verify(validator).validateMediaStateChange(roomId, fromUserId); verify(messagingTemplate).convertAndSend(eq("/topic/room/" + roomId + "/media-status"), any(WebRTCMediaStateResponse.class)); - verify(messagingTemplate, never()).convertAndSendToUser(anyString(), anyString(), any()); - verifyNoInteractions(errorHelper); - } - - @Test - @DisplayName("실패 - 검증 오류 시 예외를 던짐") - void t2() { - // given - WebRTCMediaToggleRequest request = new WebRTCMediaToggleRequest(roomId, WebRTCMediaType.SCREEN, true); - doThrow(new RuntimeException("검증 실패")).when(validator).validateMediaStateChange(roomId, fromUserId); - - // when & then - assertThrows(RuntimeException.class, () -> controller.handleMediaToggle(request, headerAccessor, authentication)); - verify(validator).validateMediaStateChange(roomId, fromUserId); - verifyNoInteractions(errorHelper); - verify(messagingTemplate, never()).convertAndSend(anyString(), any(WebRTCMediaStateResponse.class)); } } } \ No newline at end of file