스프링부트 1:1 메세지 기능 WebSocket / STOMP / SockJS 에 대하여 (2/2)

조정우·2025년 4월 17일
0

오늘의 기능 : 오늘은 직접 코드로 WebSocket을 사용해서 1:1 메세지 기능을 만들어 볼것이다

build.gradle 에 dependencies 추가해주기
아마 의존성을 넣어주면 reload 되면서 plugin을 추가 해주라고 할 것이다 그냥하면된다

// build.gradle
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-websocket'
}

1. WebSocketConfig 추가

@Configuration

  • 설정파일을 만들기 우한 애노테이션 or Bean을 등록하기 위한 애노테이션
  • 싱글톤 패턴은 객체 지향 프로그래밍에서 특정 클래스가 단 하나만의 인스턴스를 생성해준다
    @EnableWebSocketMessageBroker
  • WebSocket 메시징 기능(STOMP 포함)을 활성화 한다는 뜻이야

registerStompEndpoints()

  • ws : 클라이언트가 연결할 WebScoket 엔드포인트 , JS에서 new SockJs("/ws") 로 연결
  • setAllowedOriginPatterns("*") : CORS 허용 , 어디서든 접속 가능하도록 허용함
  • withSockJS() : 브라우저에서 WebSocket이 안 될 떄를 대비해 SockJS로 fallback 기능

setApplicationDestinationPrefixes("/app")

  • 클라이언트가 서버로 메시지를 보낼 때 사용하는 prefix
    클라리언트가 메세지를 보낼 때 "/app"를 앞에 붙여서 보낸다는 뜻

registry.enableSimpleBroker("/topic")

  • 1:1 쪽지 기능할 때 사용하는 설정이야
  • 서버가 클라이언트에게 보낼 때 사용하는 구독 주소 prefix
@Configuration // 이 클래스는 Spring 설정 파일이라는 뜻이다
@EnableWebSocketMessageBroker // WebSocket 메세지 기능(STOMP 포함)을 활성화 한다는 뜻
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        //연결할 endpoint ("/ws")
        registry.addEndpoint("/ws")
                .setAllowedOriginPatterns("*") //CORS 허용
                .withSockJS(); // SockJs 지원
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        // 클라이언트에서 구득할 경로
        registry.enableSimpleBroker("/topic");
        // 클라이너트에서 메시지 보낼 때 사용하는 prefix
        registry.setApplicationDestinationPrefixes("/app");
    }
}

✅ 1:1 쪽지 기능을 위한 테이블 구조

2. Message DB 구현하기

  • messageIdx : 현재 메세지 번호
    senderIdx -> 메세지를 보내는 유저
    receiverIdx -> 메세지를 받는 유저
    content -> 메세지 내용
    sendAt -> 메세지를 보낸 시간
    isRead -> 메세지를 읽었는지 체크
    imageContent -> 메세지 이미지 보내기

MessageDTO 구현 하기 실직적으로 데이터를 가져온다 여기서

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class MessageDTO {
    private Long senderIdx;
    private Long receiverIdx;
    private String content;
    private String imagesContent;
    private LocalDateTime sendAt;
    private boolean isRead;
}

3. 클라이언트에서 소켓 연결 요청

connectSocket()

  • 여기서 클라이언트는 서버의 /ws 엔드포인트로 SockJS를 통해 WebSocket 연결을 시도한다
  • stompClient.subscribe() 클라인언트는 서버가 메세지를 보내주는 /topic/message 주소를 구독한다

sendMessage()

  • input에서 받은 값을 @MessageMapping 로 보내준다 ( /chat/send/ )
function connectSocket() {
        const socket = new SockJS('/ws');
        stompClient = Stomp.over(socket);

        stompClient.connect({}, function () {
            console.log('Connected to WebSocket');

            // 메시지 수신 구독
            stompClient.subscribe('/topic/messages', function (message) {
                const msg = JSON.parse(message.body);
                console.log('메세지 도착:', msg);
                showMessage(msg);
            });
        });
    }

    // 직접 메세지를 chat/send 로 보내준다
    function sendMessage() {
        const senderIdx = document.getElementById("loginIdx").value;
        const receiverIdx = document.getElementById("targetIdx").value;
        const message = document.getElementById("msgInput").value;
        const now = new Date();
        const sendAt = now.toISOString(); // ISO 8601 형식 (서버, JS 모두 호환)

        stompClient.send("/app/chat/send", {}, JSON.stringify({
            senderIdx: senderIdx,
            receiverIdx: receiverIdx,
            content: message,
            sendAt: sendAt,
            imagesContent: ''
        }));

        // 입력창 초기화
        document.getElementById("msgInput").value = '';
        document.getElementById("sendMessage").classList.add("deactive");
    }
  • 서버에서 @messageMapping 처리 (/app/chat/send) 로 온 메세지는 @MessageMapping("/chat/send") 에서 처리 되고
    처리된 메세지는 @SendTo("/topic/messages")를 통해 구독자들에게 브로드캐스트가 된다
public class ChatController {

    private final MessageService messageService;

    // app/chat/send로 전송된 메시지를 처리
    @MessageMapping("/chat/send")
    @SendTo("/topic/messages")
    public MessageDTO sendMessage(MessageDTO messageDTO) {
        messageService.saveMessage(messageDTO);

        return messageDTO;
    }
}

3. Message Service

html 에서 받아온 메세지 내용을 DTO -> 보내서 DB에 저장한다

public void saveMessage(MessageDTO dto) {

        Message message = new Message();
        
        System.out.println("💬 저장된 메시지 내용: " + message.getContent());
        message.setSenderIdx(dto.getSenderIdx());
        message.setReceiverIdx(dto.getReceiverIdx());
        message.setContent(dto.getContent());
        message.setImagesContent(dto.getImagesContent());
        message.setSendAt(LocalDateTime.now());
        dto.setSendAt(LocalDateTime.now());
        message.setRead(false);

        messageRepository.save(message);

    }

4. 현재 까지 보내거나 받은 메세지 리스트 Controller 에서 처리

  1. 현재 로그인한 유저의 기준으로 idx 값을 받아온다
  2. 로그인 유저의 IDX 갑으로 Message senderUser , receiverUser 로 각각 검색
  3. 해당하는 DB에 중복없이 각 senderUser DB에 포함되는 -> receiveridx 값으로 해당 유저의 정보를 가져온다 receiverUser 도 같다
  4. 그리고 해당하는 메세지 중에 제일 최근 recently 메세지를 들고와서리스트에서 보여준다
List<Message> senderUser = messageRepository.findBySenderIdx(loginUser.getIdx());
        for (Message message : senderUser) {
            if (!addedIds.contains(message.getReceiverIdx())) {
                Member member = memberRepository.findByIdx(message.getReceiverIdx())
                        .orElseThrow(() -> new RuntimeException("User not found"));
                if (keyword == null || member.getUserName().contains(keyword)) {
                    SendUserList.add(member);
                    addedIds.add(message.getReceiverIdx());

                    // 제일 최근 메세지 들고오기
                    Optional<Message> recently = messageRepository
                            .findTopBySenderIdxAndReceiverIdxOrderBySendAtDesc(loginUser.getIdx(), message.getReceiverIdx());

                    recently.ifPresent(msg -> RecentlyMessage.put(message.getReceiverIdx(), msg));
                }
            }
        }

        List<Message> receiverUser = messageRepository.findByReceiverIdx(loginUser.getIdx()); // 나에게 메세지를 보낸 유저
        for (Message message : receiverUser) {
            if (!addedIds.contains(message.getSenderIdx())) {
                Member member = memberRepository.findByIdx(message.getSenderIdx())
                        .orElseThrow(() -> new RuntimeException("User not found"));
                if (keyword == null || member.getUserName().contains(keyword)) {
                    SendUserList.add(member);
                    addedIds.add(message.getSenderIdx());

                    // 제일 최근 메세지 들고오기
                    Optional<Message> recently = messageRepository
                            .findTopBySenderIdxAndReceiverIdxOrderBySendAtDesc(message.getSenderIdx(), loginUser.getIdx());
                    recently.ifPresent(msg -> RecentlyMessage.put(message.getSenderIdx(), msg));
                }
            }

            // 상대가 나에게 보낸 메시지 중 안 읽은 것만 필터링
            List<Message> unreadMessages = messageRepository.findBySenderIdxAndReceiverIdxAndIsReadFalse(message.getSenderIdx(), loginUser.getIdx());

            if (!unreadMessages.isEmpty()) {
                unreadMessageCountMap.put(message.getSenderIdx(), unreadMessages.size());
            }
        }

5. Messsage 보내기 이미지

  1. 메세지 보내기
  2. 메세지 입력 완료
  3. 메세지 DB에서 확인
  4. 메세지 목록 리스트
profile
)개발( 마구잡이로 글쓰기

0개의 댓글