Websocket과 STOMP를 이용해서 우리 프로젝트에 맞는 실시간 채팅을 구현
약어 해석을 하면 간단한 문자 기반 메세징 프로토콜으로 해석되며 메세지를 효율적으로 전송하기 위한 프로토콜로 기본적으로 publish/subscribe 구조로 되어있다. 그래서 메세지를 발신/수신 처리하는 부분이 명확하게 구분되어 있는 구조이기 때문에 개발할 때도 명확하게 인지하고 진행할 수 있다.
// WebSocket
implementation 'org.springframework.boot:spring-boot-starter-websocket'
implementation 'org.webjars:sockjs-client:1.1.2'
implementation 'org.webjars:stomp-websocket:2.3.3-1'
Websocket과 STOMP 관련된 의존성 추가를 하였고 나머지는 각자의 프로젝트에 맞게 추가를 하면 된다.
#Websocket
spring.devtools.livereload.enabled=true
spring.devtools.restart.enabled=false
spring.freemarker.cache=false
spring.jackson.serialization.fail-on-empty-beans=false
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/wss/chat").setAllowedOriginPatterns("*").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/sub");
registry.setApplicationDestinationPrefixes("/pub");
}
}
Stomp를 사용하기 위해 @EnableWebSocketMessageBroker을 선언하고 WebSocketMessageBrokerConfigurer를 상속받아 configureMessageBroker를 구현한다. pub/sub 메시징을 구현하기 위해 메시지를 발행하는 요청의 prefix는 /pub로 시작하도록 설정하고 메시지를 구독하는 요청의 prefix는 /sub로 시작하도록 설정한다. 그리고 stomp websocket의 연결 endpoint는 /ws/chat로 설정하여 서버의 접속 주소는 다음과 같이 ws://localhost:8080/ws/chat 으로 연결이 된다.
@Getter
@Entity
@NoArgsConstructor
public class ChatRoom extends TimeStamped{
@Id
private String chatRoomId;
@Column(nullable = false)
private String title;
public ChatRoom(String chatRoomId, String title){
this.chatRoomId = chatRoomId;
this.title = title;
}
public void update(ClubRequestDto clubRequestDto){
this.title = clubRequestDto.getClubName();
}
public String chatDate(){
LocalDateTime now = LocalDateTime.now();
String date = now.format(DateTimeFormatter.ofPattern("yyyy년 MM월 dd일 E요일 a hh:mm:ss", Locale.KOREA));
return date;
}
}
@RestController
@RequiredArgsConstructor
@RequestMapping("/chat")
public class ChatRoomController {
private final ChatRoomService chatRoomService;
// 모임 참여한 모든 채팅방 조회
@GetMapping("/rooms")
public ResponseDto<?> findAllChatRoom(@AuthenticationPrincipal UserDetailsImpl userDetails){
return chatRoomService.findAllChatRoom(userDetails.getMember());
}
}
@GetMapping("/rooms")
회원이 참여한 모든 모임의 대한 채팅방 리스트들을 불러온다
@Service
@RequiredArgsConstructor
public class ChatRoomService {
private final ChatRoomRepository chatRoomRepository;
private final ChatRoomMemberRepository chatRoomMemberRepository;
private final ChatMessageRepository chatMessageRepository;
// 모임 개설 시 채팅방 개설
public void createdMemberChatRoom(Member member, Club club) {
String createdChatRoomId = UUID.randomUUID().toString();
ChatRoom createdChatRoom = new ChatRoom(createdChatRoomId, club.getClubName());
chatRoomRepository.save(createdChatRoom);
ChatRoomMember chatRoomMember = new ChatRoomMember(createdChatRoom, member, club, false);
chatRoomMemberRepository.save(chatRoomMember);
ChatMessage chatMessage = new ChatMessage(createdChatRoomId
, "SYSTEM"
, "SYSTEM"
, "채팅방 대화가 시작되었습니다 :)"
, createdChatRoom.chatDate());
chatMessageRepository.save(chatMessage);
}
// 모임 가입 시 채팅방에 추가
public void addMemberChatRoom(Member member, Club club) {
ChatRoomMember getChatRoom = chatRoomMemberRepository.findByClubAndMember(club, club.getLeader());
String chatRoomId = getChatRoom.getChatRoom().getChatRoomId();
ChatRoom chatRoom = getChatRoom.getChatRoom();
ChatRoomMember chatRoomMember = new ChatRoomMember(chatRoom, member, club, false);
chatRoomMemberRepository.save(chatRoomMember);
ChatMessage chatMessage = new ChatMessage(chatRoomId
, "SYSTEM"
, "SYSTEM"
, member.getMemberId() + "님이 참여 하였습니다. :)"
, chatRoom.chatDate());
chatMessageRepository.save(chatMessage);
}
// 모임 탈퇴 시 채팅방 삭제
public void deleteMemberChatRoom(Member member, Club club) {
ChatRoomMember deleteChatRoomMember = chatRoomMemberRepository.findByClubAndMember(club, member);
chatRoomMemberRepository.delete(deleteChatRoomMember);
}
// 모임 참여한 모든 채팅방 조회
public ResponseDto<?> findAllChatRoom(Member member) {
List<ChatRoomMember> chatRoomMemberList = chatRoomMemberRepository.findAllByMember(member);
List<ChatRoomMemberResponseDto> chatRoomMemberResponseDtoList = new ArrayList<>();
for (ChatRoomMember chatRoomMember : chatRoomMemberList) {
chatRoomMemberResponseDtoList.add(new ChatRoomMemberResponseDto(chatRoomMember));
}
return ResponseDto.success(chatRoomMemberResponseDtoList);
}
}
모임 생성 시 채팅방 생성
모임 가입 시 채팅방에 추가
@Getter
@NoArgsConstructor
@Entity
public class ChatMessage {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long messageId;
@Column
private String chatRoomId;
@Column(nullable = false)
private String type;
@Column(nullable = false)
private String sender;
@Column
private String message;
@Column
private String date;
@Builder
public ChatMessage(String chatRoomId, String type, String sender, String message, String date){
this.chatRoomId = chatRoomId;
this.type = type;
this.sender = sender;
this.message = message;
this.date = date;
}
}
@RestController
@RequiredArgsConstructor
public class ChatMessageController {
private final ChatMessageService chatMessageService;
private final SimpMessageSendingOperations simpMessageSendingOperations;
@GetMapping("/chat/messages/{roomNo}")
public ResponseDto<?> getMessages(@PathVariable String roomNo, @AuthenticationPrincipal UserDetailsImpl userDetails) {
return chatMessageService.getMessages(userDetails, roomNo);
}
@MessageMapping("/chat/message")
public void sendMessage(MessageRequestDto requestDto) {
String temp = "/sub/chat/messages/" + requestDto.getChatRoomId();
simpMessageSendingOperations.convertAndSend( temp, chatMessageService.sendMessage(requestDto));
}
}
간단한 실시간 채팅 만들 때 WebSocketHandler의 역할(pub/sub)을 ChatMessageController에서 처리 해주기 때문에 WebSocketHandler는 필요가 없다
@GetMapping("chat/message/{roomNo}"}
@MessageMapping("/chat/message")
Websocket으로 들어오는 메세지 발행을 처리한다. 클라이언트에서 prefix를 붙여서 /pub/chat/message로 발행(pub) 요청을하면 Controller가 해당 메세지를 받아 처리한다.
SimpleMessageSendingOperations클래스로 보낸 메세지를 보여주는 역할을 한다. 클라이언트가 구독(sub)하고 있다가 메세지가 전달되면 화면에 출력한다.
temp는 채팅룸을 구분하는 값으로 Topic 역할이라고 보면 된다.
@Service
@RequiredArgsConstructor
public class ChatMessageService {
private final ChatMessageRepository chatMessageRepository;
public ResponseDto<?> getMessages(UserDetailsImpl userDetails, String roomNo) {
List<ChatMessage> messageList = chatMessageRepository.findAllByChatRoomId(roomNo);
List<MessageResponseDto> messageResponseDtoList = new ArrayList<>();
for (ChatMessage chatMessage : messageList) {
messageResponseDtoList.add(MessageResponseDto.builder()
.chatRoomId(chatMessage.getChatRoomId())
.type(chatMessage.getType())
.sender(chatMessage.getSender())
.message(chatMessage.getMessage())
.date(chatMessage.getDate())
.build()
);
}
return ResponseDto.success(messageResponseDtoList);
}
public Object sendMessage(MessageRequestDto requestDto){
LocalDateTime now = LocalDateTime.now();
String date = now.format(DateTimeFormatter.ofPattern("yyyy년 MM월 dd일 E요일 a hh:mm:ss", Locale.KOREA));
ChatMessage chatMessage = ChatMessage.builder()
.chatRoomId(requestDto.getChatRoomId())
.type(requestDto.getType())
.sender(requestDto.getSender())
.message(requestDto.getMessage())
.date(date)
.build();
chatMessageRepository.save(chatMessage);
MessageResponseDto responseDto = MessageResponseDto.builder()
.chatRoomId(chatMessage.getChatRoomId())
.type(chatMessage.getType())
.sender(chatMessage.getSender())
.message(chatMessage.getMessage())
.date(chatMessage.getDate())
.build();
return responseDto;
}
}
@Entity
@Getter
@NoArgsConstructor
public class ChatRoomMember {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long chatRoomMemberId;
@JoinColumn(name = "chatRoomId", nullable = false)
@ManyToOne(fetch = FetchType.LAZY)
private ChatRoom chatRoom;
@JoinColumn(name = "memberId", nullable = false)
@ManyToOne(fetch = FetchType.LAZY)
private Member member;
@JoinColumn(name = "clubId", nullable = false)
@ManyToOne
private Club club;
@Column(nullable = false)
private Boolean enterStatus;
@Builder
public ChatRoomMember(ChatRoom chatRoom, Member member, Club club, Boolean enterStatus) {
this.chatRoom = chatRoom;
this.member = member;
this.club = club;
this.enterStatus = enterStatus;
}
public void updateEnterStatus() {
this.enterStatus = true;
}
}
해당 채팅방 인원들에 대한 정보들을 관리하기 위해 ChatRoomMember entity 생성
회원과 해당 모임의 채팅방 생성 또는 참여를 위해 ChatRoom, Member, Club 3개의 entity 연관관계(Join)를 맺어 해당 회원이 어떤 모임인지 확인하여 채팅방 개설 또는 참여하도록 구성
자세한 내용은 아래 저희 프로젝트 깃허브를 통해 확인 해주시면 됩니다.
프로젝트 깃허브 : https://github.com/Hanghae-Hot6/Back