항해 10주차 회고 (WIL)

계리·2022년 11월 27일
0
post-custom-banner

항해 10주차 WIL List

Websocket과 STOMP를 이용해서 우리 프로젝트에 맞는 실시간 채팅을 구현

  • Websocket
  • STOMP

STOMP(Simple/Stream Text Oriented Messaging Protocol)란

약어 해석을 하면 간단한 문자 기반 메세징 프로토콜으로 해석되며 메세지를 효율적으로 전송하기 위한 프로토콜로 기본적으로 publish/subscribe 구조로 되어있다. 그래서 메세지를 발신/수신 처리하는 부분이 명확하게 구분되어 있는 구조이기 때문에 개발할 때도 명확하게 인지하고 진행할 수 있다.

  • STOMP 프로토콜은 클라이언트 - 서버 간 전송할 메세지의 유형, 형식, 내용들을 정의한 규칙
  • TCP 또는 Websocket과 같은 양방향 네트워크 프로토콜이다.
  • 헤더에 값을 세팅할 수 있어서 헤더 값을 기반으로 통신 시 인증처리를 구현할 수 있다.

build.gradle

    // 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 관련된 의존성 추가를 하였고 나머지는 각자의 프로젝트에 맞게 추가를 하면 된다.


application.properties

#Websocket
spring.devtools.livereload.enabled=true
spring.devtools.restart.enabled=false
spring.freemarker.cache=false
spring.jackson.serialization.fail-on-empty-beans=false

WebSocketConfig

@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 으로 연결이 된다.


ChatRoom

@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;
    }
}
  • chatRoomId는 채팅방 생성 시 UUID로 생성되어 String type으로 선언하고 @Id 어노테이션을 선언하여 기본 키로 설정한다
  • 독서모임 프로젝트에서는 모임 생성 또는 참가 시 자동으로 채팅방이 생성 또는 참가하게 되어 ENTER(입장), TALK(대화)를 따로 분류할 필요가 없다.
  • chatDate() 메서드는 채팅방 생성 또는 참가 시 나타내는 날짜 메서드이다.

ChatRoomController

@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")

  • 회원이 참여한 모든 모임의 대한 채팅방 리스트들을 불러온다


ChatRoomService

@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);
   }
}

모임 생성 시 채팅방 생성

  • EndPoint URL로 불러오는 것이 아닌 모임 생성 시 채팅방이 자동으로 생성이 되도록 void 타입의 메서드로 구성(다른 서비스에서 호출하는 메서드)

모임 가입 시 채팅방에 추가

  • 모임에 가입한 회원은 해당 모임의 채팅방에 자동으로 추가되되록 void 타입의 메서드로 구성(다른 서비스에서 호출하는 메서드)

ChatMessage

@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;
    }

}
  • 앞 전에 간단한 실시간 채팅에서 enum으로 ENTER(입장), TALK(대화)를 분류할 필요가 없어 해당 부분은 삭제
  • @Builder 어노테이션을 이용하여 메세지 생성

ChatMessageController

@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}"}

  • 해당 채팅방에 메세지들을 보여주기 위한 URL

@MessageMapping("/chat/message")

  • Websocket으로 들어오는 메세지 발행을 처리한다. 클라이언트에서 prefix를 붙여서 /pub/chat/message로 발행(pub) 요청을하면 Controller가 해당 메세지를 받아 처리한다.

  • SimpleMessageSendingOperations클래스로 보낸 메세지를 보여주는 역할을 한다. 클라이언트가 구독(sub)하고 있다가 메세지가 전달되면 화면에 출력한다.

  • temp는 채팅룸을 구분하는 값으로 Topic 역할이라고 보면 된다.


ChatMessageService

@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;
    }
}
  • getMessage 메서드가 해당 채팅방에서 작성한 메세지들을 보여주는 메서드
  • sendMessage 메서드는 회원이 보낸 메세지를 보여주는 메서드

ChatRoomMember

@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

profile
gyery
post-custom-banner

0개의 댓글