Overview
이번에는 Springboot 부분입니다.
설명이 거의 없습니다. 거의 아실거라 생각해서... 데이터 베이스를 사용하지 않았습니다. ☆
- Config
- Handler
- Model
- Service
- Controller
- 설명
1. Config
방법:
@EnableWebSocketMessageBroker
implements WebSocketMessageBrokerConfigurer
엔드포인트 : ws
클라이언트에서 접속시 http://localhost:8080/ws 로 접속 ? 됩니다.
.withSockJS()
클라이언트에서 SockJS사용시 사용합니다.
registry.enableSimpleBroker("/sub");
registry.setApplicationDestinationPrefixes("/pub");
사람들은 topic 이랑 app 을 많이들 쓰신던데 ㅎㅎ.. 쉽게 설명해서
pub 란 메시지를 뿌리는 쪽
sub 란 클라이언트가 어떤 주제를 구독하는 쪽 이라고 생각하시면 편할듯 합니다
한마디로, 신문을 구독 (sub) , 신문사가 신문 발행 -> 구독자들 에게 뿌림 (pub)
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(stompHandler);
}
요건뭐 핸들링 하는 부분 ㅎㅎㅎㅎ CONNECT , DISCONNECT 핸들링부분
import com.example.backend.handler.StompHandler;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.ChannelRegistration;
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
@RequiredArgsConstructor
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
private final StompHandler stompHandler;
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*")
.withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/sub");
registry.setApplicationDestinationPrefixes("/pub");
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(stompHandler);
}
}
2. Handler
이 부분은 딱 봐도 아실테니 넘어가보죠
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptorAdapter;
import org.springframework.stereotype.Component;
@Component
public class StompHandler extends ChannelInterceptorAdapter {
@Override
public void postSend(Message message, MessageChannel channel, boolean sent) {
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
String sessionId = accessor.getSessionId();
switch (accessor.getCommand()) {
case CONNECT:
System.out.println("세션 들어옴 => " + sessionId);
break;
case DISCONNECT:
System.out.println("세션 끊음 => " + sessionId);
break;
default:
break;
}
}
}
3. Model
모델을 좀 이상하게 짰는데 조금 양해 부탁 ㅋㅋㅋㅋㅋㅋㅋ
3.1. ChatMessage - 메시지
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ChatMessage {
private String sender;
private String content;
private MessageType type;
private String roomId;
private LocalDateTime timestamp;
public ChatMessage(String sender, String content, MessageType type, String roomId) {
this.sender = sender;
this.content = content;
this.type = type;
this.roomId = roomId;
this.timestamp = LocalDateTime.now();
}
}
3.2. MessageType - 메시지 타입
public enum MessageType {
CHAT,
JOIN,
LEAVE
}
3.3. ChatRoom - 채팅 방
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
public class ChatRoom {
private String roomId;
private List<String> participants = new ArrayList<>();
private List<ChatMessage> messages = new ArrayList<>();
public ChatRoom(String roomId) {
this.roomId = roomId;
}
public void join(String participant) {
participants.add(participant);
broadcastMessage(new ChatMessage("System", participant + "님이 참여하였습니다.", MessageType.JOIN, roomId));
}
public void leave(String participant) {
participants.remove(participant);
broadcastMessage(new ChatMessage("System", participant + "님이 나가셨습니다.", MessageType.LEAVE, roomId));
}
public void addMessage(ChatMessage message) {
messages.add(message);
}
public void broadcastMessage(ChatMessage message) {
messages.add(message);
}
}
4. Service
이번엔 서비스 부분입니다.
import com.example.backend.model.ChatMessage;
import com.example.backend.model.ChatRoom;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
@Service
public class ChatService {
private final Map<String, ChatRoom> chatRooms = new HashMap<>();
private final SimpMessageSendingOperations messagingTemplate;
@Autowired
public ChatService(SimpMessageSendingOperations messagingTemplate) {
this.messagingTemplate = messagingTemplate;
}
public ChatRoom getChatRoom(String roomId) {
// computeIfAbsent : roomId 가 없을때 new ChatRoom 인스턴스 반환
return chatRooms.computeIfAbsent(roomId, ChatRoom::new);
// 위와 동일
// return chatRooms.computeIfAbsent(roomId, (Id) -> new ChatRoom(Id));
}
public void handleUserJoin(String roomId, String participant) {
ChatRoom chatRoom = getChatRoom(roomId);
chatRoom.join(participant);
// 채팅방에 참여한 사용자에게 현재 채팅방 정보 전송
messagingTemplate.convertAndSend("/sub/chat/" + roomId, chatRoom);
}
public void handleUserLeave(String roomId, String participant) {
ChatRoom chatRoom = getChatRoom(roomId);
chatRoom.leave(participant);
// 채팅방에 나간 사용자에게 현재 채팅방 정보 전송
messagingTemplate.convertAndSend("/sub/chat/" + roomId, chatRoom);
}
public void handleChatMessage(String roomId, ChatMessage message) {
ChatRoom chatRoom = getChatRoom(roomId);
chatRoom.addMessage(message);
System.out.println("chatMessage 객체 : " + chatRoom);
// 채팅방에 메시지 전송
messagingTemplate.convertAndSend("/sub/chat/" + roomId, chatRoom);
}
public void sendMessage(String roomId, ChatMessage message) {
handleChatMessage(roomId, message);
}
}
5. Controller
Controller 는 두개로 나뉘어져 있습니다.
SocketController
import lombok.RequiredArgsConstructor;
import org.springframework.context.event.EventListener;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.socket.messaging.SessionConnectEvent;
import org.springframework.web.socket.messaging.SessionDisconnectEvent;
@RestController
@RequiredArgsConstructor
public class SocketController {
private final SimpMessageSendingOperations simpleMessageSendingOperations;
@EventListener
public void handleWebSocketConnectListener(SessionConnectEvent event) {
System.out.println("Received a new web socket connection");
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
String sessionId = headerAccessor.getSessionId();
System.out.println("Session Connected: " + sessionId);
}
@EventListener
public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) {
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
String sessionId = headerAccessor.getSessionId();
System.out.println("Session Disconnected: " + sessionId);
// 세션 해제 시 필요한 로직을 추가할 수 있습니다.
// 예를 들어, 채팅방에서 사용자가 나갔을 때 해당 사용자를 처리하는 등의 작업을 수행할 수 있습니다.
// 이 부분에 필요한 로직을 추가하세요.
}
}
ChatController
밑은 보시면 아시다시피 @SubcribeMapping 이 먹질 않더군요 왠지는 모르겠어요 매핑이 안되요 ㅋㅋㅋㅋㅋㅋㅋ
다만, 성능상에 문제는 없으니 넘어갔습니다.
import com.example.backend.model.ChatMessage;
import com.example.backend.model.ChatRoom;
import com.example.backend.service.ChatService;
import lombok.RequiredArgsConstructor;
import org.springframework.messaging.handler.annotation.DestinationVariable;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.simp.annotation.SubscribeMapping;
import org.springframework.stereotype.Controller;
@Controller
@RequiredArgsConstructor
public class ChatController {
private final ChatService chatService;
@MessageMapping("/chat/sendMessage")
public void sendMessage(@Payload ChatMessage message) {
chatService.handleChatMessage(message.getRoomId(), message);
}
// 현재 이부분이 안됨 딱히 상관은 없는데 짜증나네 ㅋㅋㅋㅋㅋㅋㅋ
@SubscribeMapping("/sub/chat/{roomId}")
public ChatRoom joinRoom(@DestinationVariable String roomId) {
System.out.println("구독 룸넘버 | ");
chatService.handleUserJoin(roomId , "USER");
return chatService.getChatRoom(roomId);
}
}
6. 위의 설명
보면 아실거라 생각하지만, 여기서 좀 더 적어보도록 하겠습니다.
1.
front : >>> CONNECT
http://localhost:8080/ws 로 세션이 들어오게 되면 CONNECT 가 되어 세션 아이디를 반환하게 됩니다. (원래는 그런데 잘 안되서 뺐었던거 같습니다.)
2.
CONNECT가 잘 되었다면
front : >>> subscrbie
/sub/chat/room 으로 구독을 하게 되는데
이때 /sub/주제/방번호 로 되어있는 느낌입니다.
subscribe 가 되었다면 화면에 그 채팅방의 채팅 내역이 화면에 뿌려지게 할 생각이었으나,,,, @SubscribeMapping 이 저는 안되더군요 왠지는 모르겠어요 ㅋㅋㅋㅋㅋㅋ
3.
front : >>> SEND
/pub/chat/sendMessage
프론트 쪽에서 백엔드 부분과 협상했던 매핑(/pub/chat/sendMessage)를 통해 메시지를 수신하게 됩니다.
프론트 쪽에서는 JSON.stringify 로 String 형식으로 보내어 질 것이고, 백엔드 부분에서 @Payload 를 통하여 메시지를 수신하게 됩니다. 이때 방번호와 메시지 등을 주게 됩니다.
4.
front : <<< MESSAGE
/sub/chat/room
그럼 방 room 을 구독하던 구독자들에게 메시지를 전달하게 됩니다.
네 여기까지네요
아무래도 @SubscribeMapping 은 동작을 안하는게 마음에 걸리고 , service 도 대부분 안 쓴 부분들이 하나씩 있어섴ㅋㅋㅋㅋㅋㅋㅋ 수정하시면서 사용해주세요.
결과 화면입니다.
'SpringBoot > code' 카테고리의 다른 글
Springboot - Stomp 간단 코드 (0) | 2024.03.10 |
---|---|
WebSocket 을 사용해보자 - 2. Stomp 프론트 부분 (1) | 2024.01.04 |
Springboot - OAuth2.0 Google API , kakao API , Naver API 로그인 (0) | 2023.12.14 |
SpringBoot - MyBatis - 다이나믹 SQL 작성 (0) | 2023.10.16 |
SpringBoot - SSR에서 페이징 처리에 필요한 객체 PageReq , PageRes (0) | 2023.10.12 |