API 호출 학습을 진행하던 중, 사용자가 채팅 페이지를 벗어나는 순간 마지막으로 읽은 메시지 정보를 업데이트하는 API를 호출하는 방식으로 구현하려 했습니다. 하지만 페이지를 벗어날 때마다 API를 호출하면 서버에 부담을 줄 수 있다는 점을 인지하게 되었습니다.
이를 개선하기 위해 STOMP 세션 관리를 도입하여 WebSocket 연결이 끊어질 때 서버에서 lastReadMsgId
필드를 자동으로 업데이트하도록 리팩토링을 진행했습니다.
HandshakeInterceptor
를 구현하여 세션 속성에 인증된 사용자 정보를 저장DispatcherServlet
을 기준으로 Filter는 클라이언트의 최초 요청을 처리하며, Interceptor는 비즈니스 로직과 관련된 작업을 담당합니다. 이를 시각적으로 이해하기 쉽게 표현한 이미지는 아래와 같습니다:
StompHeaderAccessor
를 사용하면 HandshakeInterceptor
에서 저장한 세션 속성에 접근할 수 있습니다.
이벤트 리스너를 활용해 연결이 종료될 때 마지막으로 읽은 메시지 ID를 업데이트합니다.
public class CustomHandshakeInterceptor implements HandshakeInterceptor {
private final TokenService tokenService;
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String accessToken = httpServletRequest.getHeader("Authorization");
String provider = httpServletRequest.getHeader("provider");
boolean isValidToken = tokenService.isValidToken(accessToken, provider);
if (isValidToken) {
attributes.put("user", tokenService.getUserFromToken(accessToken.substring(7), provider));
return true;
} else {
response.setStatusCode(HttpStatus.UNAUTHORIZED); // 인증 실패
return false;
}
}
}
@EventListener
public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) {
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
User user = (User) headerAccessor.getSessionAttributes().get("user");
String sessionId = headerAccessor.getSessionId();
Long chatRoomId = Long.parseLong(headerAccessor.getDestination().substring(DESTINATION_ROUTE.length()));
if (user != null) {
log.info("Client disconnected: " + sessionId + " (User: " + user.getNickname() + ")");
ChatMessageDto chatMessageDto = chatMessagePolicy.getLastChatMessage(chatRoomId);
chatActivityService.updateLastReadMessage(chatRoomId, new ChatActivityRequestDto(user.getId(), chatMessageDto.chatMessageId()));
} else {
log.error("Client disconnected: " + sessionId + " (User: Unknown)");
}
}
기존에는 API 호출 방식만 고집했지만, 세션 관리 방식을 도입하며 더 효율적인 설계를 구현할 수 있었습니다. 앞으로는 도전해야 할 부분을 더 과감히 시도해봐야겠다고 느꼈습니다.