랜덤 채팅 문제 해결
문제
랜덤 채팅을 시도를 하는 과정에서 접속자 수가 1명인 상태에서 20초가 초과되면 자동으로 접속이 끊겼다.
그런데 랜덤 채팅을 test 하던 과정에서 20초 시간이 지나기 전 강제 종료 후 다시 랜덤 채팅을 시도할 경우
기존 통신 시도와 새로 시작하는 것이 섞여서 띄워지게 되었다.
그래서 client에서는 기존에 통신하던 것이 있으면 강제로 종료시키고 (.abort()
)
랜덤 채팅방을 클릭시 status라는 변수에 random이라는 값을 저장하는데
채팅방을 종료시키는 버튼을 누를 때 해당 상태가 random이라면 강제 종료하는 api를 작성했다.
이렇게 코드를 작성하고 실행을 해봤는데 20초를 기다리지 않고 강제로 종료하면
back에서는 websocket 세션이 닫힐 때 실행되는 메소드의 log가 찍혀있다.
Log 분석
랜덤채팅은 접속자가 2명 이상일 경우에만 채팅이 열린다.
그러므로 접속자가 본인 한명인 경우 채팅이 시작되지 않고 종료된다.
채팅이 시작되려면 client에서 socket 통신을 해야지 시작이 된다.
그래서 채팅을 시작하기 전에 종료된 경우 client에서는 통신이 이루어지지 않은 것으로 간주해서
채팅이 연결된 후 시작하는 로직을 진행하지 않는다.
또한 back에서도 채팅이 시작될 때 찍히는 log가 없는 것을 확인했다.
그런데 접속자를 기다리는 과정에서 갑자기 종료를 시켜버리면 채팅이 종료된 것도 아닌데
@EventListener
에서 채팅이 종료되는 메소드를 실행시킨다.
코드를 뜯어가 보면서 문제를 해결해봤다.
문제 해결하기
코드 뜯어보기
채팅은 subscribe를 해야 시작된다.
randomOnConnected()
에서 subscribe가 되고 randomOnConnected()
는 2명 이상 접속되었을 때 실행된다.
1
2
3
if(users>1) {
stompClient.connect({roomId : roomId}, randomOnConnected, onError);
}
1
2
3
4
5
@EventListener
public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) {
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
OAuth2AuthenticationToken token = (OAuth2AuthenticationToken) headerAccessor.getHeader("simpUser");
}
handleWebSocketDisconnectListener 얘는 걍 메소드 명일 뿐이지
파라미터로 주는 SessionDisconnectEvent가 의미있는 것이다.
공식문서 에서 SessionDisconnectEvent
는 STOMP 세션이 종료되면 게시된다.
DISCONNECT는 클라이언트에서 전송되었거나 WebSocket 세션이 닫힐 때 자동으로 생성될 수도 있다.
경우에 따라 이 이벤트는 세션당 두 번 이상 게시될 수 있다.
여기 에서도 SessionDisconnectEvent란 WebSocket 하위 프로토콜로
Simple Messaging Protocol(예: STOMP)을 사용하는 WebSocket 클라이언트의 세션이 닫힐 때 발생하는 이벤트라고 말한다.
session이 열린 곳
그렇다는 것은 세션이 있다라는 얘긴데 세션이 어디서 열려진 것일까?
아래 2개 코드 중 첫번째는 자동으로 인식하고 아래는 Client에서 채팅을 연결할 때 실행되는 코드이다.
이 두개의 메소드에 log를 찍어서 어느 곳이 실행되는지 체크해봤다.
1
2
3
4
5
6
7
@EventListener
public void handleWebSocketConnectListener(SessionConnectedEvent event) {
}
@MessageMapping("every-chat/addUser")
public void everyChatAddUser(@Payload ChatMessage chatMessage, SimpMessageHeaderAccessor headerAccessor){
}
SessionConnectEvent
: 브로커가 CONNECT에 대한 응답으로 STOMP CONNECTED 프레임을 보낸 직후 게시된다.
이 시점에서 STOMP 세션은 완전히 설정된 것으로 간주할 수 있다
그런데 두개의 메소드(handleWebSocketConnectListener, everyChatAddUser) 모두 실행되지 않았다.
그리고 log를 자세히 보니 chat이 시작된다는 log가 찍혀있었다.
해당 log는 2명 이상이 되어야 찍히는 log인데 어째서인지 적혀있다.
그리고 해당 메소드 시작 log는 찍혀있지 않다.
코드로 살펴보면 아래와 같다.
1
2
3
4
5
6
7
public void a(){
log.info("start"); // 1번
if(users<2){
}else{
log.info("[random chat] chat start !!"); // 2번
}
}
1번 코드는 log가 찍혀있지 않는데 2번은 찍혀있는 것이다.
그리고 해당 log가 찍히는 시점을 보니
랜덤 채팅을 입장했을 때가 아닌 20초를 기다리지 않고 강제로 닫기 버튼을 눌렀을 때 해당 로그가 찍힌다.
정상적으로 20초를 기다린 후에 닫기 버튼을 누르면 chat start도 띄워지지 않고 채팅이 시작되지도 않는다는 것을 알았다.
원인 파악하기
코드를 다시 살펴보니 chat이 시작된다는 log는 사용자가 2명 이상일 경우 실행되는 메소드인데
나갔다 들어오면서 2명으로 인식이 되는 것 같았다. (waitingUsers
)
그래서 강제 종료하는 로직에 채팅이 종료되었을 때처럼 제거를 해주되 waitingUsers
접속중인 user를 제거해줬다. (connectedUsers
아님!!)
그런데 제거가 되지 않았다. 안될 수 밖에 없던게 강제 종료하는 메소드에 이미 20초가 지나면 자동취소되는 로직을 재사용하고 있었다.
그래서 이 문제는 아닌 것 같았다.
waitingUsers
를 출력해보니 같은 username이 2개 있었다.
따라서 중복체크를 하면 되면 문제를 해결 할 수 있을 것 같았다.
ChatRoomMap 에 equals()
메소드를 추가하여 nickname 속성으로 중복 체크했다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ChatRoomMap {
private String nickname;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof ChatRoomMap)) return false;
ChatRoomMap other = (ChatRoomMap) o;
return Objects.equals(nickname, other.nickname);
}
@Override
public int hashCode() {
return Objects.hash(nickname);
}
}
equals()
메소드는 nickname
속성을 비교하여 두 ChatRoomMap
객체가 동일한지 확인한다.
hashCode()
메소드는 equals()
메소드와 일관성을 유지하기 위해 nickname
속성을 기반으로 해시 값을 반환한다.
이렇게 equals()
메소드와 hashCode()
메소드를 오버라이드한 후에는 waitingUsers
맵을 사용할 때 중복된 사용자를 방지할 수 있다.
중복된 사용자가 waitingUsers
맵에 이미 존재하는지 확인한 후, 필요한 처리를 수행할 수 있다.
이렇게 작성하고 실행하니 더 이상 disconnect가 실행되지 않았다.
결론
원인은 waitingUsers
의 중복이었고 2명이상이 되면서 백에서 roomId를 생성해서
client 쪽에서 통신을 시작하지는 않았지만 해당 msg를 보내는 통신을 시도하려고 하였고 disconnect를 감지했다.
따라서 강제 종료하는 시점에 waitingUsers
를 제거하고 waitingUsers
를 담는 ChatRoomMap
에서 중복체크를 함으로써 문제를 해결했다!