stomp+websocket 방식
우리 프로젝트에서는 웹 소켓 위에 STOMP프로토콜을 사용하기로 했다.
역할
웹 소켓: 서버와 클라이언트 사이를 연결해준다
STOMP: 규칙을 제시한다. 데이터에 딱지를 붙여 준다.
예: SEND-채팅방으로 갈거야. SUBSCRIBE- 나 이제부터 귓속말 들을거야(구독하기).
api명세서로 따라가보는 흐름
[1단계] 입장 수속 (REST API)
경로: POST /api/users/join (일반 HTTP)
카운터 접수
[2단계] 도로 개통 (웹소켓 연결)
경로: ws://localhost:8080/ws
서버와 실시간으로 대화할 전용 터널 생성. -> 브라우저를 끄기 전까지 연결 유지
[3단계] 주파수 맞추기 (STOMP - SUBSCRIBE)
클라이언트가 STOMP규칙에 따라 구독 신청.

- /topic/public 구독
- /user/queue/whisper 구독
- /topic/users 구독
💡 핵심: STOMP가 없다면, 서버는 메시지가 왔을 때 이걸 누구한테 뿌려야 할지 일일이 코드로 짜야 함. 하지만 STOMP를 쓰면 "이 주소(/topic/public) 구독한 애들한테 다 뿌려!" 라고 하면 끝.
[4단계] 말하기 (STOMP - SEND)
경로: /app/chat.send (메세지 전송)
클라이언트의 메세지가 STOMP 봉투에 담겨서 감.
COMMAND: SEND
DESTINATION: /app/chat.send
{ "senderId": 1, "content": "안녕하세요!" }
-
- DESTINATION을 보고 스프링 서버(MessageBroker)가 "아, 이건 채팅 보내는 로직(chat.send)으로 보내야겠구나"라고 판단
- 서버가 뿌릴 때 (응답):
- 서버는 이 메시지를 처리한 뒤, 아까 /topic/public을 구독하고 있던 모든 사람(예림, 아연 등)에게 쏴줌.
[5단계] 퇴장 (연결 끊기)
- 클라이언트가 브라우저를 닫음. (웹소켓 터널 붕괴 💥)
- 스프링은 "연결된 선이 끊어졌네?"라고 감지. (SessionDisconnectEvent)
- 서버가 자동으로 "00님이 나갔습니다"라는 안내 방송을 만들어서, 아직 남아있는 사람들의 /topic/users 채널로 쏴줌
기존 웹 REST API랑 다른점은 Controller가 URL주소를 받는게 아니라 STOMP(메시지 주소)를 받는다.
private final ObjectMapper objectMapper;
//접속한 유저 관리 (메모리저장소)
private static final Map<String , WebSocketSession> userSessions = new ConcurrentHashMap<>();
이게 필요 없다.
구독개념으로 사용하기 떄문에, 스프링이 내부적으로 누가 들어와있는지(구독자)를 알아서 관리해준다.
비교
// 우리가 짠 코드: 수동으로 관리함
private Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
public void userEnter(String name, WebSocketSession session) {
sessions.put(name, session); // 명부에 적기
// ... for문 돌려서 전체 방송 ...
}
// STOMP 방식: 그냥 리턴하면 끝남
@MessageMapping("/enter") // 클라이언트가 /app/enter로 보내면
@SendTo("/topic/chat") // 구독한 모두에게 리턴값(/topic/chat)을 쏨
public ChatMessage enter(ChatMessage msg) {
msg.setMessage(msg.getSender() + "님이 입장하셨습니다.");
return msg; // 리턴하면 스프링이 알아서 전체 방송 때림
}
3. 그럼 STOMP에서 입/퇴장 감지는 어떻게 해?
"Map이 없는데, 진짜 연결이 끊겼는지 어떻게 알고 '퇴장하셨습니다'를 띄우지?" 👉 스프링이 제공하는 **이벤트 리스너(Event Listener)**를 사용합니다.
@Component
@RequiredArgsConstructor
public class WebSocketEventListener {
private final SimpMessageSendingOperations messagingTemplate;
// 연결이 끊기면(Disconnect) 스프링이 이 함수를 자동으로 실행해줌
@EventListener
public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) {
// 여기서 "OO님이 퇴장하셨습니다" 메시지를 만들어서
// messagingTemplate.convertAndSend("/topic/chat", msg); 로 쏘면 끝.
}
}
입장은 클라이언트가 버튼을 누르면 입장되니까 cotroller가 관리하고, 퇴장은 사용자가 창을 꺼서 끊기때문에, 감시자 (Listener)가 필요하다.
1. 전체 흐름도 (Flow)
- [입장] 클라이언트(버튼 클릭) ➡ /app/chat.join 전송 ➡ Controller 실행
- Controller:
- DB에서 유저 찾음.
- UserService: 명단에 이름 올림 (addActiveUser).
- Session: 나갈 때를 대비해 이름표 붙임 (put("username")).
- ChatService: "이제 방송해!"라고 명령 (userJoin).
- ChatService:
- 개인: 지난 대화 내용을 그 사람에게만 쏴줌 (loadHistory).
- 전체: "OOO님 입장!" 알림 메시지 방송 (/topic/public).
- 전체: [철수, 영희, OOO] 갱신된 명단 방송 (/topic/users).
- [퇴장] 클라이언트(창 닫음) ➡ 연결 끊김 감지 ➡ Listener 실행
- 3. WebSocketEventListener (Listener):
- 이벤트에서 StompHeaderAccessor를 통해 세션 정보를 엽니다.
- 아까 입장할 때(joinChat) 넣어뒀던 username("철수")을 꺼냅니다.
- UserService에게 "철수 명단에서 지워!" 라고 시킵니다.
- ChatService에게 "철수 나갔다고 방송해!" 라고 시킵니다.
stomp 이해
왜 userid가 아니라 string을 서비스에 보낼까?

STOMP는 Username을 보고 배달하기 때문이다. (headAccesor가 각 유저의 소켓에 userName을 붙여 관리한다.)
'프로젝트' 카테고리의 다른 글
| 자바 프로젝트-4 클라이언트 시작! stomp서버 연결하기 (0) | 2025.12.05 |
|---|---|
| 자프실 프로젝트-3 (0) | 2025.11.29 |
| 자프실 프로젝트-1 (0) | 2025.11.29 |
| 위드플래닛 프로젝트: 기획 재구성 (코더가 기획의 벽에 부딪혔을 때) (0) | 2025.09.28 |
| Unity 코루틴 (프로젝트) (0) | 2025.09.15 |