Error-websocket메세지 보내기 전 종료

topics 300-백엔드개발
types 에러해결
tags #websocket #spring #error

WebSocket 메세지 보내기 전 종료

WebSocket 세션이 이미 닫혔는데 메세지 보내려고 하면 터진다.

상황

실시간 알림이나 채팅 기능에서 클라이언트가 갑자기 연결 끊었는데, 서버는 모르고 메세지 보내려다 에러 발생.

java.lang.IllegalStateException: The WebSocket session has been closed

or

java.io.IOException: 현재 연결은 원격 호스트에 의해 강제로 끊겼습니다

왜 발생할까

  1. 클라이언트가 브라우저 닫음
  2. 네트워크 끊김
  3. 클라이언트에서 socket.close() 호출
  4. 타임아웃

문제는 연결 종료가 비동기라서 서버가 즉시 인지 못함.

해결 방법

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 필수
  • 세션 종료 시 정리 로직 확실히