SSE 알림을 설계할 때 확인할 연결 수명과 재연결
팔로우, 좋아요, 댓글처럼 서버에서 발생한 일을 사용자에게 알려주는 기능은 매번 새로고침하지 않아도 화면이 바뀌어야 한다. 이때 먼저 떠올릴 수 있는 방법은 폴링, WebSocket, SSE다. 알림은 서버에서 브라우저로 흐르는 방향이 주로 필요하므로, 프로젝트에서는 SSE(Server-Sent Events)를 선택할 수 있다.
세 가지 방식을 구분하는 기준
| 방식 | 통신 방향 | 연결 방식 | 잘 맞는 기능 |
|---|---|---|---|
| 폴링 | 요청과 응답 | 일정 주기로 새 요청 | 구현이 단순하고 즉시성이 낮아도 되는 상태 확인 |
| WebSocket | 양방향 | 하나의 지속 연결 | 채팅, 협업 편집, 게임 |
| SSE | 서버에서 클라이언트로 | HTTP 스트리밍 연결 | 알림, 진행률, 로그 스트림 |
SSE는 브라우저가 EventSource로 연결을 열고, 서버가 text/event-stream 형식으로 이벤트를 보낸다. 브라우저에서 서버로 메시지를 보내야 하는 기능은 일반 HTTP 요청으로 처리하고, 실시간 수신만 SSE로 분리할 수 있다.
기본 요청 흐름
브라우저
└─ GET /notifications/stream
└─ 서버가 연결 보관
알림 발생
└─ 대상 사용자 연결을 찾아 event: notification 전송
브라우저
└─ message 이벤트를 받아 화면 갱신
클라이언트 예시는 다음처럼 작성할 수 있다.
const source = new EventSource('/notifications/stream');
source.addEventListener('notification', (event) => {
const notification = JSON.parse(event.data);
renderNotification(notification);
});
source.onerror = () => {
// 연결 상태만 표시
};
서버는 이벤트 사이에 빈 줄을 두고 전송해야 한다. id를 함께 보내면 브라우저가 재연결할 때 Last-Event-ID 헤더를 활용할 수 있어, 끊긴 동안의 이벤트를 다시 조회하는 설계로 확장할 수 있다.
연결을 사용자별로 관리하기
연결을 전역 리스트 하나에만 넣으면 사용자가 로그아웃하거나 서버가 재시작된 뒤 오래된 연결이 남을 수 있다. 사용자 식별자를 키로 연결을 관리하고, 연결 종료 시 정리하는 정책이 필요하다.
private final Map<Long, Set<SseEmitter>> emitters = new ConcurrentHashMap<>();
public SseEmitter connect(Long userId) {
SseEmitter emitter = new SseEmitter(30 * 60_000L);
emitters.computeIfAbsent(userId, ignored -> ConcurrentHashMap.newKeySet())
.add(emitter);
Runnable cleanup = () -> remove(userId, emitter);
emitter.onCompletion(cleanup);
emitter.onTimeout(cleanup);
emitter.onError(ignored -> cleanup.run());
return emitter;
}
연결 컬렉션은 동시 접근을 고려해야 한다. 알림을 보내는 스레드가 연결을 순회하는 중에 브라우저가 끊길 수 있기 때문이다. 또한 하나의 사용자에게 여러 탭이 열릴 수 있으므로 Set<SseEmitter>처럼 여러 연결을 허용할지 정책을 먼저 정한다.
운영에서 빠지기 쉬운 부분
첫 이벤트를 보내지 않으면 프록시나 브라우저가 연결을 유휴 상태로 오해할 수 있다. 연결 직후 connected 이벤트를 보내고, 일정 주기의 heartbeat를 전송하면 중간 프록시의 idle timeout을 확인하기 쉽다. heartbeat는 사용자 알림이 아니므로 클라이언트에서 무시하도록 이벤트 이름을 분리한다.
서버가 여러 대로 늘어나면 한 인스턴스가 가진 연결만으로는 다른 인스턴스의 알림을 전달할 수 없다. 이때 Redis Pub/Sub이나 메시지 브로커를 통해 알림 이벤트를 공유하고, 각 인스턴스가 자신에게 연결된 브라우저에 전달하는 구조를 고려한다. 단일 서버에서는 먼저 연결 수, 전송 실패 수, 평균 연결 시간부터 측정한다.
보안과 개인정보
SSE 연결도 인증된 API다. 사용자 ID를 쿼리 파라미터로 받아 신뢰하지 말고 세션이나 토큰에서 인증 주체를 얻어야 한다. 알림 payload에는 화면에 꼭 필요한 최소 정보만 넣고, 다른 사용자의 내부 식별자나 민감한 내용을 그대로 전송하지 않는다. 로그에는 알림 본문 대신 이벤트 유형과 대상 사용자 식별자의 해시처럼 운영에 필요한 값만 남긴다.
SSE는 채팅을 대신하는 기술이 아니라 단방향 스트림을 단순하게 유지하는 선택지다. 연결 수명, 재연결 시점, 다중 인스턴스 전달 방식을 먼저 정하면 구현보다 운영에서 생기는 문제가 크게 줄어든다.
함께 읽기:
원문 기록: Spring Day 24: SSE