์น ์์ผ์ ์์ ๋ถํฐ ์ฌ์ฉํด๋ณด๊ณ ์ถ์๋ค. ์ง์ ํ๋ก์ ํธ์์ ์ค์๊ฐ ์๋ ๊ธฐ๋ฅ์ ์ถ๊ฐํ๋๋ฐ ๊ทธ๋ ์ฌ์ฉํ๋ ค๋ค ์๋์๋ SSE๊ฐ ๋ ์ ์ ํด ๋ณด์๊ณ ์์ผ์ ์ฌ์ฉํ์ง ์์๊ณ ์ด๋ฒ์ ๊ฐ์ธ์ ์ผ๋ก ์ฑํ ์๋น์ค๋ฅผ ๊ตฌํํ๊ธฐ๋ก ํ๋ค.
์น ์์ผ์ ๋ํด ์์๋ณด๊ธฐ ์ http ํต์ ์ ํน์ง๊ณผ ํ๊ณ์ ๋ํด ์์๋ณด์
http ํต์ ์ ํน์ง๊ณผ ํ๊ณ
httpํต์ ์ HyperText Transfer Plotocol์ ์ฝ์๋ก์ ์ค๋๋ ๊ด๋ฒ์ํ๊ณ ์ผ๋ฐ์ ์ผ๋ก ์ฌ์ฉ๋๋ ํต์ ๊ธฐ๋ฒ์ด๋ค.
http์ ํต์ ๊ณผ์ ์ ์๋์ ๊ฐ๋ค.
- client๊ฐ server์๊ฒ ์์ ์ด ๋ฐ๊ณ ์ถ์ ์ ๋ณด๋ฅผ request์ ๋ด์ ์ ์กํ๋ค.
- server๋ client์ request์ ๋ฐ๋ผ์ ์๋ง์ response๋ก ์๋ตํ๋ค.
- client๋ server์๊ฒ ๋ฐ์ response์ ๋ฐ์ดํฐ๋ฅผ ์ฌ์ฉํ๋ค.
์ฆ ์๋ฒ์์ ๋ฐ์ดํฐ๋ฅผ ์ ๊ณตํ๋ ์์ ์ client๊ฐ server์ ์์ฒญ์ ํ์ ๋ ์ด๋ค.
httpํต์ ์ ๊ฐ์ฅ ํฐ ํน์ง ์ค Stateless, Connectionless๋ผ๋ ํน์ง์ด ์๋ค.
client๊ฐ server์ request๋ฅผ ๋ณด๋ด๋ฉด, ์๋ฒ๋ ํด๋ผ์ด์ธํธ์๊ฒ response๋ฅผ ํ๊ณ , ๊ทธ ์ดํ ์ฐ๊ฒฐ์ด ๋์ด์ ธ ์๋ฒ์ ํด๋ผ์ด์ธํธ๋ ๋
๋ฆฝ๋ ์ํ๋ฅผ ์ ์งํ๋ ๊ฒ์ด๋ค.
๋ฐ๋ผ์ http ํต์ ์ผ๋ก ์ค์๊ฐ ์ฑํ ์ ๊ตฌํํ๋ ค๋ฉด ์๋์ ๊ฐ์ ์ ์ฐจ๋ฅผ ๋ฐ์์ผ ํ๋ค.
- client1์ด ์๋ฒ์ ๋ฉ์ธ์ง๋ฅผ ์ ์กํ๊ณ ,
- server๋ ๊ทธ ๋ฉ์ธ์ง๋ฅผ client2์๊ฒ ์ ์กํ๊ณ ,
- client2๋ ์์ ์๊ฒ ์จ ๋ฉ์ธ์ง๋ฅผ ํ์ธํ๋ค.
๊ทธ๋ ๋ค๋ฉด client2๋ ์ธ์ ์ด๋ ์ฃผ๊ธฐ๋ก ์์ ์๊ฒ ๋ฉ์ธ์ง๊ฐ ์๋์ง ํ์ธํด์ผํ ๊น? ์ฃผ๊ธฐ์ ์ผ๋ก ์์ฒญํ๋ ๋ฐฉ๋ฒ๋ฐ์ ์๋๋ฐ ์ด๋ ๊ฒ ๋๋ฉด ์๋ฒ์ ๋ฌด๋ฆฌ๊ฐ ๋๋ฉฐ ์ค์๊ฐ ์ฑํ ๊ธฐ๋ฅ์ด๋ผ๊ณ ๋ ํ ์ ์๋ค.
๋ฐ๋ผ์ ํด๋ผ์ด์ธํธ๊ฐ ์๋ฒ์ ํ๋ฒ ์ฐ๊ฒฐ์ ๋งบ๊ณ ๋๋ฉด ์ปค๋ฅ์ ์ ๋์ง ์๊ณ ์ ์งํ๊ณ ์์ด์ผ ํ๋ค.
SSE(server push)
sse๋ server sent event์ ์ค์๋ง๋ก ์ด๋ฒคํธ๊ฐ [์๋ฒ -> ํด๋ผ์ด์ธํธ] ๋ฐฉํฅ์ผ๋ก๋ง ํ๋ฅด๋ ๋จ๋ฐฉํฅ ํต์ ์ฑ๋์ด๋ค. SSE๋ ํด๋ผ์ด์ธํธ๊ฐ polling๊ณผ ๊ฐ์ด ์ฃผ๊ธฐ์ ์ผ๋ก http ์์ฒญ์ ๋ณด๋ผ ํ์์์ด http ์ฐ๊ฒฐ์ ํตํด ์๋ฒ์์ ํด๋ผ์ด์ธํธ๋ก ๋ฐ์ดํฐ๋ฅผ ๋ณด๋ผ ์ ์๋ค.
์๋ฆผ ๊ฐ์ ๊ฒฝ์ฐ ์๋ฒ์ ์ฐ๊ฒฐ๋์ด ์๋ ํด๋ผ์ด์ธํธ์๊ฒ ์๋ฒ๊ฐ ๋ฐ์ดํฐ๋ฅผ ๋ณด๋ด๋ ๊ฒ์ด๋ค. ๋ฐ๋ผ์ ์๋ฒ->ํด๋ผ์ด์ธํธ ์ปค๋ฅ์ ๋ง ์ ์งํ๊ณ ์์ด๋ ์ถฉ๋ถํ๋ค.
ํ์ง๋ง ์ฑํ ์ ์ ์ A๊ฐ ์๋ฒ์ ๋ฐ์ดํฐ๋ฅผ ๋ณด๋ด๊ณ ์๋ฒ๋ ์ฑํ ๋ฐฉ์ ์๋ ๋ชจ๋ ์ ์ B, C, D.. ์๊ฒ ๋ฐ์ดํฐ๋ฅผ ์ ์กํด์ผ ํ๋ค. ๋ง์ฐฌ๊ฐ์ง๋ก B, C, D..๋ ๋ฉ์ธ์ง๋ฅผ ๋ณด๋ผ ์ ์๋ค. ๋ฐ๋ผ์ web socket ๊ธฐ์ ์ ์ฌ์ฉํด์ผ ํ๋ค.
Web Socket
Web socket์ ์ค์๊ฐ ์๋ฐฉํฅ ํต์ ์ ์ํ ์คํ์ผ๋ก ์๋ฒ์ ๋ธ๋ผ์ฐ์ ๊ฐ ์ง์์ ์ผ๋ก ์ฐ๊ฒฐ๋ TCP๋ผ์ธ์ ํตํด ์ค์๊ฐ์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ์ฃผ๊ณ ๋ฐ์ ์ ์๋ค.
์ง์์ ์ผ๋ก ์ ๋ฐ์ดํธ๋๋ ์ ๋ณด๋ฅผ ์์ ํด์ผ ํ๋ ์ฑํ ์ด๋ ์ฃผ์ ๋ณด๊ณ ์์์ WebSocket ํ๋กํ ์ฝ์ ์ฌ์ฉํ๋ ์ด์ ์ด ์๋ค. ์ด ํ๋กํ ์ฝ์ ์ ๋ณด๋ฅผ ๋์์ ์ก์์ ํ ์ ์์ผ๋ฏ๋ก ์ ์ด์ค ์๋ฐฉํฅ ํต์ ์ด ๊ฐ๋ฅํ๋ฏ๋ก ์ ๋ณด ๊ตํ์ด ๋ ๋นจ๋ผ์ง๋ค.
์น ์์ผ ๋์ ์๋ฆฌ๋ ๋ฌด์์ผ๊น?
ํด๋ผ์ด์ธํธ์ ์๋ฒ ๊ฐ์ ์ฐ๊ฒฐ์ ๋น์ฌ์ ์ค ํ๋์ ์ํด ์ข ๋ฃ๋๊ฑฐ๋ ์๊ฐ ์ด๊ณผ์ ์ํด ๋ซํ ๋๊น์ง ์ด๋ฆฐ ์ํ๋ก ์ ์ง๋๋ค. ํด๋ผ์ด์ธํธ์ ์๋ฒ ๊ฐ์ ์ฐ๊ฒฐ์ ์ค์ ํ๊ธฐ ์ํด ์ ์ฌ์ง ์ฒ๋ผ ํธ๋์ ฐ์ดํฌ๋ฅผ ์ํํ๋ค. ์ค์ ๋ ์ฐ๊ฒฐ์ ์ด๋ฆฐ ์ํ๋ก ์ ์ง๋๋ฉฐ ํด๋ผ์ด์ธํธ ๋๋ ์๋ฒ ์ธก์์ ์ฐ๊ฒฐ์ด ์ข ๋ฃ๋ ๋๊น์ง ๋์ผํ ์ฑ๋์ ์ฌ์ฉํ์ฌ ํต์ ์ด ์ํ๋๋ค. ๋ฐ๋ผ์ ๋ฉ์์ง๋ ์๋ฐฉํฅ์ผ๋ก ์ ์ก๋ ์ ์๋ค.
์น ์์ผ์ ํน์ง
- ์๋ฐฉํฅ ํต์
- ๋ฐ์ดํฐ ์ก์์ ์ ๋์์ ์ฒ๋ฆฌํ ์ ์๋ ํต์ ๋ฐฉ๋ฒ
- ํต์์ ์ธ http ํต์ ์ client๊ฐ ์์ฒญ์ ๋ณด๋ด๋ ๊ฒฝ์ฐ์๋ง server์์ ์๋ต
- ์ค์๊ฐ ๋คํธ์ํน
- ์น ํ๊ฒฝ์์ ์ฐ์๋ ๋ฐ์ดํฐ๋ฅผ ๋น ๋ฅด๊ฒ ๋ ธ์ถ
- ex) ์ฑํ , ์ฃผ์, ๋น๋์ค ๋ฐ์ดํฐ
- ์์ฒญ ํด๋
- Upgrade: websocket
- Connection: Upgrade
- ์ ๋ ํค๋๊ฐ ์์ผ๋ฉด ์น ์์ผ ์ฐ๊ฒฐ์ด ๋์ง ์๋๋ค.
- GET๋ฐฉ์์ผ๋ก ํธ๋์์ดํน ํ๋ฉฐ HTTP ๋ฒ์ ์ ๋ฐ๋์ 1.1 ์ด์
- ์ ๋ด์ฉ๋ค์ Nginx๋ฅผ ์ฌ์ฉ ํ ๋ ๋ฐ๋ก ์ค์ ํด์ค์ผ ํ๋ ๋ถ๋ถ๋ค์
์ฝ๋๋ก ์ดํด๋ณด์
WebSocketConfig.class
@Configuration
@RequiredArgsConstructor
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
private final ChatPreHandler chatPreHandler;
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/send");
registry.enableSimpleBroker("/room");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws-stomp").setAllowedOriginPatterns("*").withSockJS();
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(chatPreHandler);
}
}
- configureMessageBroker
- ๋ณด๋ผ ๋์ ๋ฐ์ ๋์ prefix ์ง์
- ํด๋ผ์ด์ธํธ๊ฐ ๋ฉ์์ง๋ฅผ ๋ณด๋ผ ๋ ๊ฒฝ๋ก ๋งจ์์ "/send"์ด ๋ถ์ด์์ผ๋ฉด Broker๋ก ๋ณด๋ด์ง.
- "/room" ๊ฒฝ๋ก๊ฐ ๋ถ์ ๊ฒฝ์ฐ messageBroker๊ฐ ์ก์์ ํด๋น ์ฑํ ๋ฐฉ์ ๊ตฌ๋ ํ๊ณ ์๋ ํด๋ผ์ด์ธํธ์๊ฒ ๋ฉ์์ง๋ฅผ ์ ๋ฌํด์ค
- registerStompEndpoints
- Client์์ websocket์ฐ๊ฒฐํ ๋ ์ฌ์ฉํ API ๊ฒฝ๋ก๋ฅผ ์ค์ ํด์ฃผ๋ ๋ฉ์๋.
- configureClientInboundChannel
- ์ธ์ฆ๋ ์ฌ์ฉ์๋ง ๋ฐ๊ธฐ ์ํด chatPreHandler ๋ฑ๋ก.
ChatPreHandler
@RequiredArgsConstructor
@Component
@Slf4j
public class ChatPreHandler implements ChannelInterceptor {
private final JwtTokenProvider jwtTokenProvider;
private static final String BEARER_PREFIX = "Bearer ";
// websocket์ ํตํด ๋ค์ด์จ ์์ฒญ์ด ์ฒ๋ฆฌ ๋๊ธฐ์ ์คํ๋๋ค.
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
if (StompCommand.CONNECT == accessor.getCommand()) { // websocket ์ฐ๊ฒฐ์์ฒญ
String jwtToken = accessor.getFirstNativeHeader("Authorization");
log.info("CONNECT {}", jwtToken);
// Header์ jwt token ๊ฒ์ฆ
String token = jwtToken.substring(7);
jwtTokenProvider.validateTokenInChat(token);
}
return message;
}
}
ChatController.class
@Controller
@RequiredArgsConstructor
@Slf4j
public class ChatController {
private final ChatService chatService;
private final JwtTokenProvider jwtTokenProvider;
@MessageMapping("/{roomId}")
@SendTo("/room/{roomId}") // ์ฌ๊ธธ ๊ตฌ๋
ํ๊ณ ์๋ ๊ณณ์ผ๋ก ๋ฉ์์ง ์ ์ก
public ChatDto messageHandler(@DestinationVariable Long roomId, ChatDto message, @Header("token") String token) {
String loginId = jwtTokenProvider.getUserNameFromJwt(token);//loginId ๊ฐ์ ธ์ด
return chatService.createChat(roomId, message.getMessage(), loginId);
}
}
Client
Socket ์ฐ๊ฒฐ, ๋ฉ์์ง send ๋ฑ ํด๋ผ์ด์ธํธ ์ฝ๋๋ ์๋์ ๊ฐ์ด ์์ฑํ์๋ค.
์ฑํ ๊ตฌํ ๊ด๋ จ ๋ธ๋ก๊ทธ ๊ธ์ ๋ณด๋ฉด ํด๋ผ์ด์ธํธ์ชฝ ์ฝ๋๊ฐ ์ ์์ด ํ๋ค์๊ธฐ์ ์ ์ฒด ์ฝ๋๋ฅผ ๊ณต์ ํ๋ค.
<script th:inline="javascript">
// WebSocket ์ฐ๊ฒฐ์ ๊ด๋ฆฌํ๋ ํด๋ผ์ด์ธํธ
let stompClient = null;
// ์๋ฒ์์ ๋ฐ์์จ ๋ฐฉ ID์ ์ฑํ
๋ชฉ๋ก
let roomId = [[${roomId}]];
let chatList = [[${chatList}]];
const token = localStorage.getItem('token');
const name = localStorage.getItem('name');
// ์น ์์ผ ์ฐ๊ฒฐ ์์ฑ
function connect() {
var socket = new SockJS('/ws-stomp');
stompClient = Stomp.over(socket);
let headers = {Authorization: token};
stompClient.connect(headers, function (frame) {
console.log('Connected: ' + frame);
loadChat(chatList); // ์ ์ฅ๋ ์ฑํ
๋ถ๋ฌ์ค๊ธฐ
// ๊ตฌ๋
์ค์
stompClient.subscribe('/room/' + roomId, function (chatMessage) {
console.log(JSON.parse(chatMessage.body));
showChat(JSON.parse(chatMessage.body));
});
});
}
// ์คํฌ๋กค ํญ์ ์๋๋ก ์ ์ง
function scrollChatToBottom() {
let chatContainer = document.getElementById('chatting');
chatContainer.scrollTop = chatContainer.scrollHeight;
}
function disconnect() {
if (stompClient !== null) {
stompClient.disconnect();
}
console.log("Disconnected");
}
// ์
๋ ฅ๋ ์ฑํ
์ ์ก
function sendChat() {
const message = $("#message").val();
// WebSocket ํต์ ์ ์ํ ํค๋ ์ค์
const headers = {
"token": token // ํ ํฐ ๊ฐ์ ์ฌ๊ธฐ์ ๋ฃ์ด์ฃผ์ธ์
};
stompClient.send("/send/" + roomId, headers, JSON.stringify({message: message}))
$("#message").val("");
}
// ์ ์ฅ๋ ์ฑํ
๋ถ๋ฌ์ ํ๋ฉด์ ํ์
function loadChat(chatList) {
if (chatList != null) {
for (let chat of chatList) {
let chatHtml = '';
if (chat.sender !== name) {
chatHtml = `<div class="chat ch1">
<div class="icon"><i class="fa-solid fa-user"></i></div>
<div class="sender">${chat.sender}</div>
<div class="textbox">${chat.message}</div>
</div>`;
} else if (chat.sender === name) {
chatHtml = `<div class="chat ch2">
<div class="icon"><i class="fa-solid fa-user"></i></div>
<div class="sender">${chat.sender}</div>
<div class="textbox">${chat.message}</div>
</div>`;
}
$("#chatting").append(chatHtml);
}
}
}
// ์ค์๊ฐ์ผ๋ก ๋ฐ์ ์ฑํ
์ ํ๋ฉด์ ํ์
function showChat(chatMessage) {
let chatHtml = '';
if (chatMessage.sender !== name) {
chatHtml = `<div class="chat ch1">
<div class="icon"><i class="fa-solid fa-user"></i></div>
<div class="sender">${chatMessage.sender}</div>
<div class="textbox">${chatMessage.message}</div>
</div>`;
} else if (chatMessage.sender === name) {
chatHtml = `<div class="chat ch2">
<div class="icon"><i class="fa-solid fa-user"></i></div>
<div class="sender">${chatMessage.sender}</div>
<div class="textbox">${chatMessage.message}</div>
</div>`;
}
$("#chatting").append(chatHtml);
scrollChatToBottom(); // ์คํฌ๋กค ํญ์ ์๋๋ก ์ ์ง
}
// ํผ ์ ์ถ ์ ๊ธฐ๋ณธ ๋์ ๋ฐฉ์ง
$(function () {
$("#chat-form").on('submit', function (e) {
e.preventDefault();
});
// ๋ฒํผ ํด๋ฆญ ์ด๋ฒคํธ์ ํจ์ ํ ๋น
$("#send").click(function () {
sendChat();
});
});
</script>