프로젝트를 진행하는 과정에서 실시간으로 채팅방 개수를 나타내야 하는 기능이 추가가 되었는데 처음에는 단순히 Socket으로 하면 되지 않을까 생각했는데 다른 방식을 시도해보고 싶었다.
그래서 인터넷을 뒤진 결과 SSE 방식을 알게되었다.
SSE 방식이란?
- SSE는 웹 브라우저와 서버 간 단방향 통신 방식입니다.
- 서버에서 클라이언트로 정보를 전송할 수 있습니다.
- 새로운 채팅방이 생성될 때, 서버에서 클라이언트로 정보를 전송하여 실시간으로 업데이트합니다.
- 서버에 부하가 웹소켓에 비해 적은 편입니다.
서버에 부하가 웹소켓에 비해 적다는게 정말 맘에 들었다.
아래부분은 내가 사용한 코드다.
SseController.java
package shop.dodotalk.dorundorun.sse.Controller;
import java.io.IOException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import shop.dodotalk.dorundorun.chatroom.repository.ChatRoomRepository;
import shop.dodotalk.dorundorun.sse.Entity.SseEmitters;
@Slf4j
@Controller
@RequiredArgsConstructor
public class SseController {
private final SseEmitters sseEmitters;
@GetMapping("/ssehtml")
public String ssehtml() {
return "ssechatroom";
}
@ResponseBody
@GetMapping(value = "/connect", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public ResponseEntity<SseEmitter> connect() {
SseEmitter emitter = new SseEmitter();
sseEmitters.add(emitter);
try {
emitter.send(SseEmitter.event()
.name("connect")
.data("connected!"));
} catch (IOException e) {
throw new RuntimeException(e);
}
return ResponseEntity.ok(emitter);
}
@ResponseBody
@GetMapping("/count")
public ResponseEntity<Void> count() {
System.out.println("count 실행중");
sseEmitters.count();
return ResponseEntity.ok().build();
}
}
SseEmitters.java
package shop.dodotalk.dorundorun.sse.Entity;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import shop.dodotalk.dorundorun.chatroom.entity.ChatRoom;
import shop.dodotalk.dorundorun.chatroom.repository.ChatRoomRepository;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicLong;
@Slf4j
@Component
@RequiredArgsConstructor
public class SseEmitters {
private static final AtomicLong counter = new AtomicLong();
private final List<SseEmitter> emitters = new CopyOnWriteArrayList<>();
public SseEmitter add(SseEmitter emitter) {
this.emitters.add(emitter);
System.out.println("emitter : " + emitter);
System.out.println("emitter list size: " + emitters.size());
emitter.onCompletion(() -> {
System.out.println("만료됨");
this.emitters.remove(emitter); // 만료되면 리스트에서 삭제
});
emitter.onTimeout(() -> {
System.out.println("타임아웃");
emitter.complete();
});
return emitter;
}
public void count() {
long count = counter.incrementAndGet();
emitters.forEach(emitter -> {
try {
emitter.send(SseEmitter.event()
.name("count")
.data(chatRooms.size()));
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
}
ssehtml.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SSE Example</title>
</head>
<body>
<div id="count"></div>
<button onclick="handleCountEvent()">클릭</button>
<script>
const sse = new EventSource("http://localhost:8080/connect");
sse.addEventListener('connect', (e) => {
const { data: receivedConnectData } = e;
console.log('connect event data:', receivedConnectData); // "connected!"
});
sse.addEventListener('count', e => {
const { data: receivedCount } = e;
console.log("count event data:", receivedCount);
handleCountEvent(receivedCount);
});
function handleCountEvent(count) {
console.log("count event data:", count);
const countElement = document.getElementById('count');
countElement.innerText = `현재 채팅방 수: ${count}`;
}
</script>
</body>
</html>
1. EventSource를 사용해서 Sse 연결을 수행했으며 EventSource는 연결이 끊어져도 다시 재연결해주는 기능이 있어서 클라이언트에서 Sse의 연결이 끊길때마다 다시 재연결을 할 수 있다.
--- 서버 배포시 ---
만약 NGINX를 사용하고 있다면 HTTP1.1로 통신 버전을 업그레이드 시켜줘야한다.
참고 : https://tecoble.techcourse.co.kr/post/2022-10-11-server-sent-events/