[Spring] 실시간 채팅 기능 구현-1

·2023년 1월 11일
7

킁킁메이트

목록 보기
6/10
post-thumbnail

💡개발 환경
Java 11, Srping 2.7.X, MySQL

시나리오

사용자 요구 사항

  1. 일대일 실시간 채팅이 가능해야 한다.

ERD 다이어그램

[TIL] 채팅 기능 ERD 설계 에서 ERD 설계를 진행 하였지만 구독한 채팅방의 정보와 해당 채팅방을 구독하고 있는 사용자의 정보를 List 값으로 저장하게 하면서 멘붕에 빠졌다.

도저히 이 방법으로는 프로젝트 기간 내에 해결할 수 없겠다 싶어서 호다닥 변경하였다.

회원과 채팅방의 관계는 회원(1):채팅방(N)을 가지며, 일대일 채팅인 점을 고려하여 1:N 매핑을 두번 맺게 하였다.

채팅방과 메세지의 관계는 채팅방(1):메세지(N)을 가진다!

WebSocket

[Web] 웹 소켓(Socket)에서 학습한대로 채팅 기능을 구현하기 위해서는 WebSocket을 사용해야 한다.

🚨 WebSocket만 사용하여 구현하더라도 충분히 채팅 기능을 완성할 수 있지만, WebSocket은 단순 통신 구조이기 때문에 채팅 메세지가 어떤 요청인지, 어떻게 처리해야 하는지에 대한 로직을 별도로 구현해야 해야 한다.

이는 가독성 측면에서도, 효율성 면에서도 부적절한 방법이라고 생각하였다.

STOMP

:Simple Text Oriented Messaging Protocol의 약자로 메세징 전송을 효율적으로 하기 위한 프로토콜이다.

  • STOMP 프로토콜은 WebSocket 위에서 동작하며, 클라이언트와 서버가 전송할 메세지의 유형, 형식, 내용들을 정의한다.
  • 메세지를 공급하는 주체와 소비하는 주체를 분리해 제공하는 메세징 방법인 publish(발행)/Subscribe(구독) 방식을 사용한다.

메세지 브로커

WebSocket은 메세지 브로커에서 클라이언트로 다량의 메세지가 빠른 속도로 전송되는 일반적인 메신저 구조에 매우 적합하여, STOMP와 외부 메세지 브로커를 함께 사용하는 것이 보편적이다.

💡 Spring에서 지원하는 STOMP를 사용하면 외부 메세지 브로커의 사용없이 STOMP의 기능 중 Simple In-Memory Broker를 이용해 SUBSCRIBER 중인 다른 클라이언트에게 메세지를 전송할 수 있다.

기본 설정

의존성 라이브러리 추가

implementation 'org.springframework.boot:spring-boot-starter-websocket'

WebSocketConfig

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    // sockJS Fallback을 이용해 노출할 endpoint 설정
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // 웹소켓이 handshake를 하기 위해 연결하는 endpoint
        registry.addEndpoint("/ws")
                .setAllowedOriginPatterns("*")
                .withSockJS();

    }

    //메세지 브로커에 관한 설정
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        // 서버 -> 클라이언트로 발행하는 메세지에 대한 endpoint 설정 : 구독
        registry.enableSimpleBroker("/sub");

        // 클라이언트->서버로 발행하는 메세지에 대한 endpoint 설정 : 구독에 대한 메세지
        registry.setApplicationDestinationPrefixes("/pub");
    }
}

WebSocketMessageBrokerConfigurer를 구현한 WebSocketConfig는 STOMP 엔드포인드를 설정하고, 메세지 브로커가 사용할 pub/sub 엔드포인트를 설정한다.

💡 withSockJS()
SockJS는 WebSocket을 지원하지 않는 브라우저에서 HTTP의 Polling과 같은 방식으로 WebSocket의 요청을 수행하도록 도와준다.

🚨 SockJS를 사용할 경우, 클라이언트에서 WebSocket 요청을 보낼 때 설정한 엔드포인트 뒤에 /webSocket를 추가해줘야 정상 작동된다.

ChatRoom

@Entity
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ChatRoom extends Auditable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long roomId;

    @ManyToOne
    @JoinColumn(name = "sender_id")
    private Member sender;

    @ManyToOne
    @JoinColumn(name = "receiver_id")
    private Member receiver;

    public void setSender(Member sender) {
        this.sender = sender;
    }

    public void setReceiver(Member receiver) {
        this.receiver = receiver;
    }
}

ChatMessage

@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ChatMessage  {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long messageId;

    @Column(nullable = false)
    private String content;

    @Column(nullable = false)
    private LocalDateTime sendTime;

    @ManyToOne
    @JoinColumn(name = "sender_id")
    private Member sender;

    @ManyToOne
    @JoinColumn(name = "room_id")
    private ChatRoom chatRoom;

    public void setMember(Member sender) {
        this.sender = sender;
    }

    public void setChatRoom(ChatRoom chatRoom) {
        this.chatRoom = chatRoom;
    }

}
profile
🧑‍💻백엔드 개발자, 조금씩 꾸준하게

13개의 댓글

comment-user-thumbnail
2023년 11월 23일

안녕하세요 혤님 혹시 테이블에 sender, reciever로 한 이유 알 수 있을까요? 그리고 레디스에 메시지 내용 저장하는 걸로 알고있는데 메시지테이블에 메시지내용행은 어떤걸 저장하나요??

1개의 답글
comment-user-thumbnail
2023년 11월 26일

안녕하세요! 혤님! 수신인 테이블은 없는건가요? message테이블 = 수신인(상대방)이라고 생각하면 되는 걸까요??!

1개의 답글