Spring 채팅에서 브라우저 종료 후 남은 참여 정보 정리
브라우저를 강제로 닫아도 채팅방 인원이 남았습니다
WebSocket으로 채팅 서비스를 만들던 중, 브라우저를 강제 종료해도 채팅방 인원수가 줄지 않는 문제가 있었습니다.
당시 로직은 클라이언트의 DISCONNECT 요청을 받으면 인원수를 줄이는 방식이었습니다. 요청이 오지 않으면 입장한 사용자가 그대로 남았습니다.
세션과 채팅방, 사용자를 연결해 저장했습니다
먼저 현재 세션 ID가 어느 채팅방의 누구에게 속하는지 저장했습니다. 아래는 당시 코드의 일부로, 클래스의 끝이나 다른 분기 등은 생략되어 있습니다.
public class Session {
@Id @GeneratedValue
@Column(name = "session_id")
private Long id;
@ManyToOne
@JoinColumn(name = "chatroom_id")
private ChatRoom chatRoom;
@ManyToOne
@JoinColumn(name = "member_id")
private Member member;
@Column
private String nowSessionId;이후 인바운드 채널에 인터셉터를 등록했습니다.
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(stompHandler);
}SUBSCRIBE를 처리할 때는 세션 ID, 방 ID, 사용자 ID를 DB에 저장하고 채팅방 인원수를 갱신합니다.
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
switch (accessor.getCommand()) {
...
case SUBSCRIBE:
String roomId = accessor.getFirstNativeHeader("roomId");
Long uidFromToken = jwtService.getUidFromToken(accessor.getFirstNativeHeader("Authorization"));
// SUBSCRIBE 동작시 session에 현재 유저 아이디 uid와, 메세지 헤더 정보의 roomId를 매핑해서 DB에 저장
Long sessionId = sessionService.setEnterInfo(accessor.getSessionId(), roomId, uidFromToken);
chatService.countPeopleChatRoom(roomId, "SUBSCRIBE");
log.info("SUBSCRIBE : [ {} ] [ {} ] [ {} ]", sessionId, accessor.getSessionId(), roomId);
break;
...
}연결이 끝났을 때 정리할 참여 정보를 찾으려고 세션과 사용자를 묶어 두었습니다. 세션 정보를 저장해도 연결이 살아 있는지 주기적으로 검사하지는 않습니다.
DISCONNECT에서 참여 정보를 정리했습니다
같은 인터셉터에 DISCONNECT 분기를 두었습니다. 세션 ID로 저장된 정보를 찾고 인원수를 갱신한 뒤 세션을 삭제합니다. checkCountPeople이 참이면 채팅방도 삭제합니다.
Spring의 STOMP 이벤트 문서에는 클라이언트가 DISCONNECT를 보내지 않아도 WebSocket이 닫히면 종료 처리가 자동으로 일어날 수 있다고 나옵니다. DB에 세션 정보를 저장하는 일과 연결 이상을 감지하는 일은 별개입니다.
종료 이벤트는 한 세션에 여러 번 발생할 수도 있어서 반복 호출에도 안전해야 합니다. 그런데 아래 코드는 이미 지워진 세션을 다시 찾으면 예외를 던집니다. 당시 확인한 동작만으로는 모든 강제 종료나 네트워크 단절 상황에서도 안전한지 장담하지 못합니다.
case DISCONNECT:
session = sessionRepository.findSessionByNowSessionId(accessor.getSessionId())
.orElseThrow(() -> new StompConversionException("올바른 세션이 아닙니다."));
checkCountPeople = chatService.countPeopleChatRoom(session.getChatRoom().getRoomId(), "DISCONNECT");
sessionRepository.delete(session);
if (checkCountPeople) {
chatRoomRepository.deleteById(session.getChatRoom().getRoomId());
}
break;이렇게 바꾸고 나서는 브라우저를 강제 종료해도 인원수가 정리됐습니다. 다만 어떤 경로로 DISCONNECT 처리가 호출됐는지는 자세히 이해하지 못했습니다. 클라이언트가 요청을 보내지 않아도 서버가 연결 이상을 알아채고 종료 처리를 수행한 것이 아닐까요? 이때는 그렇게 추측했습니다.