Backend
home
๐Ÿ“ง

Web Socket(STOMP, Redis)

์ƒ์„ฑ ์ผ์‹œ
2025/06/19 14:19
ํƒœ๊ทธ
SpringBoot
๊ฒŒ์‹œ์ผ
2025/06/19
์ตœ์ข… ํŽธ์ง‘ ์ผ์‹œ
2025/06/19 15:03

WebSocketConfig

package org.example.backendproject.stompwebsocket.config; import org.example.backendproject.stompwebsocket.handler.CustomHandshakeHandler; import org.springframework.context.annotation.Configuration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistry registry) { /** * ๊ตฌ๋…์ค‘ Prefix * /topic ์ผ๋ฐ˜ ์ฑ„ํŒ…์„ ๋ฐ›์„ ์ ‘๋‘์–ด * /queue ๊ท“์†๋ง์„ ๋ฐ›์„ ์ ‘๋‘์–ด * ์„œ๋ฒ„๊ฐ€ ๋ณด๋‚ด๋Š” ๋ฉ”์‹œ์ง€๋ฅผ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๊ตฌ๋…ํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ๋กœ */ registry.enableSimpleBroker("/topic", "/queue"); // ๊ตฌ๋…์šฉ ๊ฒฝ๋กœ /** ์ „์†ก์šฉ Prefix **/ // ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์„œ๋ฒ„์— ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‚ผ ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ๋กœ ์ ‘๋‘์–ด registry.setApplicationDestinationPrefixes("/app"); // ํด๋ผ์ด์–ธํŠธ -> ์„œ๋ฒ„ // ์„œ๋ฒ„๊ฐ€ ํŠน์ • ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‚ผ ๋•Œ, ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๊ตฌ๋…ํ•  ๊ฒฝ๋กœ ์ ‘๋‘์–ด registry.setUserDestinationPrefix("/user"); // ์„œ๋ฒ„ -> ํŠน์ • ์‚ฌ์šฉ์ž } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/ws-chat") .setHandshakeHandler(new CustomHandshakeHandler()) .setAllowedOriginPatterns("*"); } }
Java
๋ณต์‚ฌ

1. @EnableWebSocketMessageBroker

โ€ข
์ด ์–ด๋…ธํ…Œ์ด์…˜์€ Spring์ด WebSocket ๋ฉ”์‹œ์ง€ ๋ธŒ๋กœ์ปค ๊ธฐ๋Šฅ์„ ํ™œ์„ฑํ™”ํ•˜๋„๋ก ์ง€์‹œํ•œ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด STOMP ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ ํ™˜๊ฒฝ์ด ์ž๋™์œผ๋กœ ๊ตฌ์„ฑ๋œ๋‹ค.

2. configureMessageBroker(MessageBrokerRegistry registry)

์ด ๋ฉ”์„œ๋“œ๋Š” ๋ฉ”์‹œ์ง€ ๋ธŒ๋กœ์ปค์˜ ๋™์ž‘ ๋ฐฉ์‹์„ ์„ค์ •ํ•œ๋‹ค.
โ€ข
registry.enableSimpleBroker("/topic", "/queue");
โ—ฆ
@EnableWebSocketMessageBroker์™€ ํ•จ๊ป˜ ์‚ฌ์šฉ๋  ๋•Œ, ์ด ์„ค์ •์€ ์ธ๋ฉ”๋ชจ๋ฆฌ(in-memory) ๋ฉ”์‹œ์ง€ ๋ธŒ๋กœ์ปค๋ฅผ ํ™œ์„ฑํ™”ํ•œ๋‹ค.
โ—ฆ
/topic: ์ผ๋ฐ˜์ ์œผ๋กœ ๊ณต๊ฐœ ์ฑ„ํŒ…์ด๋‚˜ ์—ฌ๋Ÿฌ ์‚ฌ์šฉ์ž๊ฐ€ ๊ตฌ๋…ํ•˜๋Š” ๋ฉ”์‹œ์ง€์— ์‚ฌ์šฉ๋œ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, "/topic/publicChat"๊ณผ ๊ฐ™์€ ๊ฒฝ๋กœ๋กœ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‚ด๋ฉด ํ•ด๋‹น ๊ฒฝ๋กœ๋ฅผ ๊ตฌ๋…ํ•˜๋Š” ๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋ฉ”์‹œ์ง€๋ฅผ ์ˆ˜์‹ ํ•œ๋‹ค.
โ—ฆ
/queue: ์ฃผ๋กœ ํŠน์ • ์‚ฌ์šฉ์ž์—๊ฒŒ 1:1 ๋ฉ”์‹œ์ง€(๊ท“์†๋ง)๋ฅผ ๋ณด๋‚ผ ๋•Œ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. ํด๋ผ์ด์–ธํŠธ๋Š” /user/queue/privateMessages์™€ ๊ฐ™์€ ๊ฒฝ๋กœ๋ฅผ ๊ตฌ๋…ํ•˜์—ฌ ์ž์‹ ์—๊ฒŒ ์˜จ ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐ›๊ฒŒ ๋œ๋‹ค.
โ€ข
registry.setApplicationDestinationPrefixes("/app");
โ—ฆ
๋ถ„์„: ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์„œ๋ฒ„๋กœ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‚ผ ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ๋ชฉ์ ์ง€(destination)์˜ ์ ‘๋‘์–ด๋ฅผ ์„ค์ •ํ•œ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ํด๋ผ์ด์–ธํŠธ๊ฐ€ /app/chat.sendMessage๋กœ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‚ด๋ฉด, Spring์€ @MessageMapping("/chat.sendMessage")์™€ ๊ฐ™์€ ์–ด๋…ธํ…Œ์ด์…˜์ด ๋ถ™์€ ์ปจํŠธ๋กค๋Ÿฌ ๋ฉ”์„œ๋“œ๋ฅผ ์ฐพ์•„ ํ•ด๋‹น ๋ฉ”์‹œ์ง€๋ฅผ ์ฒ˜๋ฆฌํ•œ๋‹ค.
โ€ข
registry.setUserDestinationPrefix("/user");
โ—ฆ
Spring STOMP๋Š” ์‚ฌ์šฉ์ž๋ณ„(user-specific) ๋ฉ”์‹œ์ง•์„ ์ง€์›ํ•œ๋‹ค. ์ด ์ ‘๋‘์–ด๋Š” ์„œ๋ฒ„๊ฐ€ ํŠน์ • ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‚ผ ๋•Œ ์‚ฌ์šฉ๋˜๋Š” ๊ฒฝ๋กœ์˜ ์ ‘๋‘์–ด๋ฅผ ์ •์˜ํ•œ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์„œ๋ฒ„๊ฐ€ ํŠน์ • userId๋ฅผ ๊ฐ€์ง„ ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‚ด๊ณ ์ž ํ•  ๋•Œ, ๋‚ด๋ถ€์ ์œผ๋กœ /user/{userId}/queue/messages์™€ ๊ฐ™์€ ๊ฒฝ๋กœ๋ฅผ ์‚ฌ์šฉํ•˜๊ฒŒ ๋œ๋‹ค. ํด๋ผ์ด์–ธํŠธ ์ž…์žฅ์—์„œ๋Š” stompClient.subscribe('/user/queue/messages')์™€ ๊ฐ™์ด ๊ตฌ๋…ํ•˜๋ฉด ์ž์‹ ์—๊ฒŒ ์ „์†ก๋œ ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐ›์„ ์ˆ˜ ์žˆ๋‹ค.

3. registerStompEndpoints(StompEndpointRegistry registry)

์ด ๋ฉ”์„œ๋“œ๋Š” WebSocket ์—ฐ๊ฒฐ์„ ์œ„ํ•œ STOMP ์—”๋“œํฌ์ธํŠธ๋ฅผ ๋“ฑ๋กํ•œ๋‹ค.
โ€ข
registry.addEndpoint("/ws-chat"):
โ—ฆ
ํด๋ผ์ด์–ธํŠธ๊ฐ€ WebSocket ์—ฐ๊ฒฐ์„ ์‹œ๋„ํ•  URL ๊ฒฝ๋กœ๋ฅผ ์ •์˜ํ•œ๋‹ค. ํด๋ผ์ด์–ธํŠธ๋Š” new WebSocket("ws://localhost:8080/ws-chat")์™€ ๊ฐ™์ด ์ด ๊ฒฝ๋กœ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์—ฐ๊ฒฐ์„ ์‹œ์ž‘ํ•œ๋‹ค. ์ด ๊ฒฝ๋กœ๋Š” HTTP ํ•ธ๋“œ์…ฐ์ดํฌ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋ฉฐ, ์—ฐ๊ฒฐ์ด ์„ฑ๊ณตํ•˜๋ฉด WebSocket ํ”„๋กœํ† ์ฝœ๋กœ ์—…๊ทธ๋ ˆ์ด๋“œ ๋œ๋‹ค.
โ€ข
.setHandshakeHandler(new CustomHandshakeHandler()):
โ—ฆ
WebSocket ํ•ธ๋“œ์…ฐ์ดํฌ ๊ณผ์ •์—์„œ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•ํ•˜๊ฑฐ๋‚˜ ํŠน์ • ๋กœ์ง์„ ์ถ”๊ฐ€ํ•  ๋•Œ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. CustomHandshakeHandler ํด๋ž˜์Šค๋ฅผ ํ†ตํ•ด ์‚ฌ์šฉ์ž ์„ธ์…˜์— ํŠน์ • ์ •๋ณด๋ฅผ ์ €์žฅํ•˜๊ฑฐ๋‚˜, ์ธ์ฆ/์ธ๊ฐ€ ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, HTTP ์„ธ์…˜์— ์žˆ๋Š” ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ STOMP ์„ธ์…˜์— ์—ฐ๊ฒฐํ•˜๋Š” ๋ฐ ์œ ์šฉํ•˜๋‹ค.
โ€ข
.setAllowedOriginPatterns("*"):
โ—ฆ
๋ถ„์„: ์ด ์„ค์ •์€ CORS(Cross-Origin Resource Sharing) ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ๋œ๋‹ค. "*"๋Š” ๋ชจ๋“  ์˜ค๋ฆฌ์ง„(origin)์—์„œ์˜ ์—ฐ๊ฒฐ์„ ํ—ˆ์šฉํ•œ๋‹ค๋Š” ์˜๋ฏธ์ด๋‹ค. ๊ฐœ๋ฐœ ๋‹จ๊ณ„์—์„œ๋Š” ํŽธ๋ฆฌํ•˜์ง€๋งŒ, ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” ๋ณด์•ˆ์ƒ ํŠน์ • ๋„๋ฉ”์ธ์œผ๋กœ ์ œํ•œํ•˜๋Š” ๊ฒƒ์ด ๊ฐ•๋ ฅํžˆ ๊ถŒ์žฅ๋œ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, .setAllowedOriginPatterns("https://yourfrontend.com", "http://localhost:3000")์™€ ๊ฐ™์ด ๋ช…์‹œ์ ์œผ๋กœ ํ—ˆ์šฉํ•  ์˜ค๋ฆฌ์ง„์„ ์ง€์ •ํ•ด์•ผ ํ•œ๋‹ค.

CustomHandshakeHandler

CustomHandshakeHandler๋Š” WebSocket ํ•ธ๋“œ์…ฐ์ดํฌ ๊ณผ์ •์—์„œ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์ถ”์ถœํ•˜๊ณ  Principal ๊ฐ์ฒด๋ฅผ ์„ค์ •ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋œ๋‹ค.
package org.example.backendproject.stompwebsocket.handler; import java.security.Principal; import java.util.Map; import org.springframework.http.server.ServerHttpRequest; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.server.support.DefaultHandshakeHandler; public class CustomHandshakeHandler extends DefaultHandshakeHandler { @Override protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, Map<String, Object> attributes) { String nickname = getNickName(request.getURI().getQuery()); return new StompPrincipal(nickname); } private String getNickName(String query) { if (query == null || !query.contains("nickname=")) { return "๋‹‰๋„ค์ž„์—†์Œ"; } else { return query.split("nickname=")[1]; } } }
Java
๋ณต์‚ฌ
โ€ข
DefaultHandshakeHandler๋ฅผ ์ƒ์†๋ฐ›์•„ WebSocket ํ•ธ๋“œ์…ฐ์ดํฌ์˜ ๊ธฐ๋ณธ ๋™์ž‘์„ ํ™•์žฅํ•œ๋‹ค.
โ€ข
determineUser ๋ฉ”์„œ๋“œ๋ฅผ ์˜ค๋ฒ„๋ผ์ด๋“œํ•˜์—ฌ WebSocket ์—ฐ๊ฒฐ ์‹œ ์‚ฌ์šฉ์ž(Principal)๋ฅผ ๊ฒฐ์ •ํ•œ๋‹ค.
โ€ข
์ด ๋ฉ”์„œ๋“œ์—์„œ๋Š” ServerHttpRequest ๊ฐ์ฒด์—์„œ ์š”์ฒญ URI์˜ ์ฟผ๋ฆฌ ์ŠคํŠธ๋ง์„ ๊ฐ€์ ธ์™€ getNickName ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค.
โ€ข
getNickName ๋ฉ”์„œ๋“œ๋Š” ์ฟผ๋ฆฌ ์ŠคํŠธ๋ง์—์„œ "nickname="์œผ๋กœ ์‹œ์ž‘ํ•˜๋Š” ๋ถ€๋ถ„์„ ์ฐพ์•„ ๊ทธ ๋’ค์˜ ๊ฐ’์„ ์ถ”์ถœํ•œ๋‹ค. ๋งŒ์•ฝ ์ฟผ๋ฆฌ ์ŠคํŠธ๋ง์ด ์—†๊ฑฐ๋‚˜ "nickname="์„ ํฌํ•จํ•˜์ง€ ์•Š์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ "๋‹‰๋„ค์ž„์—†์Œ"์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
โ€ข
์ถ”์ถœ๋œ ๋‹‰๋„ค์ž„ ๋ฌธ์ž์—ด์„ ์‚ฌ์šฉํ•˜์—ฌ StompPrincipal ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ์ด๋ฅผ Principal๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค. ์ด๋ ‡๊ฒŒ ์„ค์ •๋œ Principal์€ ํ•ด๋‹น WebSocket ์„ธ์…˜์˜ ์‚ฌ์šฉ์ž ์‹๋ณ„์ž๋กœ ์‚ฌ์šฉ๋œ๋‹ค.
โ€ข
๋‹‰๋„ค์ž„์„ URI ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ์—์„œ ์ง์ ‘ ์ถ”์ถœํ•˜๋Š” ๋ฐฉ์‹์€ ๊ฐ„๋‹จํ•˜์ง€๋งŒ, ๋ณด์•ˆ์ ์ธ ์ธก๋ฉด์—์„œ ์ทจ์•ฝํ•  ์ˆ˜ ์žˆ๋‹ค. ์‹ค์ œ ์„œ๋น„์Šค์—์„œ๋Š” ํ† ํฐ(JWT ๋“ฑ)์ด๋‚˜ ์„ธ์…˜ ๊ธฐ๋ฐ˜ ์ธ์ฆ์„ ํ†ตํ•ด ์‚ฌ์šฉ์ž๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ์‹๋ณ„ํ•˜๊ณ , ํ•ด๋‹น ์ธ์ฆ ์ •๋ณด์—์„œ ์‚ฌ์šฉ์ž ๊ณ ์œ  ID๋‚˜ ๋‹‰๋„ค์ž„์„ ์ถ”์ถœํ•˜๋Š” ๊ฒƒ์ด ํ›จ์”ฌ ์•ˆ์ „ํ•˜๋‹ค.
โ€ข
์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ ํŒŒ์‹ฑ ๋กœ์ง(getNickName)์ด ๋งค์šฐ ๊ฐ„๋‹จํ•˜์—ฌ, "nickname=" ๋ฌธ์ž์—ด์ด ์ฟผ๋ฆฌ ์ค‘๊ฐ„์— ์žˆ๊ฑฐ๋‚˜ ๋‹ค๋ฅธ ํŒŒ๋ผ๋ฏธํ„ฐ์™€ ์กฐํ•ฉ๋  ๊ฒฝ์šฐ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๋™์ž‘์„ ํ•  ์ˆ˜ ์žˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ?param=value&nickname=myname๊ณผ ๊ฐ™์€ ๊ฒฝ์šฐ์—๋Š” ์ •์ƒ ์ž‘๋™ํ•˜์ง€๋งŒ, ?nickname=myname&param=value์ฒ˜๋Ÿผ ๋‹‰๋„ค์ž„์ด ๋จผ์ € ๋‚˜์˜ค์ง€ ์•Š์œผ๋ฉด ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค. UriComponentsBuilder๋‚˜ UriUtils ๊ฐ™์€ Spring ์œ ํ‹ธ๋ฆฌํ‹ฐ๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ํŒŒ์‹ฑํ•˜๋Š” ๊ฒƒ์ด ๋” ๊ฒฌ๊ณ ํ•œ ๊ตฌํ˜„ ๋ฐฉ๋ฒ•์ด๋‹ค.
โ€ข
๋‹‰๋„ค์ž„์ด ์—†์„ ๊ฒฝ์šฐ "๋‹‰๋„ค์ž„์—†์Œ"์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ถ€๋ถ„์€ ๋””๋ฒ„๊น…์ด๋‚˜ ๊ฐ„๋‹จํ•œ ํ…Œ์ŠคํŠธ์—๋Š” ์œ ์šฉํ•˜์ง€๋งŒ, ์‹ค์ œ ์„œ๋น„์Šค์—์„œ๋Š” ๋ช…ํ™•ํ•œ ์—๋Ÿฌ ์ฒ˜๋ฆฌ๋‚˜ ๊ธฐ๋ณธ ๋‹‰๋„ค์ž„ ๋ถ€์—ฌ ์ •์ฑ…์„ ๊ณ ๋ คํ•ด์•ผ ํ•œ๋‹ค.

StompPrincipal

StompPrincipal์€ Principal ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•˜์—ฌ STOMP ์„ธ์…˜์—์„œ ์‚ฌ์šฉ์ž๋ฅผ ์‹๋ณ„ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋˜๋Š” ์ปค์Šคํ…€ Principal ๊ตฌํ˜„์ฒด์ด๋‹ค.
package org.example.backendproject.stompwebsocket.entity; import java.security.Principal; public class StompPrincipal implements Principal { private final String name; public StompPrincipal(String name) { this.name = name; } @Override public String getName() { return name; } }
Java
๋ณต์‚ฌ
โ€ข
java.security.Principal ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•œ๋‹ค. ์ด ์ธํ„ฐํŽ˜์ด์Šค๋Š” ์ธ์ฆ๋œ ๊ฐœ์ฒด(์‚ฌ์šฉ์ž)์˜ ์ด๋ฆ„์„ ๋‚˜ํƒ€๋‚ด๋Š” ๊ธฐ๋ณธ์ ์ธ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•œ๋‹ค.
โ€ข
name ํ•„๋“œ๋ฅผ ํ†ตํ•ด ์‚ฌ์šฉ์ž์˜ ๊ณ ์œ  ์‹๋ณ„์ž(์ด๋ฆ„, ์—ฌ๊ธฐ์„œ๋Š” ๋‹‰๋„ค์ž„)๋ฅผ ์ €์žฅํ•œ๋‹ค.
โ€ข
getName() ๋ฉ”์„œ๋“œ๋ฅผ ์˜ค๋ฒ„๋ผ์ด๋“œํ•˜์—ฌ name ํ•„๋“œ์˜ ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. Spring STOMP ๋ฉ”์‹œ์ง€ ๋ธŒ๋กœ์ปค๋Š” ์ด getName() ๋ฉ”์„œ๋“œ๊ฐ€ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฐ’์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์‚ฌ์šฉ์ž๋ณ„ ๋ฉ”์‹œ์ง• (์˜ˆ:/user/{userName}/queue/messages)์„ ์ฒ˜๋ฆฌํ•œ๋‹ค.

STOMP ํ”„๋กœํ† ์ฝœ์„ ํ™œ์šฉํ•œ ํ†ต์‹  ํ๋ฆ„

WebSocketConfig์—์„œ /ws-chat ์—”๋“œํฌ์ธํŠธ์— CustomHandshakeHandler๋ฅผ ๋“ฑ๋กํ–ˆ์œผ๋ฏ€๋กœ, ํด๋ผ์ด์–ธํŠธ๊ฐ€ /ws-chat์œผ๋กœ WebSocket ์—ฐ๊ฒฐ์„ ์‹œ๋„ํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํ๋ฆ„์œผ๋กœ ์ง„ํ–‰๋œ๋‹ค.
1.
ํด๋ผ์ด์–ธํŠธ์˜ WebSocket ์—ฐ๊ฒฐ ์š”์ฒญ: ์›น ๋ธŒ๋ผ์šฐ์ €๋‚˜ STOMP ํด๋ผ์ด์–ธํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ws://localhost:8080/ws-chat?nickname=์‚ฌ์šฉ์ž๋‹‰๋„ค์ž„๊ณผ ๊ฐ™์€ URL๋กœ WebSocket ํ•ธ๋“œ์…ฐ์ดํฌ ์š”์ฒญ์„ ๋ณด๋‚ธ๋‹ค.
2.
Spring์˜ ํ•ธ๋“œ์…ฐ์ดํฌ ์ฒ˜๋ฆฌ: Spring WebSocket ์ปจํ…Œ์ด๋„ˆ๋Š” ์ด ์š”์ฒญ์„ ๊ฐ์ง€ํ•˜๊ณ  WebSocketConfig์— ๋“ฑ๋ก๋œ CustomHandshakeHandler์˜ determineUser ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค.
3.
determineUser ๋ฉ”์„œ๋“œ ์‹คํ–‰:
โ€ข
CustomHandshakeHandler์˜ determineUser ๋ฉ”์„œ๋“œ๊ฐ€ ์‹คํ–‰๋œ๋‹ค.
โ€ข
request.getURI().getQuery()๋ฅผ ํ†ตํ•ด nickname=์‚ฌ์šฉ์ž๋‹‰๋„ค์ž„๊ณผ ๊ฐ™์€ ์ฟผ๋ฆฌ ์ŠคํŠธ๋ง์„ ๊ฐ€์ ธ์˜จ๋‹ค.
โ€ข
getNickName ๋ฉ”์„œ๋“œ๊ฐ€ ํ˜ธ์ถœ๋˜์–ด ์ด ์ฟผ๋ฆฌ ์ŠคํŠธ๋ง์—์„œ "์‚ฌ์šฉ์ž๋‹‰๋„ค์ž„" ๋ถ€๋ถ„์„ ์ถ”์ถœํ•œ๋‹ค.
โ€ข
์ถ”์ถœ๋œ ๋‹‰๋„ค์ž„์„ ์ธ์ž๋กœ new StompPrincipal(nickname)์„ ํ˜ธ์ถœํ•˜์—ฌ StompPrincipal ๊ฐ์ฒด๊ฐ€ ์ƒ์„ฑ๋œ๋‹ค.
โ€ข
์ด StompPrincipal ๊ฐ์ฒด๊ฐ€ determineUser ๋ฉ”์„œ๋“œ์˜ ๋ฐ˜ํ™˜ ๊ฐ’์œผ๋กœ Spring์— ์ „๋‹ฌ๋œ๋‹ค.
4.
Principal ๊ฐ์ฒด ์„ค์ •: Spring์€ determineUser ๋ฉ”์„œ๋“œ๊ฐ€ ๋ฐ˜ํ™˜ํ•œ StompPrincipal ๊ฐ์ฒด๋ฅผ ํ˜„์žฌ WebSocket ์„ธ์…˜์˜ Principal๋กœ ์„ค์ •ํ•œ๋‹ค. ์ด Principal์€ STOMP ๋ฉ”์‹œ์ง€์˜ ํ—ค๋”(SimpMessageHeaderAccessor.USER_HEADER ๋“ฑ)์— ์ž๋™์œผ๋กœ ์ถ”๊ฐ€๋˜์–ด, ๋‚˜์ค‘์— @MessageMapping์ด ์ ์šฉ๋œ ์ปจํŠธ๋กค๋Ÿฌ ๋ฉ”์„œ๋“œ๋‚˜ ๋ฉ”์‹œ์ง€ ๊ฐ€๋กœ์ฑ„๊ธฐ(Interceptor) ๋“ฑ์—์„œ ํ˜„์žฌ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‚ธ ์‚ฌ์šฉ์ž๋ฅผ ์‹๋ณ„ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋  ์ˆ˜ ์žˆ๋‹ค. ๋˜ํ•œ, /user ์ ‘๋‘์‚ฌ๋ฅผ ์‚ฌ์šฉํ•œ ์‚ฌ์šฉ์ž๋ณ„ ๋ฉ”์‹œ์ง• ์‹œ ์ด Principal์˜ getName() ๊ฐ’์ด ์‚ฌ์šฉ์ž ์‹๋ณ„์ž๋กœ ํ™œ์šฉ๋œ๋‹ค.
5.
WebSocket ์—ฐ๊ฒฐ ์ˆ˜๋ฆฝ: ํ•ธ๋“œ์…ฐ์ดํฌ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์™„๋ฃŒ๋˜๋ฉด, ํด๋ผ์ด์–ธํŠธ์™€ ์„œ๋ฒ„ ๊ฐ„์— WebSocket ์—ฐ๊ฒฐ์ด ์ˆ˜๋ฆฝ๋˜๊ณ  STOMP ๋ฉ”์‹œ์ง€๋ฅผ ์ฃผ๊ณ ๋ฐ›์„ ์ค€๋น„๊ฐ€ ๋œ๋‹ค.
6.
STOMP ๋ฉ”์‹œ์ง•: ์ดํ›„ ํด๋ผ์ด์–ธํŠธ๊ฐ€ /app ์ ‘๋‘์–ด๋ฅผ ๊ฐ€์ง„ ๋ชฉ์ ์ง€๋กœ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‚ด๊ฑฐ๋‚˜, /topic, /queue, /user ์ ‘๋‘์–ด๋ฅผ ๊ฐ€์ง„ ๋ชฉ์ ์ง€๋ฅผ ๊ตฌ๋…ํ•  ๋•Œ, ์ด ์„ธ์…˜์— ์—ฐ๊ฒฐ๋œ StompPrincipal ๊ฐ์ฒด์˜ getName() ๋ฉ”์„œ๋“œ๊ฐ€ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋‹‰๋„ค์ž„ ๊ฐ’์ด ์‚ฌ์šฉ์ž ์‹๋ณ„์ž๋กœ ์‚ฌ์šฉ๋œ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์„œ๋ฒ„๊ฐ€ /user/{nickname}/queue/messages๋กœ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‚ด๋ฉด, ์ด nickname์— ํ•ด๋‹นํ•˜๋Š” StompPrincipal์„ ๊ฐ€์ง„ ํด๋ผ์ด์–ธํŠธ๋งŒ ๋ฉ”์‹œ์ง€๋ฅผ ์ˆ˜์‹ ํ•œ๋‹ค.

RedisConfig

RedisConfig ํด๋ž˜์Šค๋Š” Redis pub/sub ๋ฉ”์‹œ์ง• ์‹œ์Šคํ…œ์˜ ํ•ต์‹ฌ ์„ค์ •์œผ๋กœ, ๋ฉ”์‹œ์ง€ ๋ฆฌ์Šค๋„ˆ ์ปจํ…Œ์ด๋„ˆ๋ฅผ ์ •์˜ํ•œ๋‹ค.
package org.example.backendproject.stompwebsocket.redis; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.listener.PatternTopic; import org.springframework.data.redis.listener.RedisMessageListenerContainer; import org.springframework.data.redis.listener.adapter.MessageListenerAdapter; @RequiredArgsConstructor @Configuration public class RedisConfig { private final RedisSubscriber redisSubscriber; @Bean public RedisMessageListenerContainer container(RedisConnectionFactory redisConnectionFactory) { RedisMessageListenerContainer container = new RedisMessageListenerContainer(); container.setConnectionFactory(redisConnectionFactory); container.addMessageListener(new MessageListenerAdapter(redisSubscriber), new PatternTopic("room.*")); container.addMessageListener(new MessageListenerAdapter(redisSubscriber), new PatternTopic("private.*")); // ๊ท“์†๋ง return container; } }
Java
๋ณต์‚ฌ
โ€ข
@Configuration ์–ด๋…ธํ…Œ์ด์…˜์€ ์ด ํด๋ž˜์Šค๊ฐ€ Spring์˜ ์„ค์ • ํด๋ž˜์Šค์ž„์„ ๋‚˜ํƒ€๋‚ธ๋‹ค.
โ€ข
@RequiredArgsConstructor๋Š” final ํ•„๋“œ์ธ redisSubscriber์— ๋Œ€ํ•œ ์ƒ์„ฑ์ž๋ฅผ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•œ๋‹ค.
โ€ข
container ๋นˆ์€ RedisMessageListenerContainer๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ์„ค์ •ํ•œ๋‹ค. ์ด ์ปจํ…Œ์ด๋„ˆ๋Š” Redis๋กœ๋ถ€ํ„ฐ ๋ฉ”์‹œ์ง€๋ฅผ ์ˆ˜์‹ ํ•˜๋Š” ์—ญํ• ์„ ํ•œ๋‹ค.
โ€ข
container.setConnectionFactory(redisConnectionFactory);๋ฅผ ํ†ตํ•ด Redis ์—ฐ๊ฒฐ ํŒฉํ† ๋ฆฌ๋ฅผ ์„ค์ •ํ•˜์—ฌ Redis ์„œ๋ฒ„์™€์˜ ํ†ต์‹ ์„ ๊ด€๋ฆฌํ•œ๋‹ค.
โ€ข
container.addMessageListener๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฉ”์‹œ์ง€ ๋ฆฌ์Šค๋„ˆ(RedisSubscriber๋ฅผ ๋ž˜ํ•‘ํ•œ MessageListenerAdapter)๋ฅผ ํŠน์ • ์ฑ„๋„ ํŒจํ„ด์— ๋“ฑ๋กํ•œ๋‹ค.
โ—ฆ
new PatternTopic("room.*"): "room."์œผ๋กœ ์‹œ์ž‘ํ•˜๋Š” ๋ชจ๋“  ์ฑ„๋„(์˜ˆ: room.1, room.2)์—์„œ ๋ฐœํ–‰๋˜๋Š” ๋ฉ”์‹œ์ง€๋ฅผ redisSubscriber๊ฐ€ ์ˆ˜์‹ ํ•˜๋„๋ก ์„ค์ •ํ•œ๋‹ค. ์ด๋Š” ์ผ๋ฐ˜ ์ฑ„ํŒ…๋ฐฉ ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ์— ์‚ฌ์šฉ๋œ๋‹ค.
โ—ฆ
new PatternTopic("private.*"): "private."์œผ๋กœ ์‹œ์ž‘ํ•˜๋Š” ๋ชจ๋“  ์ฑ„๋„(์˜ˆ: private.user1, private.user2)์—์„œ ๋ฐœํ–‰๋˜๋Š” ๋ฉ”์‹œ์ง€๋ฅผ redisSubscriber๊ฐ€ ์ˆ˜์‹ ํ•˜๋„๋ก ์„ค์ •ํ•œ๋‹ค. ์ด๋Š” ๊ท“์†๋ง ์ฒ˜๋ฆฌ์— ์‚ฌ์šฉ๋œ๋‹ค.

RedisPublisher

RedisPublisher ํด๋ž˜์Šค๋Š” Redis ์ฑ„๋„๋กœ ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐœํ–‰ํ•˜๋Š” ์—ญํ• ์„ ๋‹ด๋‹นํ•œ๋‹ค.
package org.example.backendproject.stompwebsocket.redis; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; @RequiredArgsConstructor @Component public class RedisPublisher { private final StringRedisTemplate stringRedisTemplate; /** ๋ฉ”์„ธ์ง€๋ฅผ ๋ฐœํ–‰ํ•˜๋Š” ํด๋ž˜์Šค **/ public void publish(String channel, String msg) { stringRedisTemplate.convertAndSend(channel, msg); } }
Java
๋ณต์‚ฌ
โ€ข
@Component ์–ด๋…ธํ…Œ์ด์…˜์€ ์ด ํด๋ž˜์Šค๊ฐ€ Spring์˜ ์ปดํฌ๋„ŒํŠธ ์Šค์บ” ๋Œ€์ƒ์ด ๋˜์–ด ๋นˆ์œผ๋กœ ๋“ฑ๋ก๋จ์„ ๋‚˜ํƒ€๋‚ธ๋‹ค.
โ€ข
@RequiredArgsConstructor๋Š” StringRedisTemplate์— ๋Œ€ํ•œ ์ƒ์„ฑ์ž๋ฅผ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•œ๋‹ค.
โ€ข
StringRedisTemplate์€ Redis์™€์˜ ์ƒํ˜ธ์ž‘์šฉ์„ ์œ„ํ•œ ํŽธ๋ฆฌํ•œ ํ…œํ”Œ๋ฆฟ์œผ๋กœ, ํŠนํžˆ ๋ฌธ์ž์—ด ๊ธฐ๋ฐ˜์˜ ํ‚ค-๊ฐ’ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค๋ฃฐ ๋•Œ ์œ ์šฉํ•˜๋‹ค.
โ€ข
publish ๋ฉ”์„œ๋“œ๋Š” ์ฃผ์–ด์ง„ channel๋กœ msg๋ฅผ ๋ฐœํ–‰ํ•œ๋‹ค. stringRedisTemplate.convertAndSend๋Š” Redis์˜ PUBLISH ๋ช…๋ น์„ ์‹คํ–‰ํ•œ๋‹ค.

RedisSubscriber

RedisSubscriber ํด๋ž˜์Šค๋Š” Redis ์ฑ„๋„๋กœ๋ถ€ํ„ฐ ๋ฉ”์‹œ์ง€๋ฅผ ์ˆ˜์‹ ํ•˜๊ณ , ์ด๋ฅผ WebSocket ํด๋ผ์ด์–ธํŠธ๋กœ ์ „์†กํ•˜๋Š” ์—ญํ• ์„ ํ•œ๋‹ค.
package org.example.backendproject.stompwebsocket.redis; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import org.example.backendproject.stompwebsocket.dto.ChatMessage; import org.springframework.data.redis.connection.Message; import org.springframework.data.redis.connection.MessageListener; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor public class RedisSubscriber implements MessageListener { private final SimpMessagingTemplate simpMessagingTemplate; private ObjectMapper objectMapper = new ObjectMapper(); @Override public void onMessage(Message message, byte[] pattern){ try { String msgBody = new String(message.getBody()); ChatMessage chatMessage = objectMapper.readValue(msgBody, ChatMessage.class); if (chatMessage.getTo() != null && !chatMessage.getTo().isEmpty()) { simpMessagingTemplate.convertAndSendToUser(chatMessage.getTo(), "/queue/private", chatMessage); } else { simpMessagingTemplate.convertAndSend("/topic/room." + chatMessage.getRoomId(), chatMessage); } } catch (Exception e) { // ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๋กœ์ง (๋กœ๊น… ๋“ฑ) } } }
Java
๋ณต์‚ฌ
โ€ข
@Service ์–ด๋…ธํ…Œ์ด์…˜์€ ์ด ํด๋ž˜์Šค๊ฐ€ Spring์˜ ์„œ๋น„์Šค ๋ ˆ์ด์–ด ์ปดํฌ๋„ŒํŠธ์ž„์„ ๋‚˜ํƒ€๋‚ธ๋‹ค.
โ€ข
MessageListener ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•˜์—ฌ Redis ๋ฉ”์‹œ์ง€๋ฅผ ์ˆ˜์‹ ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•œ๋‹ค.
โ€ข
@RequiredArgsConstructor๋Š” SimpMessagingTemplate์— ๋Œ€ํ•œ ์ƒ์„ฑ์ž๋ฅผ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•œ๋‹ค. SimpMessagingTemplate์€ Spring WebSocket STOMP ๋ฉ”์‹œ์ง€ ๋ธŒ๋กœ์ปค๋ฅผ ํ†ตํ•ด ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‚ผ ๋•Œ ์‚ฌ์šฉ๋˜๋Š” ํ•ต์‹ฌ ๋„๊ตฌ์ด๋‹ค.
โ€ข
ObjectMapper๋Š” JSON ๋ฌธ์ž์—ด์„ Java ๊ฐ์ฒด๋กœ ์—ญ์ง๋ ฌํ™”ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋œ๋‹ค.
โ€ข
onMessage ๋ฉ”์„œ๋“œ๋Š” Redis ์ฑ„๋„์—์„œ ๋ฉ”์‹œ์ง€๊ฐ€ ์ˆ˜์‹ ๋  ๋•Œ๋งˆ๋‹ค ํ˜ธ์ถœ๋œ๋‹ค.
โ—ฆ
์ˆ˜์‹ ๋œ Message ๊ฐ์ฒด์˜ ๋ณธ๋ฌธ(body)์„ String์œผ๋กœ ๋ณ€ํ™˜ํ•œ๋‹ค.
โ—ฆ
ObjectMapper๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฉ”์‹œ์ง€ ๋ณธ๋ฌธ(JSON ๋ฌธ์ž์—ด)์„ ChatMessage ๊ฐ์ฒด๋กœ ์—ญ์ง๋ ฌํ™”ํ•œ๋‹ค.
โ—ฆ
chatMessage.getTo() ํ•„๋“œ์˜ ์กด์žฌ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•˜์—ฌ ๊ท“์†๋ง์ธ์ง€ ์ผ๋ฐ˜ ๋ฉ”์‹œ์ง€์ธ์ง€๋ฅผ ํŒ๋‹จํ•œ๋‹ค.
โ–ช
๊ท“์†๋ง์ธ ๊ฒฝ์šฐ: simpMessagingTemplate.convertAndSendToUser(chatMessage.getTo(), "/queue/private", chatMessage);๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํŠน์ • ์‚ฌ์šฉ์ž(chatMessage.getTo())์—๊ฒŒ /queue/private ๋ชฉ์ ์ง€๋กœ ๋ฉ”์‹œ์ง€๋ฅผ ์ „์†กํ•œ๋‹ค. convertAndSendToUser๋Š” /user/{userName}/queue/private ํ˜•ํƒœ๋กœ ๋ฉ”์‹œ์ง€๋ฅผ ๋ผ์šฐํŒ…ํ•œ๋‹ค.
โ–ช
์ผ๋ฐ˜ ๋ฉ”์‹œ์ง€์ธ ๊ฒฝ์šฐ: simpMessagingTemplate.convertAndSend("/topic/room." + chatMessage.getRoomId(), chatMessage);๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ•ด๋‹น ์ฑ„ํŒ…๋ฐฉ์˜ topic ๋ชฉ์ ์ง€(/topic/room.{roomId})๋ฅผ ๊ตฌ๋…ํ•˜๊ณ  ์žˆ๋Š” ๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๋ฉ”์‹œ์ง€๋ฅผ ์ „์†กํ•œ๋‹ค.

ChatController

ChatController ํด๋ž˜์Šค๋Š” ํด๋ผ์ด์–ธํŠธ๋กœ๋ถ€ํ„ฐ STOMP ๋ฉ”์‹œ์ง€๋ฅผ ์ˆ˜์‹ ํ•˜๊ณ , ์ด๋ฅผ Redis ์ฑ„๋„๋กœ ๋ฐœํ–‰ํ•˜๋Š” ์—ญํ• ์„ ํ•œ๋‹ค.
package org.example.backendproject.stompwebsocket.controller; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import org.example.backendproject.stompwebsocket.dto.ChatMessage; import org.example.backendproject.stompwebsocket.redis.RedisPublisher; import org.springframework.beans.factory.annotation.Value; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.SendTo; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.stereotype.Controller; @Controller @RequiredArgsConstructor public class ChatController { private final RedisPublisher redisPublisher; private ObjectMapper objectMapper = new ObjectMapper(); @Value("${PROJECT_NAME:web Server}") private String instanceName; // ๋™์ ์œผ๋กœ ๋ฐฉ ์ƒ์„ฑ ๊ฐ€๋Šฅ @MessageMapping("/chat.sendMessage") public void sendMessage(ChatMessage message) throws JsonProcessingException { message.setMessage(instanceName + " " + message.getMessage()); String channel = null; String msg = null; if (message.getTo() != null && !message.getTo().isEmpty()) { // ๊ท“์†๋ง // ๋‚ด ์•„์ด๋””๋กœ ๊ท“์†๋ง ๊ฒฝ๋กœ๋ฅผ ํ™œ์„ฑํ™” ํ•จ channel = "private." + message.getRoomId(); // Note: room.Id in private chat might be confusing if it's not a real room msg = objectMapper.writeValueAsString(message); } else { // ์ผ๋ฐ˜ ๋ฉ”์‹œ์ง€ // message์—์„œ roomId๋ฅผ ์ถ”์ถœํ•ด์„œ ํ•ด๋‹น roomId๋ฅผ ๊ตฌ๋…ํ•˜๊ณ  ์žˆ๋Š” ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๋ฉ”์„ธ์ง€๋ฅผ ์ „๋‹ฌ channel = "room." + message.getRoomId(); msg = objectMapper.writeValueAsString(message); } redisPublisher.publish(channel, msg); } }
Java
๋ณต์‚ฌ
โ€ข
@Controller ์–ด๋…ธํ…Œ์ด์…˜์€ ์ด ํด๋ž˜์Šค๊ฐ€ Spring MVC ์ปจํŠธ๋กค๋Ÿฌ์ž„์„ ๋‚˜ํƒ€๋‚ด์ง€๋งŒ, WebSocket ๋ฉ”์‹œ์ง•์—์„œ๋Š” @MessageMapping์„ ์ฒ˜๋ฆฌํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ ์—ญํ• ์„ ํ•œ๋‹ค.
โ€ข
@RequiredArgsConstructor๋Š” RedisPublisher์— ๋Œ€ํ•œ ์ƒ์„ฑ์ž๋ฅผ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•œ๋‹ค.
โ€ข
@Value("${PROJECT_NAME:web Server}")๋Š” ํ™˜๊ฒฝ ๋ณ€์ˆ˜ PROJECT_NAME์˜ ๊ฐ’์„ instanceName ํ•„๋“œ์— ์ฃผ์ž…ํ•˜๋ฉฐ, ๊ธฐ๋ณธ๊ฐ’์€ "web Server"์ด๋‹ค. ์ด ๊ฐ’์€ ๋ฉ”์‹œ์ง€์— ์„œ๋ฒ„ ์ธ์Šคํ„ด์Šค ์ด๋ฆ„์„ ์ถ”๊ฐ€ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋œ๋‹ค.
โ€ข
ObjectMapper๋Š” Java ๊ฐ์ฒด๋ฅผ JSON ๋ฌธ์ž์—ด๋กœ ์ง๋ ฌํ™”ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋œ๋‹ค.
โ€ข
@MessageMapping("/chat.sendMessage")๋Š” ํด๋ผ์ด์–ธํŠธ๊ฐ€ /app/chat.sendMessage ๋ชฉ์ ์ง€๋กœ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‚ผ ๋•Œ ์ด sendMessage ๋ฉ”์„œ๋“œ๊ฐ€ ํ˜ธ์ถœ๋˜๋„๋ก ๋งคํ•‘ํ•œ๋‹ค.
โ€ข
sendMessage ๋ฉ”์„œ๋“œ ๋‚ด๋ถ€:
โ—ฆ
์ˆ˜์‹ ๋œ ChatMessage์˜ ๋ฉ”์‹œ์ง€ ๋ณธ๋ฌธ์— instanceName์„ ์ถ”๊ฐ€ํ•œ๋‹ค.
โ—ฆ
chatMessage.getTo() ํ•„๋“œ์˜ ์กด์žฌ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•˜์—ฌ ๊ท“์†๋ง์ธ์ง€ ์ผ๋ฐ˜ ๋ฉ”์‹œ์ง€์ธ์ง€๋ฅผ ํŒ๋‹จํ•œ๋‹ค.
โ–ช
์ฃผ์˜์‚ฌํ•ญ
โ€ข
๊ท“์†๋ง์ธ ๊ฒฝ์šฐ: Redis ์ฑ„๋„์„ private.{roomId}๋กœ ์„ค์ •ํ•˜๊ณ , ChatMessage ๊ฐ์ฒด๋ฅผ JSON ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•œ๋‹ค. ์—ฌ๊ธฐ์„œ roomId๊ฐ€ ๊ท“์†๋ง ์ฑ„๋„์— ์‚ฌ์šฉ๋˜๋Š” ๊ฒƒ์€ ์กฐ๊ธˆ ์˜์•„ํ•  ์ˆ˜ ์žˆ๋‹ค. ๋ณดํ†ต ๊ท“์†๋ง์€ private.{์ˆ˜์‹ ์žID} ํ˜•ํƒœ๊ฐ€ ๋” ์ผ๋ฐ˜์ ์ด๋‹ค. ๋งŒ์•ฝ roomId๊ฐ€ ์‹ค์ œ ์ˆ˜์‹ ์ž์˜ ๊ณ ์œ  ID๋ฅผ ๋‚˜ํƒ€๋‚ธ๋‹ค๋ฉด ๋ฌธ์ œ๊ฐ€ ์—†์ง€๋งŒ, ๊ทธ๋ ‡์ง€ ์•Š๋‹ค๋ฉด ํ˜ผ๋ž€์„ ์ค„ ์ˆ˜ ์žˆ๋‹ค.
โ€ข
์ผ๋ฐ˜ ๋ฉ”์‹œ์ง€์ธ ๊ฒฝ์šฐ: Redis ์ฑ„๋„์„ room.{roomId}๋กœ ์„ค์ •ํ•˜๊ณ , ChatMessage ๊ฐ์ฒด๋ฅผ JSON ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•œ๋‹ค.
โ—ฆ
์ค€๋น„๋œ channel๊ณผ msg๋ฅผ redisPublisher.publish ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด Redis๋กœ ๋ฐœํ–‰ํ•œ๋‹ค.

Redis Pub/Sub์„ ํ†ตํ•œ ์ „์ฒด ํ†ต์‹  ํ๋ฆ„

1.
ํด๋ผ์ด์–ธํŠธ์˜ ๋ฉ”์‹œ์ง€ ์ „์†ก: WebSocket ํด๋ผ์ด์–ธํŠธ๊ฐ€ STOMP๋ฅผ ํ†ตํ•ด /app/chat.sendMessage ๋ชฉ์ ์ง€๋กœ ChatMessage๋ฅผ ๋ณด๋‚ธ๋‹ค. (์˜ˆ: { "roomId": "general", "sender": "user1", "message": "Hello" } ๋˜๋Š” { "to": "user2", "roomId": "privateChatWithUser2", "sender": "user1", "message": "Hi private" })
2.
ChatController์˜ ๋ฉ”์‹œ์ง€ ์ˆ˜์‹ : Spring์€ @MessageMapping("/chat.sendMessage")์— ๋”ฐ๋ผ ChatController์˜ sendMessage ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค.
3.
Redis ์ฑ„๋„ ์„ ํƒ ๋ฐ ๋ฐœํ–‰:
โ€ข
ChatController๋Š” ์ˆ˜์‹ ๋œ ChatMessage์˜ to ํ•„๋“œ๋ฅผ ํ™•์ธํ•˜์—ฌ ์ผ๋ฐ˜ ๋ฉ”์‹œ์ง€์ธ์ง€ ๊ท“์†๋ง์ธ์ง€๋ฅผ ํŒ๋‹จํ•œ๋‹ค.
โ€ข
์ผ๋ฐ˜ ๋ฉ”์‹œ์ง€: Redis ์ฑ„๋„์„ room.{roomId}๋กœ ์„ค์ •ํ•˜๊ณ , ChatMessage๋ฅผ JSON ๋ฌธ์ž์—ด๋กœ ์ง๋ ฌํ™”ํ•˜์—ฌ RedisPublisher.publish๋ฅผ ํ†ตํ•ด Redis๋กœ ๋ฐœํ–‰ํ•œ๋‹ค.
โ€ข
๊ท“์†๋ง: Redis ์ฑ„๋„์„ private.{roomId} (์—ฌ๊ธฐ์„œ roomId๋Š” ๊ท“์†๋ง ๋Œ€์ƒ์˜ ๊ณ ์œ  ID๊ฐ€ ๋˜์–ด์•ผ ํ•จ)๋กœ ์„ค์ •ํ•˜๊ณ , ChatMessage๋ฅผ JSON ๋ฌธ์ž์—ด๋กœ ์ง๋ ฌํ™”ํ•˜์—ฌ RedisPublisher.publish๋ฅผ ํ†ตํ•ด Redis๋กœ ๋ฐœํ–‰ํ•œ๋‹ค.
4.
Redis์˜ ๋ฉ”์‹œ์ง€ ์ „๋‹ฌ: Redis๋Š” ๋ฐœํ–‰๋œ ๋ฉ”์‹œ์ง€๋ฅผ ํ•ด๋‹น ์ฑ„๋„์„ ๊ตฌ๋…ํ•˜๊ณ  ์žˆ๋Š” ๋ชจ๋“  RedisMessageListenerContainer ์ธ์Šคํ„ด์Šค(์ฆ‰, ์—ฌ๋Ÿฌ ์„œ๋ฒ„ ์ธ์Šคํ„ด์Šค)๋กœ ์ „๋‹ฌํ•œ๋‹ค.
5.
RedisSubscriber์˜ ๋ฉ”์‹œ์ง€ ์ˆ˜์‹ : ๊ฐ ์„œ๋ฒ„ ์ธ์Šคํ„ด์Šค์— ์žˆ๋Š” RedisConfig์— ์˜ํ•ด ๋“ฑ๋ก๋œ RedisSubscriber์˜ onMessage ๋ฉ”์„œ๋“œ๊ฐ€ Redis๋กœ๋ถ€ํ„ฐ ๋ฉ”์‹œ์ง€๋ฅผ ์ˆ˜์‹ ํ•œ๋‹ค.
6.
๋ฉ”์‹œ์ง€ ์—ญ์ง๋ ฌํ™” ๋ฐ WebSocket ์ „์†ก:
โ€ข
RedisSubscriber๋Š” ์ˆ˜์‹ ๋œ ๋ฉ”์‹œ์ง€ ๋ณธ๋ฌธ์„ ChatMessage ๊ฐ์ฒด๋กœ ์—ญ์ง๋ ฌํ™”ํ•œ๋‹ค.
โ€ข
ChatMessage์˜ to ํ•„๋“œ๋ฅผ ๋‹ค์‹œ ํ™•์ธํ•œ๋‹ค.
โ—ฆ
๊ท“์†๋ง: simpMessagingTemplate.convertAndSendToUser(chatMessage.getTo(), "/queue/private", chatMessage);๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์›๋ž˜ ๊ท“์†๋ง์˜ ๋Œ€์ƒ(chatMessage.getTo())์—๊ฒŒ๋งŒ WebSocket ๋ฉ”์‹œ์ง€๋ฅผ ์ „์†กํ•œ๋‹ค. chatMessage.getTo()์— ํ•ด๋‹นํ•˜๋Š” ์‚ฌ์šฉ์ž๋งŒ "/user/{to}/queue/private"๋ฅผ ๊ตฌ๋…ํ•˜์—ฌ ์ด ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐ›๊ฒŒ ๋œ๋‹ค.
โ—ฆ
์ผ๋ฐ˜ ๋ฉ”์‹œ์ง€: simpMessagingTemplate.convertAndSend("/topic/room." + chatMessage.getRoomId(), chatMessage);๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ•ด๋‹น ์ฑ„ํŒ…๋ฐฉ์„ ๊ตฌ๋…ํ•˜๊ณ  ์žˆ๋Š” ๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ WebSocket ๋ฉ”์‹œ์ง€๋ฅผ ์ „์†กํ•œ๋‹ค. ์ด๋“ค์€ "/topic/room.{roomId}"๋ฅผ ๊ตฌ๋…ํ•˜๊ณ  ์žˆ๋‹ค.
7.
ํด๋ผ์ด์–ธํŠธ์˜ ๋ฉ”์‹œ์ง€ ์ˆ˜์‹ : WebSocket ํด๋ผ์ด์–ธํŠธ๋Š” ๊ตฌ๋…ํ•˜๊ณ  ์žˆ๋Š” STOMP ๋ชฉ์ ์ง€(์˜ˆ: /topic/room.{roomId} ๋˜๋Š” /user/{myNickname}/queue/private)๋ฅผ ํ†ตํ•ด ๋ฉ”์‹œ์ง€๋ฅผ ์ˆ˜์‹ ํ•œ๋‹ค.
์ด ์„ค๊ณ„์˜ ์žฅ์ :
โ€ข
์ˆ˜ํ‰ ํ™•์žฅ์„ฑ: Redis Pub/Sub์„ ์‚ฌ์šฉํ•˜๋ฉด ์—ฌ๋Ÿฌ Spring WebSocket ์„œ๋ฒ„ ์ธ์Šคํ„ด์Šค๋ฅผ ์‹คํ–‰ํ•˜์—ฌ ๋ถ€ํ•˜๋ฅผ ๋ถ„์‚ฐํ•˜๊ณ  ์„œ๋น„์Šค์˜ ํ™•์žฅ์„ฑ์„ ํ™•๋ณดํ•  ์ˆ˜ ์žˆ๋‹ค. ์–ด๋–ค ์„œ๋ฒ„ ์ธ์Šคํ„ด์Šค๋กœ ๋ฉ”์‹œ์ง€๊ฐ€ ๋“ค์–ด์˜ค๋“  Redis๋ฅผ ํ†ตํ•ด ๋ชจ๋“  ๊ด€๋ จ ์„œ๋ฒ„ ์ธ์Šคํ„ด์Šค์— ์ „๋‹ฌ๋˜๊ณ , ๊ฐ ์ธ์Šคํ„ด์Šค๋Š” ์—ฐ๊ฒฐ๋œ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๋ฉ”์‹œ์ง€๋ฅผ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ๋‹ค.
โ€ข
Decoupling (๊ฒฐํ•ฉ๋„ ๊ฐ์†Œ): ๋ฉ”์‹œ์ง€ ๋ฐœํ–‰์ž์™€ ๊ตฌ๋…์ž๊ฐ€ ์ง์ ‘ ํ†ต์‹ ํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ Redis๋ฅผ ํ†ตํ•ด ๊ฐ„์ ‘์ ์œผ๋กœ ํ†ต์‹ ํ•˜๋ฏ€๋กœ, ์‹œ์Šคํ…œ ์ปดํฌ๋„ŒํŠธ ๊ฐ„์˜ ๊ฒฐํ•ฉ๋„๊ฐ€ ๋‚ฎ์•„์ง„๋‹ค.