๊ธฐ์กด MVP ๋ชจ๋ธ์์ ์ค์๊ฐ ์๋ฆผ์ ์ํด ํด๋ง ๊ธฐ๋ฐ์ผ๋ก ๋ฉ์์ง ์๋ฆผ์ ๊ตฌํํ๋ค.
20์ด ์ฃผ๊ธฐ๋ก ํด๋ง(polling) ์ค์ ํ์ฌ ์๋ฆผ ๋ฉ์์ง ๊ฐฏ์๋ฅผ ์ ๋ฌํ๋ ๋ฐฉ์์ด์๊ณ ,
AWS ํ๋ฆฌํฐ์ด ๊ธฐ๋ฐ ์ต์ ์คํ์ผ๋ก ์๋ฒ๋ฅผ ๊ตฌ์ฑํ๊ณ ์์ด ๋น์ฉ๊ณผ ๋ถํ๋ฅผ ์ต์ํํ๊ธฐ ์ ๊ทผ ๋ฐฉ์์ด์๋ค.
ํ์ง๋ง ์๋ฆผ ๊ธฐ๋ฅ์ ์ต์ํ์ ์ค์๊ฐ์ฑ์ ๋ณด์ฅํ์ง ๋ชปํ๋ ํผ๋๋ฐฑ์ด ์์ด ๋ค๋ฅธ ์ ๊ทผ ๋ฐฉ๋ฒ์ด ํ์ํ๋ค.
์ต์ํ์ ์ค์๊ฐ์ฑ์ ๋ณด์ฅํ๊ธฐ ์ํด ์งง์ ํด๋ง ์ฃผ๊ธฐ๋ ๊ฒํ ํ์ง๋ง,
๋ฎ์ ์คํ์ ์๋ฒ ํ๊ฒฝ์์ ๋ถํ๋ฅผ ๋ค์ ์ฆ๊ฐํ๋ ๋ฌธ์ ๊ฐ ์์ด ํด๋น ๋ฐฉ๋ฒ์ ์ ํฉํ์ง ์์ ๋ค๋ฅธ ์ ๊ทผ ๋ฐฉ์์ด ํ์ํ๋ค.
๊ทธ๋์ ๋์ ์ค ํ๋๋ก SSE(server-sent-event) ๋ฅผ ๊ฒํ ํ ๊ณผ์ ๊ณผ ์ด๋ฅผ ์ ์ฉํ๋ ๊ณผ์ ์ ์ ์ด๋ณด๊ณ ์ ํ๋ค.

1. ์ค์๊ฐ์ฑ์ ์ํ ์ ํ: WebSocket vs SSE ??
์ต์ํ์ ์ค์๊ฐ์ฑ์ ๋ณด์ฅํ๊ธฐ ์ํด WebSocket ๊ณผ SSE ์ ๊ฒํ ํ๊ธฐ ์ด์ ์ ๋น์ฆ๋์ค ํน์ฑ์ ๊ฒํ ํด๋ณด๊ธฐ๋ก ํ๋ค.
ํ์ฌ ์๋ฆผ ๋ฉ์์ง ํน์ฑ์ ๊ฐ์ธํ๋ ๋ฐ์ดํฐ๋ฅผ ๊ธฐ๋ฐํ ์ด๋ฒคํธ ํธ์๊ฐ ๋ง์๋ค.
์๋ฅผ ๋ค์ด, ๊ฒ์๊ธ์ ๋ฆฌ๋ทฐ ์์ฑ ๋๋ ์ฒดํฌ๋ฆฌ์คํธ์ ์ํ๊ฐ ๋ณ๊ฒฝ๋๋ฉด
์์ฑ์์๊ฒ ์๋ฆผ์ด ์ ๋ฌ๋๋ ํํ ๋ชจ๋ ๊ฐ์ธํ๋ ์๋ฆผ์ ์ ๋ฌํ๋ ๋ฐฉ์์ด์๋ค.
๋ ๋ฐฉ์ ๋ชจ๋ ๋ถํ์ํ ํด๋ง์ ์ต์ํํ ์ ์๋ค.
WebSocket ์ ๊ฒฝ์ฐ ์๋ฐฉํฅ์ฑ ํต์ ์ ๊ดํ ์๊ตฌ์ฌํญ์ ์ถฉ์กฑํ๊ธฐ ์ํ ์ผ๋ฐ์ ์ธ ์ ๊ทผ ๋ฐฉ๋ฒ์ด๋ค.
ํ์ง๋ง, ํ์ฌ ๋น์ฆ๋์ค ๋ก์ง ํน์ฑ์ ๊ณ ๋ คํ๋ฉด ์๋ฒ์์ ์ฌ์ฉ์์๊ฒ ๋จ๋ฐฉํฅ ๋ฉ์์ง๋ฅผ ์ ๋ฌํ๋ ๋ฐฉ์์ด๋ฏ๋ก
์๋ฒ์์ ํด๋ผ์ด์ธํธ๋ก์ ๋จ๋ฑกํฅ ํต์ ์ ๊ดํ ์๊ตฌ์ฌํญ๋ง์ ์ถฉ์กฑํ๋ SSE ์ฌ์ฉ์ด ์ ํฉํ๋ค๊ณ ํ๋จํ๋ค.
์ถ๊ฐ์ ์ผ๋ก SSE๋ ์ถ๊ฐ์ ์ธ ํ๋กํ ์ฝ์ ๊ดํ ์ง์์ด ํ์ํ์ง ์์ ๊ธฐ์ ์ ์ฉ์ ํ๋ค์ด ๋ฎ์ ์ฅ์ ์ด ์์๋ค.
๊ธฐ์กด HTTP protocol ์ ๊ธฐ๋ฐํ TCP Connection ์ ์งํ๋ ๋ฐฉ์์ผ๋ก ๋์ํ๊ธฐ ๋๋ฌธ์ด์๋ค.
(์ฐธ๊ณ ๋ก Websocket ์ ws protocol ๊ธฐ๋ฐ์ผ๋ก ๋์ํ๋ฏ๋ก ์ถ๊ฐ์ ์ธ ์ง์์ด ํ์ํ๋ค.)
(์ฐธ๊ณ )
SSE ๋ Auto Reconnect ์ง์๊ณผ Cookie ์ฒจ๋ถ ์์ฒญ์ด ๊ฐ๋ฅํ๊ณ ์๋ต ํํ๋ฅผ text ๋ก ์ ๋ฌํ๋ ํน์ฑ์ ๊ฐ์ง๋ค.
๋ฐ๋ฉด, WebSocket ์ ๊ฒฝ์ฐ์๋ Auto Reconnect ์ง์์ ํ์ง ์๊ณ text ๋๋ binary ํํ๋ก ๋ฐ์ดํฐ ์ ๋ฌ์ด ๊ฐ๋ฅํ๋ค.
2. SSE ๋์ ๋ฐฉ์
SSE ๋ํ HTTP/1.1 ๊ธฐ๋ฐ์ผ๋ก ๋์ํ๊ธฐ ๋๋ฌธ์ TCP ๊ธฐ๋ฐ์ ์ฐ๊ฒฐ ์์ ์ ์งํํ๋ค.
- TCP 3-way handshake ๊ณผ์ ์ ํตํด TCP Connection ์ ์๋ฆฝํ๋ค.
- ํด๋ผ์ด์ธํธ๋ ์์ฒญ์ผ๋ก HTTP ํค๋์ Accept: text/event-stream ๋ก ์ค์ ํ์ฌ ์์ฒญํ๋ค.
- ์๋ฒ๋ ์๋ต์ผ๋ก HTTP ํค๋์ Content-Type: text/event-stream ์ผ๋ก ์ ๋ฌํ๋ค.
- ์๋ฒ์์ ํด๋ผ์ด์ธํธ ์ชฝ์ผ๋ก ์ง์์ ์ผ๋ก ์คํธ๋ฆฌ๋ฐ์ ํตํด ๋ฐ์ดํฐ๋ฅผ ํธ์ํ๋ค.

3. SSE ์ฃผ์ ์ฌํญ - ์ฐ๊ฒฐ ์๋ฆฝ๋ ์๋ฒ์๊ฒ๋ง ๋ฐ์ดํฐ ์ ๋ฌ์ด ๊ฐ๋ฅํ๋ค.
SSE ๋ฅผ ์ฌ์ฉํ ๊ฒฝ์ฐ ๋ฉ์์ง๋ Connection ์ ์๋ฆฝํ ์๋ฒ์์๋ง ๋ฉ์์ง ์คํธ๋ฆฌ๋ฐ์ด ๊ฐ๋ฅํ๋ค.
TCP ๋ ์ฐ๊ฒฐ์งํฅ ํ๋กํ ์ฝ์ด๋ค.
TCP ์ฐ๊ฒฐ์ Client IP, Client Port, Server IP, Server Port ์ ๋ณด๋ฅผ ํ ๋๋ก ๊ณ ์ ํ๊ฒ ์๋ณ๋๋ค.
4๊ฐ์ง ์ ๋ณด๋ฅผ ํตํด ํ๋์ ์ปค๋ฅ์ ์์ผ์ ์ ์ํ๋ค.
์ปค๋ ๋ฉ๋ชจ๋ฆฌ์ TCP ์ปค๋ฅ์ ์ํ ํ ์ด๋ธ(connection-state table) ์ ํด๋น ์ ๋ณด๋ฅผ ์ ์ฅํ๋ค.
์ปค๋ ๋ฉ๋ชจ๋ฆฌ๋ OS ๋จ์๋ก ๊ด๋ฆฌ๋๊ธฐ ๋๋ฌธ์ ์ปค๋ฅ์ ์ ์๋ฆฝํ ์๋ฒ์์๋ง ์ ์ง๋๋ฉฐ ๋ค๋ฅธ ์๋ฒ์์ ์ด ์ฐ๊ฒฐ ์ํ๋ฅผ ์ ์ ์๋ค. (e.g. ๋ฒํผ, ์์, ํจํท ๋ฒํธ ๋ฑ)
State Recv-Q Send-Q Local Address:Port Peer Address:Port
------------------------------------------------------------------------
LISTEN 0 511 *:80 *:*
SYN-RECV 0 0 192.0.2.145:80 203.0.113.5:35449
SYN-RECV 0 0 192.0.2.145:80 203.0.113.27:53599
ESTAB 0 0 192.0.2.145:80 203.0.113.27:33605
TIME-WAIT 0 0 192.0.2.145:80 203.0.113.47:50685
(1) State : TCP ์ํ (LISTEN, SYN-RECV, ESTAB, ๋ฑ)
(2) Recv-Q : ์์ ํ์ ์์ธ ๋ฐ์ดํฐ (์ฝํ์ง ์์)
(3) Send-Q : ์ก์ ํ์ ์์ธ ๋ฐ์ดํฐ (์์ง ์ ์ก ์ ๋จ)
(4) Local Address:Port : ์๋ฒ ์ธก IP์ ํฌํธ
(5) Peer Address:Port : ํด๋ผ์ด์ธํธ ์ธก IP์ ํฌํธ
4. Scale-out ํ๊ฒฝ์์ SSE ๋ pub/sub ๊ตฌ์กฐ ๋์ ์ ๊ณ ๋ คํด์ผ ํ๋ค.
ALB ์ ํตํด ๋ก๋ ๋ฐธ๋ฐ์ฑํ๊ณ ์๋ค๋ฉด ๋ฉ์์ง๊ฐ SSE Connection ์ด ์๋ฆฝ๋ ์๋ฒ๋ก ์ ๋ฌ์ ๋์ง ์์ ๊ฐ๋ฅ์ฑ์ด ๋๋ค.
ALB๋ ์ HTTP ์์ฒญ์ด ๋์ฐฉํ ๋๋ง๋ค L7 ๊ธฐ๋ฐ ๋ก๋ ๋ฐธ๋ฐ์ฑ ์๊ณ ๋ฆฌ์ฆ์ ๋ฐ๋ผ ๋ผ์ฐํ ํ ์๋ฒ๋ฅผ ์ ํํ๋ค.
(L7 ๊ธฐ๋ฐ ๋ก๋ ๋ฐธ๋ฐ์ฑ ์๊ณ ๋ฆฌ์ฆ : ๋ผ์ด๋๋ก๋น, IP ํด์, ๊ฐ์ค์น ๋ฑ)
ALB ๋ ๋งค ์์ฒญ๋ง๋ค ์๋ก ๋ฐฑ์๋ ์๋ฒ๋ฅผ ์ ํํ๊ณ ์ ์์ผ์ ์์ฑํ๊ฑฐ๋ ๊ธฐ์กด ์์ผ ํ์์ ํด๋น ์๋ฒ๋ก ์ฐ๊ฒฐ๋ ์์ผ์ ์ ํํด ์ฌ์ฉํ๋ค.
์ฆ, ์์ผ ํ ์ด๋ธ์ ํญ๋ชฉ์ด ๋ณ๊ฒฝ๋๋ ๊ฒ ์๋๋ผ, ALB ๊ฐ ๊ด๋ฆฌํ๋ ์ปค๋ฅ์ ํ ๋ด์์ ๋ค๋ฅธ ์๋ฒ์ ์ฐ๊ฒฐ๋ ์์ผ์ ์ ํํด ์ฌ์ฉํ๋ ๊ฒ์ด๋ค.
์ด์ ๊ฐ์ ์ด์ ๋ก ์ธํด ์ค์ผ์ผ ์์ ํ๊ฒฝ์์ ์ปค๋ฅ์ ์๋ฆฝ๋ ์๋ฒ์์ ๋ฉ์์ง ์คํธ๋ฆฌ๋ฐ์ ์ง์ ๊ฐ๋ฅํ๊ฒ ํ๊ธฐ ์ํ ๊ณ ๋ฏผ์ด ํ์ํ๋ค.
์ด์ ๊ฐ์ด ํด๋ผ์ด์ธํธ ์ ๋ณด๋ฅผ ๊ฐ์ง ์๋ฒ๊ฐ ๋ฉ์์ง ์ ์ ํ๊ฒ ๋ฉ์์ง ํธ์ํ๊ธฐ ์ํด pub/sub ๊ตฌ์กฐ๋ฅผ ํตํด ํด๋น ์๋ฒ๊ฐ ์คํธ๋ฆฌ๋ฐํ๊ธฐ ์ํ ๋ฉ์์ง๋ฅผ ๊ตฌ๋ ํ๊ณ ํด๋น ๋ฉ์์ง๊ฐ ์คํธ๋ฆฌ๋ฐ ํด์ผํ๋ ๋ฉ์์ง๋ฅผ ํ๋ณํ์ฌ ํธ์ํ๋๋ก ๊ตฌ์ฑํ ์ ์๋ค.

5. Spring ์์ SSE ๋์ ์ ๋์์ฑ์ ๊ณ ๋ คํด์ผ ํ๋ ์ด์
์คํ๋ง์์๋ SseEmitter ๋ฅผ ์ ๊ณตํด ์์ฝ๊ฒ SSE ๋ฅผ ์ฌ์ฉํ ์ ์๋ค. ํด๋น SseEmitter ๋ฅผ ์์ฑ ํ, ๋ฉ์์ง๋ฅผ ์ ๋ฌํ๋ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ค.
์ฌ์ฉ์๋ณ ๋๋ ๊ธฐ๊ธฐ๋ณ๋ก Emitter ๋ฅผ ๊ด๋ฆฌํ๊ธฐ ์ํด์๋ ์ ํ๋ฆฌ์ผ์ด์ ๋ฉ๋ชจ๋ฆฌ์ Emitter ๋ฅผ ๊ด๋ฆฌํด์ผ ํ๋ค.
final SseEmitter sseEmitter = new SseEmitter(600 * 1000L);
ํ๊ฐ์ง ์ฃผ์ํด์ผ ํ ์ ์ ๋์์ฑ์ ๊ณ ๋ คํด์ผ ํ๋ค.
SseEmitter ์์ ์ ๊ณตํ๋ ์ฝ๋ฐฑ ํจ์์ธ onCompletion, onTimeout, onError ๋ฉ์๋๋ ๋ณ๋์ ์ค๋ ๋์์ ๋น๋๊ธฐ์ ์ผ๋ก ๋์ํ๋ค
๊ทธ๋ฌ๋ฏ๋ก ํ์ฌ ์์ฒญ ์ค์ ์ฒ๋ฆฌ ์ค์ธ ์ค๋ ๋์ ๋์์ฑ ์ด์๊ฐ ๋ฐ์ํ ๊ฐ๋ฅ์ฑ์ด ์์ผ๋ฏ๋ก ๋์์ฑ์ ๊ณ ๋ คํ ์ค๊ณ๊ฐ ํ์ํ๋ค.
๋ํ์ ์ธ ์์๋ก๋ Emitter ๋ฅผ ๊ด๋ฆฌํ๋ ์๋ฃ ๊ตฌ์กฐ๋ฅผ ์ฌ์ฉํ ๊ฒฝ์ฐ ๋์์ฑ ์ ์ด๋ฅผ ์ง์ํ๋ ์๋ฃ๊ตฌ์กฐ ์ฌ์ฉ์ ๊ถ์ฅํ๋ค.
// key : memberId
private final Map<UUID, SseEmitter> sseEmitters = new ConcurrentHashMap<>();
์๋ SseEmitter ๋ฅผ ๊ตฌ์ฑํ๋ ์์ ์ฝ๋์ด๋ค.
@Slf4j
@Component
public class SseEmitters {
// key : memberId
private final Map<UUID, SseEmitter> sseEmitters = new ConcurrentHashMap<>();
public SseEmitter add(UUID clientId) {
final SseEmitter sseEmitter = new SseEmitter(600 * 1000L);
if (sseEmitters.containsKey(clientId)) {
return sseEmitters.get(clientId);
} else {
sseEmitters.put(clientId, sseEmitter);
}
sseEmitter.onCompletion(() -> {
log.info("onCompletion sseEmitter : {}", sseEmitter);
sseEmitters.remove(clientId);
});
sseEmitter.onTimeout(() -> {
log.info("onTimeout sseEmitter : {}", sseEmitter);
sseEmitters.remove(clientId);
});
sseEmitter.onError(e -> {
log.info("onError sseEmitter : {}, error: {}", sseEmitter, e.getMessage());
this.sseEmitters.remove(clientId);
});
log.info("added sseEmitter id : {}, sseEmitters size: {}", clientId, sseEmitters.size());
return sseEmitter;
}
public SseEmitter remove(UUID memberId) {
if (!sseEmitters.containsKey(memberId)) {
throw new NotificationEmitterNotFoundException(NotificationErrorType.EMITTER_NOT_FOUND);
}
log.debug("removing sseEmitter for memberId: {}", memberId);
final SseEmitter removedEmitter = sseEmitters.remove(memberId);
removedEmitter.complete();
return removedEmitter;
}
public <T> boolean send(UUID clientId, String eventName, T data) {
if (!sseEmitters.containsKey(clientId)) {
return false;
}
SseEmitter emitter = sseEmitters.get(clientId);
try {
emitter.send( // ๋ฉ์์ง ์ ๋ฌ
SseEmitter.event()
.name(eventName)
.data(data)
.build());
return true;
} catch (Exception exception) {
log.info("send sseEmitter error : {}", exception.getMessage());
sseEmitters.remove(clientId);
emitter.completeWithError(exception);
}
return false;
}
}
6. ํด๋ผ์ด์ธํธ๋ ์ผ์ ์๊ฐ ๋ฉ์์ง๋ฅผ ์ ๋ฌ๋ฐ์ง ๋ชปํ๋ฉด ์ฐ๊ฒฐ์ด ๋์ด์ง๋ค. (health check )
SSE ๋ ์ฐ๊ฒฐ์ ์ ์งํ์ฌ ๋ฉ์์ง๋ฅผ ๋จ๋ฐฉํฅ์ผ๋ก ์ก์์ ํ๋ ๊ตฌ์กฐ์ด๋ค.
SSE ๋ ํจ์จ์ ์ธ ์ฐ๊ฒฐ์ ์ํด ์๋ฒ์์ ํด๋ผ์ด์ธํธ ๋ฐฉํฅ์ผ๋ก health check ๋ฅผ ํด์ผ ํ๋ค.
๊ทธ๋ ๊ธฐ ๋๋ฌธ์ ์ผ์ ์๊ฐ๋์ ๋ฉ์์ง๋ฅผ ํด๋ผ์ด์ธํธ์์ ์์ ๋ฐ์ง ๋ชปํ๋ฉด ์ฐ๊ฒฐ์ ํด์ ํ๊ธฐ ๋๋ฌธ์ health check ๋ฅผ ์ํ ์์์ ๋ฉ์์ง๋ฅผ ์ฃผ๊ธฐ์ ์ผ๋ก ์ ๋ฌํ๋ ๋ก์ง์ ๊ตฌ์ฑํด์ผ ํ๋ค. (default : 45์ด)
Reference
- [naver d2] TCP/IP ๋คํธ์ํฌ ์คํ ์ดํดํ๊ธฐ : https://d2.naver.com/helloworld/47667
- [TOSS] SLASH 24 - SSE ์ด๋ฒคํธ ํธ์ฌ๋ก ๋ถํ์ํ Polling ์ ๊ฑฐํ๊ธฐ : https://www.youtube.com/watch?v=ovGgdPPUZ2I
- TCP Connection States : https://maxnilz.com/docs/004-network/003-tcp-connection-state/
- [tecoble] Spring์์ Server-Sent-Events ๊ตฌํํ๊ธฐ : https://tecoble.techcourse.co.kr/post/2022-10-11-server-sent-events/