SSE vs WebSocket
SSE도 WebSocket처럼 실시간으로 데이터를 전달할 수 있지만, WebSocket 프로토콜이 아닌 HTTP 연결 위에서 이루어진다. 또한 서버 -> 클라이언트 단방향 통신이다.
SSE는 알림, 실시간 로그, 상태 전달 등에 주로 사용한다.
Polling
클라이언트가 일정 간격으로 HTTP 요청을 보내 새 데이터가 있는지 확인하는 방식
실시간성이 떨어진다.
@EnableWebSocket 필요addHandler()에서 /ws로 연결한 클라이언트의 메시지를 이 핸들러(WebSocketHandler)가 처리하도록 등록@Configuration
@EnableWebSocket
@RequiredArgsConstructor
public class WebSocketConfig implements WebSocketConfigurer {
private final WebSocketHandler webSocketHandler;
@Override
public void registerWebSocketHandlers(
WebSocketHandlerRegistry registry
) {
registry.addHandler(webSocketHandler, "/ws");
}
}
WebSocketHandler
TextWebSocketHandler: 문자열 WebSocket 메시지 처리용 기본 클래스
이를 상속하면 Spring이 연결, 수신, 종료 시점에 해당하는 메서드를 호출
@Slf4j
@Component
public class WebSocketHandler extends TextWebSocketHandler {
// 연결
@Override
public void afterConnectionEstablished(
WebSocketSession session
) {
log.info("연결됨: id={}", session.getId());
}
// 수신
@Override
protected void handleTextMessage(
WebSocketSession session,
TextMessage message
) throws Exception {
log.info("받음: {}", message.getPayload());
// 이 예제에서는 받은 메시지를 그대로 다시 보내줌
// 상대방이 없기 때문에 이런 방식으로 양방향 통신을 확인
session.sendMessage(message);
}
// 종료
@Override
public void afterConnectionClosed(
WebSocketSession session,
CloseStatus status
) {
log.info("끊김: id={} code={}", session.getId(), status.getCode());
}
}
throw vs throwsthrowpublic void checkAge(int age) {
if (age < 0) {
// 나이가 음수일 경우 강제로 예외 발생
throw new IllegalArgumentException("나이는 음수가 될 수 없습니다.");
}
}
throws// 이 메서드를 호출하는 곳에서 예외를 처리해야 함을 명시
public void readFile() throws IOException {
FileReader file = new FileReader("not_found.txt");
}
handleTextMessage에서는 소켓을 통해 데이터를 전송한다. (session.sendMessage(message))
이때 물리적인 통신 에러(IOException등)가 발생할 수 있고, 이런 네트워크 에러를 호출자(스프링)가 처리하도록 위임하기 위해throws Exception을 붙여준다.
참고로, 표준적인 스프링 웹소켓 개발 관점에서는 afterConnectionEstablished, handleTextMessage, afterConnectionClosed에 모두 throws Exception을 열어두는 것이 일반적이다.
실제로 기본 인터페이스 WebSocketHandler를 확인해보면, 세 메서드 모두 throws Exception이 선언되어 있다.

스프링 컨테이너가 웹소켓 호출부 주변을 데코레이터 패턴(ExceptionWebSocketHandlerDecorator)으로 감싸고 있기 때문에, 메서드 밖으로 예외가 터져나왔을 때 적절한 핸들러를 호출하고 세션을 정리하도록 설계되어 있다.
메서드 내부에서는 비즈니스 로직과 연관된 에러(JSON 파싱 실패 등)만 처리하고, 일반적이고 예측 불가능한 에러(I/O 에러 등)는 컨테이너로 던지는 것이 책임 분리 관점에서 깔끔하다.

마치 ws://localhost:8080/ws으로 바로 웹소켓 통신을 하는 것처럼 보이지만, 내부적으로는 HTTP 요청을 먼저 주고받고, 웹소켓 프로토콜로의 전환이 이루어진다.
WebSocket 연결을 허용하기 전 필요한 처리를 수행하는 곳
- 요청에 필요한 정보가 있는지, 그 값이 유효한지 등을 확인
- 조건에 맞지 않는 요청(인터셉터의
beforeHandshake()에서false를 반환)은 거절- 확인한 정보는 이후에도 메시지 처리에 사용
이전에 공부한 Interceptor의 WebSocket 버전!
@Component
public class NicknameHandshakeInterceptor implements HandshakeInterceptor {
public static final String ATTR_NICKNAME = "nickname";
private static final Pattern NICKNAME = Pattern.compile("^[A-Za-z]{2,12}$"); // 영문만 허용, 2~12 글자
@Override
public boolean beforeHandshake(
ServerHttpRequest request,
ServerHttpResponse response,
WebSocketHandler handler,
Map<String, Object> attributes
) {
String query = request.getURI().getQuery(); // URL에서 쿼리 추출
String nickname = (query != null && query.startsWith("nickname="))
? query.substring("nickname=".length()) : null; // 쿼리에서 필요한 정보 추출
// 값의 유효성 검사
// 정규식 패턴 매칭
// 잘못된 입력은 400 BAD REQUEST
if (nickname == null || !NICKNAME.matcher(nickname).matches()) {
response.setStatusCode(HttpStatus.BAD_REQUEST);
return false;
}
// 검사한 정보를 이후 사용할 수 있도록 보관
attributes.put(ATTR_NICKNAME, nickname);
return true;
}
@Override
public void afterHandshake(
ServerHttpRequest request,
ServerHttpResponse response,
WebSocketHandler handler,
Exception exception
) {
}
}
위에서 attributes.put(ATTR_NICKNAME, nickname);으로 넣어준 정보를 꺼내 활용한다.
@Component
public class WebSocketHandler extends TextWebSocketHandler {
@Override
public void afterConnectionEstablished(
WebSocketSession session
) throws Exception {
String nickname = (String) session.getAttributes()
.get(NicknameHandshakeInterceptor.ATTR_NICKNAME);
TextMessage response = new TextMessage(nickname);
session.sendMessage(response);
}
}
핸들러와 마찬가지로 인터셉터도 등록해주어야 함
@Configuration
@EnableWebSocket
@RequiredArgsConstructor
public class WebSocketConfig implements WebSocketConfigurer {
private final WebSocketHandler webSocketHandler;
private final NicknameHandshakeInterceptor nicknameHandshakeInterceptor;
@Override
public void registerWebSocketHandlers(
WebSocketHandlerRegistry registry
) {
registry.addHandler(webSocketHandler, "/ws")
.addInterceptors(nicknameHandshakeInterceptor);
}
}
| 닉네임이 없을 때 | 닉네임 형식이 틀릴 때 | 닉네임이 올바를 때 |
|---|---|---|
![]() | ![]() | ![]() |