Error-websocket메세지 보내기 전 종료
| topics | 300-백엔드개발 |
| types | 에러해결 |
| tags | #websocket #spring #error |
WebSocket 메세지 보내기 전 종료
WebSocket 세션이 이미 닫혔는데 메세지 보내려고 하면 터진다.
상황
실시간 알림이나 채팅 기능에서 클라이언트가 갑자기 연결 끊었는데, 서버는 모르고 메세지 보내려다 에러 발생.
java.lang.IllegalStateException: The WebSocket session has been closed
or
java.io.IOException: 현재 연결은 원격 호스트에 의해 강제로 끊겼습니다
왜 발생할까
- 클라이언트가 브라우저 닫음
- 네트워크 끊김
- 클라이언트에서
socket.close()호출 - 타임아웃
문제는 연결 종료가 비동기라서 서버가 즉시 인지 못함.
해결 방법
1. 보내기 전에 세션 상태 확인
public void sendMessage(WebSocketSession session, String message) {
if (session.isOpen()) {
try {
session.sendMessage(new TextMessage(message));
} catch (IOException e) {
log.warn("메세지 전송 실패: {}", e.getMessage());
removeSession(session);
}
}
}
주의:
isOpen()체크해도 그 사이에 닫힐 수 있어서 try-catch는 필수.
2. 세션 관리 제대로 하기
@Component
public class WebSocketSessionManager {
private final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
public void addSession(String id, WebSocketSession session) {
sessions.put(id, session);
}
public void removeSession(String id) {
sessions.remove(id);
}
public void broadcast(String message) {
sessions.values().removeIf(session -> !session.isOpen());
for (WebSocketSession session : sessions.values()) {
try {
session.sendMessage(new TextMessage(message));
} catch (IOException e) {
log.warn("전송 실패, 세션 제거: {}", session.getId());
}
}
}
}
3. Spring WebSocket Handler에서 처리
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
log.info("연결 종료: {} ({})", session.getId(), status);
sessionManager.removeSession(session.getId());
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) {
log.error("전송 에러: {}", exception.getMessage());
sessionManager.removeSession(session.getId());
}
삽질 포인트
브로드캐스트할 때 ConcurrentModificationException 터지는 경우 있음.
왜냐면: 순회하면서 세션 제거하면 안 됨.
해결:
// 삭제할 세션 따로 모으기
List<String> toRemove = new ArrayList<>();
for (Map.Entry<String, WebSocketSession> entry : sessions.entrySet()) {
if (!entry.getValue().isOpen()) {
toRemove.add(entry.getKey());
}
}
toRemove.forEach(sessions::remove);
결론
- 보내기 전에
isOpen()체크 - 그래도 try-catch 필수
- 세션 종료 시 정리 로직 확실히