*본 글은 회사의 코드와 연관이 없으며, 업무 외의 시간에 공부 목적으로 쓴 글입니다
기존 풀링 방식에서는 5초에 한 번씩 땡겨서 사용했음
→ 실시간으로 채팅이 막 오면 동시성을 보장하기 어렵고
바로바로 채팅이 반영이 안되니까 사용자 사용성이 매우매우 떨어짐
또한 채팅을 이용하는 사람 수와 채팅방이 많아지면 서버에 부하가 매우 많이 걸릴 것이라고 예상함
기능자체는 잘 되지만, 성능면에서 매우 비효율적임
POST /api/chat/send): 클라이언트가 메시지를 보내면, 서버는 DB에 저장하고 저장된 메시지 객체를 그대로 클라이언트에게 응답합니다.GET /api/chat/messages/{courseId}): 클라이언트는 useEffect 안에서 setInterval을 사용해 5초마다 이 API를 호출합니다. 서버는 DB에서 최신 메시지 목록을 조회해 전달합니다.1. 완벽한 실시간 통신
WebSocket은 서버와 클라이언트 간에 하나의 연결 통로(TCP 연결)를 계속 열어두고, 양방향으로 원할 때 언제든지 데이터를 주고받을 수 있다
서버에 새로운 메시지가 오면, 서버가 즉시 클라이언트로 메시지를 밀어주는 방식이므로 실시간 반영이 가능!
2. 서버 부하 감소
한번 연결을 맺으면 불필요한 요청을 반복하지 않는다
-> 폴링 방식에 비해 서버 자원 낭비를 크게 줄일 수 있음
3. 메시징 프로토콜(STOMP)
WebSocket은 통신 통로일 뿐, 그 안에서 어떤 형식으로 데이터를 주고받을지에 대한 규칙은 없다
STOMP를 함께 사용하면 메시지의 구독-발행 모델을 쉽게 구현할 수 있음
implementation 'org.springframework.boot:spring-boot-starter-websocket'
/topic/app: 클라이언트가 서버로 메시지를 발행할 때 사용하는 경로/app/chat/send/ws-chat// src/main/java/com/yourproject/config/WebSocketConfig.java
package com.yourproject.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@Configuration
@EnableWebSocketMessageBroker // STOMP 사용을 위한 어노테이션
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 1. 구독자에게 메시지를 보낼 때 사용할 경로의 prefix 설정
// "/topic"으로 시작하는 경로를 구독하는 클라이언트에게 메시지를 보냄
registry.enableSimpleBroker("/topic");
// 2. 클라이언트가 서버로 메시지를 보낼 때 사용할 경로의 prefix 설정
// "/app"으로 시작하는 경로로 메시지를 보내면 @MessageMapping이 붙은 메소드로 라우팅됨
registry.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// 3. 클라이언트가 웹소켓에 연결하기 위한 최초의 경로(endpoint) 설정
// 예: const client = new Stomp.Client({ brokerURL: 'ws://localhost:8080/ws-chat' });
registry.addEndpoint("/ws-chat")
.setAllowedOriginPatterns("*") // CORS 문제 해결을 위해 Origin 패턴 설정
.withSockJS(); // SockJS 사용 설정 (웹소켓 미지원 브라우저 호환)
}
}
스프링 시큐리티를 사용하고 있으므로 토큰에서 역할과 이름을 파싱해와야함.
→ 인증객체에서 유저 정보를 가져와서 덮어쓰기
// ChatSocketController.java
@Controller
@RequiredArgsConstructor
public class ChatSocketController {
private final ChatMessageRepository chatMessageRepository;
// 1. 핵심 비즈니스 로직
@MessageMapping("/chat/send/{courseId}")
@SendTo("/topic/chat/room/{courseId}")
public ChatMessage sendMessage(
@DestinationVariable Long courseId,
@Payload ChatMessage chatMessage,
Authentication auth
) {
String username = auth.getName();
String role = auth.getAuthorities()
.stream()
.map(GrantedAuthority::getAuthority)
.findFirst()
.orElse("ROLE_STUDENT")
.replace("ROLE_", "");
chatMessage.setUsername(username);
chatMessage.setRole(role);
chatMessage.setCourseId(courseId);
return chatMessageRepository.save(chatMessage);
}
// 2. 예외 처리 전담 메소드
@MessageExceptionHandler
@SendToUser("/topic/errors")
public String handleException(Throwable exception) {
// 여기서 예외를 로깅하거나, 클라이언트에게 보낼 에러 메시지를 가공
System.err.println("An error occurred: " + exception.getMessage());
return "오류가 발생했습니다: " + exception.getMessage();
}
}