이전 글에서 사용한 방식은 세션을 직접 관리,처리 해주어야했다.
하지만 스프링에서는 웹소켓에 STOMP를 함께 사용할 수 있는 방법을 지원해주는데, 이를 사용하면 메시지처리를 직접하지 않고 편리하게 통신을 구현할 수 있다.
STOMP란 Simple Text Oriented Messaging Protocol의 약자로 쉽게 메시지를 주고 받을 수 있게 하기 위한 프로토콜이다.
pub/sub 기반으로 작동하며 웹소켓만을 위한 것은 아니나 웹소켓 위에 얹어 편리하게 메시지전송을 구현할수있다.
pub/sub 을 간단하게 예로들면 클라이언트들은 특정 주소(채팅방)를 구독할 수 있고, 메시지를 보낸다면 메시지브로커가 해당 주소를 구독하는 모든 클라이언트들에게 메시지를 보여주는 방식이다.
stomp를 사용해 통신을 구현할때는 이전과 달리 WebSocketMessageBrokerConfigurer를 구현해서 설정파일을 작성한다.
공식문서를 참고할때는 enableSimpleBroker의 경로로 /topic,/queue를 사용하고, setApplicationDestinationPrefixes는 /app을 붙여주어 설명하는데 좀더 직관적인 이해를 위해 sub,pub으로 해주었다.
registerStompEndpoints에는 이전시간 했던것처럼 소켓연결을 위한 endpoint 경로와 cors설정을 해준다.
@Slf4j
@Configuration
@EnableWebSocketMessageBroker
@RequiredArgsConstructor
//@Order(Ordered.HIGHEST_PRECEDENCE + 99)
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
//메시지 구독 url (topic을 구독)
config.enableSimpleBroker("/sub");
//메시지 발행 url
config.setApplicationDestinationPrefixes("/pub");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*");
//.withSockJS();
}
}
DTO를 설정해주는데 이전처럼 session을 가질 필요가 없다.
@Getter
@Setter
public class ChatRoom {
private String roomId; // 채팅방 아이디
private String roomName; // 채팅방 이름
private long userCount; // 채팅방 인원수
private HashMap<String, String> userList = new HashMap<String, String>();
public ChatRoom create(String roomName){
ChatRoom chatRoom = new ChatRoom();
chatRoom.roomId = UUID.randomUUID().toString();
chatRoom.roomName = roomName;
return chatRoom;
}
}
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ChatDTO {
public enum MessageType{
ENTER, TALK, LEAVE;
}
private MessageType type; // 메시지 타입
private String roomId; // 방 번호
private String sender; // 채팅을 보낸 사람
private String message; // 메시지
private String time; // 채팅 발송 시간
}
채팅방을 생성하고 관리하는 기능을 위한 service가 필요한데, 우선 DB를 연결하지 않고 메모리방식으로 예제를 만들기때문에 Repository 하나로 작성한다.
@Repository
@Slf4j
public class ChatRepository {
private Map<String, ChatRoom> chatRoomMap = new LinkedHashMap<>();
// 전체 채팅방 조회
public List<ChatRoom> findAllRoom(){
List chatRooms = new ArrayList<>(chatRoomMap.values());
Collections.reverse(chatRooms);
return chatRooms;
}
// roomID 기준으로 채팅방 찾기
public ChatRoom findRoomById(String roomId){
return chatRoomMap.get(roomId);
}
// roomName 로 채팅방 만들기
public ChatRoom createChatRoom(String roomName){
ChatRoom chatRoom = new ChatRoom().create(roomName);
chatRoomMap.put(chatRoom.getRoomId(), chatRoom);
return chatRoom;
}
// 채팅방 인원+1
public void plusUserCnt(String roomId){
ChatRoom room = chatRoomMap.get(roomId);
room.setUserCount(room.getUserCount()+1);
}
// 채팅방 인원-1
public void minusUserCnt(String roomId){
ChatRoom room = chatRoomMap.get(roomId);
room.setUserCount(room.getUserCount()-1);
}
// 채팅방 유저 리스트에 유저 추가
public String addUser(String roomId, String userName){
ChatRoom room = chatRoomMap.get(roomId);
String userUUID = UUID.randomUUID().toString();
room.getUserlist().put(userUUID, userName);
return userUUID;
}
// 채팅방 유저 리스트 삭제
public void delUser(String roomId, String userUUID){
ChatRoom room = chatRoomMap.get(roomId);
room.getUserlist().remove(userUUID);
}
// 채팅방 userName 조회
public String getUserName(String roomId, String userUUID){
ChatRoom room = chatRoomMap.get(roomId);
return room.getUserlist().get(userUUID);
}
// 채팅방 전체 userlist 조회
public ArrayList<String> getUserList(String roomId){
ArrayList<String> list = new ArrayList<>();
ChatRoom room = chatRoomMap.get(roomId);
room.getUserlist().forEach((key, value) -> list.add(value));
return list;
}
}
이제 기능은 모두 끝났고 요청을 처리하기 위한 controller를 만들면 된다.
@RestController
@Slf4j
@RequiredArgsConstructor
@RequestMapping("/chatroom")
public class ChatRoomController {
private final ChatRepository chatRepository;
// 채팅 리스트 화면
@GetMapping("/")
public ResponseEntity<ResultResponse> goChatRoom(){
List<ChatRoom> chatRooms = chatRepository.findAllRoom();
return ResponseEntity.ok(ResultResponse.of(CREATE_POST_SUCCESS,chatRooms));
}
// 채팅방 생성
@PostMapping("/room")
public ResponseEntity<ResultResponse> createRoom(@RequestParam String name) {
ChatRoom room = chatRepository.createChatRoom(name);
return ResponseEntity.ok(ResultResponse.of(CREATE_POST_SUCCESS,room.getRoomId()));
}
// 채팅에 참여한 유저 리스트 반환
@GetMapping("/userlist")
public ArrayList<String> userList(String roomId) {
return chatRepository.getUserList(roomId);
}
}
@Slf4j
@RequiredArgsConstructor
@RestController
public class ChatController {
private final SimpMessageSendingOperations template;
private final ChatRepository repository;
@MessageMapping("/enterUser")
public void enterUser(@Payload ChatDTO chat, SimpMessageHeaderAccessor headerAccessor) {
repository.plusUserCnt(chat.getRoomId());
String userUUID = repository.addUser(chat.getRoomId(), chat.getSender());
headerAccessor.getSessionAttributes().put("userUUID", userUUID);
headerAccessor.getSessionAttributes().put("roomId", chat.getRoomId());
chat.setMessage(chat.getSender() + " 님 입장!!");
template.convertAndSend("/sub/chat/room/" + chat.getRoomId(), chat);
}
@MessageMapping("/sendMessage")
public void sendMessage(@Payload ChatDTO chat) {
log.info("CHAT {}", chat);
chat.setMessage(chat.getMessage());
template.convertAndSend("/sub/chat/room/" + chat.getRoomId(), chat);
}
}
모든 구현이 끝났다.
저번시간 웹소켓을 테스트할때 사용했던 simple websocket client는 stomp를 지원하지 않기 때문에
이번에는 Apic을 사용해 테스트한다.

위와 같은 형식으로 connect를 하면 연결된다.

메시지를 보낼때는 위와같은 형식으로 보낸다.

같은 주소를 구독하고있는 클라이언트에만 메시지가 전달되는 것을 확인할수있다.
'Web > Spring' 카테고리의 다른 글
[Spring] 소켓통신 이용한 채팅 구현하기 4 - 채팅방 DB저장 (0) | 2023.07.21 |
---|---|
[Spring] 소켓통신 이용한 채팅 구현하기 3 - RabbitMQ 연동 (2) | 2023.07.21 |
[Spring] 소켓통신 이용한 채팅 구현하기 1 (1) | 2023.07.21 |
[Spring security] jwt 로그인 구현에서 OncePerRequestFilter (0) | 2023.07.20 |
[Spring] 자바 네이밍 컨벤션의 중요성 (0) | 2023.07.20 |