토이 프로젝트/Spring&Java 갖고놀기

Spring Boot Web Chatting : 스프링 부트로 실시간 채팅 만들기(2) chatDTO, DAO, Socket.js 코드 알아보기

TerianP 2022. 9. 1.
728x90

10.29 추가 : 일반(문자) 채팅만 구현하는 코드는 git 의 master 브렌치를 참고해주시기 바랍니다.  master-Webrtc-jpa 는 화상 채팅과 jpa 를 이용한 DB 연결을 포함하는 브렌치입니다.

 

1. 기본 개념 설명

STOMP

- STOMP 는 Simple Text Oriented Messaging Protocol 의 약자로 메시지 전송을 위한 프로토콜이다. 기본적인 Websocket 과 가장 크게 다른 점은 기존의 Websocket 만을 사용한 통신은 발신자와 수신자를 Spring 단에서 직접 관리를 해야만 했다. 즉 WebSocketHandler 를 만들어서 WebSocket 통신을 하는 사용자들을 Map 등으로 저장하고 이를 직접 관리하며 클라이언트에서 들어오는 메시지를 다른 사용자에게 전달하는 코드를 직접 구현해야만 했다.

 

그러나 STOMP 는 다르다! stmp 는 pub/sub 기반으로 동작하기 때문에 메시지의 송신, 수신에 대한 처리를 명확하게 정의 할 수 있다. 이 말인 즉슨 추가적으로 코드 작업할 필요 없이 @MessagingMapping 같은 어노테이션을 사용해서 메시지 발행 시 엔드포인트만 조정해줌으로써 훨씬 쉽게 메시지 전송/수신이 가능하다!

그럼 여기서 Sub/Pub 개념에 대해서 알아야 한다.

채팅방 생성 : pub/sub 구현을 위한 Topic 생성 -> 즉 채팅방과 그에 맞는 주제 혹은 채팅방 명을 생각하면 된다.
채팅방 입장 : Topic 구독(sub) -> 해당 채팅방을 웹 소켓이 연결되어있는 동안 구독한다. 구독의 개념은 해당 채팅방을 지속적으로 바라본다라고 생각하면 좋다. 지속적으로 연결되고 바라보고 있기 때문에 새로운 채팅이 송신(pub) 되면 이를 수신(구독, sub) 할 수 있다
==> 이해하기 힘들면 넷플릭스나 디즈니 플러스 같은 '구독' 서비스를 생각해보자. 해당 서비스를 내가 '구독' 하는 상태인 동안 연결해서 내용을 수신(sub) 가능하기 때문!!
채팅방 메시지 수신 : 해당 Topic 로 메시지 송신(pub) -> 해당 채팅방으로 메시지를 송신(pub) 한다.

대략 이런 느낌 && 서버에서는 해당 Topic 을 Sub 하는 모든 인원에게 메시지를 전달한다


2. 코드로 확인하기

- 중요한 내용만 추가 설명을 해두었고, 대부분의 내용은 코드 주석으로 달아두었습니다.

- 설명문과 함께 코드 주석 참고해주세요!

 

1) Gradle 라이브러리 임포트

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

// sockjs
implementation 'org.webjars:sockjs-client:1.5.1'
// stomp
implementation 'org.webjars:stomp-websocket:2.3.4'
// gson
implementation 'com.google.code.gson:gson:2.9.0'

 

2)  SpringConfig - Stomp 엔드포인트 -> sub/pub 엔드포인트 설정

- 여기서 엔드포인트란 일종의 "통신의 도작지점" 이라고 생각하면 될 것 같다. 즉 특정한 통신이 어떤 엔드포인트에 도착했을 때 어떤 행위를 하게 만들것이다라는 것이다.

- 아래에서 처럼 Endpoint 를 "/ws-stomp" 로 설정해두면 웹소켓 통신이 /ws-stomp 로 도착할때 우리는 해당 통신이 웹 소켓 통신 중에서 stomp 통신인 것을 확인하고, 이를 연결한다는 의미이다.

- 추가로 /sub 로 도착하는 것은 메시지를 구독(sub) 할 때 사용하고, "/pub" 로 도착하는 것은 메시지를 송신 할 때 사용하는 엔드포인트가 되는 것이다.

@Configuration
@EnableWebSocketMessageBroker
public class SpringConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // stomp 접속 주소 url => /ws-stomp
        registry.addEndpoint("/ws-stomp") // 연결될 엔드포인트
                .withSockJS(); // SocketJS 를 연결한다는 설정
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        // 메시지를 구독하는 요청 url => 즉 메시지 받을 때
        registry.enableSimpleBroker("/sub");

        // 메시지를 발행하는 요청 url => 즉 메시지 보낼 때
        registry.setApplicationDestinationPrefixes("/pub");
    }
}

 

3) ChatDTO

- 채팅 내용을 위한 DTO => 간단하기 때문에 자세한 내용은 주석 참고

package webChat.dto;

import lombok.*;

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ChatDTO {
    // 메시지  타입 : 입장, 채팅
    // 메시지 타입에 따라서 동작하는 구조가 달라진다.
    // 입장과 퇴장 ENTER 과 LEAVE 의 경우 입장/퇴장 이벤트 처리가 실행되고,
    // TALK 는 말 그대로 내용이 해당 채팅방을 SUB 하고 있는 모든 클라이언트에게 전달된다.
    public enum MessageType{
        ENTER, TALK, LEAVE;
    }

    private MessageType type; // 메시지 타입
    private String roomId; // 방 번호
    private String sender; // 채팅을 보낸 사람
    private String message; // 메시지
    private String time; // 채팅 발송 시간간
}

 

4) ChatRoom

- 채팅룸을 위한 DTO => 간단하기 때문에 자세한 내용은 주석 참고

package webChat.dto;

import lombok.Data;
import java.util.HashMap;
import java.util.UUID;


// Stomp 를 통해 pub/sub 를 사용하면 구독자 관리가 알아서 된다!!
// 따라서 따로 세션 관리를 하는 코드를 작성할 필도 없고,
// 메시지를 다른 세션의 클라이언트에게 발송하는 것도 구현 필요가 없다!
@Data
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;
    }

}

 

5) ChatRepository

- 사실  클래스명은 ChaService 가 맞고, 어노테이션도 @Service 가 맞습니다 : 10.26 수정

- DAO 역할을 하는 ChatRepository 이다.

- Repository 라고 해뒀지만 실제로 하는 일은 Service 단의 내용이 섞여있다. 원래는 DB 와 연결해서 로그인 시 유저끼리 채팅이 가능하도록하는 것을 계획했기 때문에 여기 있는 코드는 추후 DB 와 연결되면 Service 와 Repository 로 분리 예정이다.

- @PostConstruct 어노테이션 : 이 어노테이션은 의존성 주입이 이루어진 후 초기화 작업이 필요한 메서드에 사용된다. 해당 어노테이션이 적용된 초기화 메서드는 WAS 가 띄워질 때 혹은 Bean 이 생성된 후 실행된다.

=>  해당 어노테이션은 채팅방을 만들어 저장하는 ChatRoomMap 에 대해서 사용하였다. 이를 통해서 ChatRepository Bean 이 생성된 후 초기화 되도록 만들었다.

- LinkedHashMap : LinkedHashMap 은 key:value 를 저장할때 HashMap 과는 다르게 순서대로! 저장된다는 특징이 있다. 성능상 HashMap 보다 더 좋다는데 크게 차이가 나는것은 아니라고 한다.

자세한 내용은 아래 레퍼런스 참고

package webChat.dao;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Repository;
import webChat.dto.ChatRoom;

import javax.annotation.PostConstruct;
import java.util.*;

// 추후 DB 와 연결 시 Service 와 Repository(DAO) 로 분리 예정
@Repository
@Slf4j
public class ChatRepository {

    private Map<String, ChatRoom> chatRoomMap;

    @PostConstruct
    private void init() {
        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); // 채팅룸 이름으로 채팅 룸 생성 후

        // map 에 채팅룸 아이디와 만들어진 채팅룸을 저장장
       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();

        // 아이디 중복 확인 후 userList 에 추가
        room.getUserlist().put(userUUID, userName);

        return userUUID;
    }

    // 채팅방 유저 이름 중복 확인
    public String isDuplicateName(String roomId, String username){
        ChatRoom room = chatRoomMap.get(roomId);
        String tmp = username;

        // 만약 userName 이 중복이라면 랜덤한 숫자를 붙임
        // 이때 랜덤한 숫자를 붙였을 때 getUserlist 안에 있는 닉네임이라면 다시 랜덤한 숫자 붙이기!
        while(room.getUserlist().containsValue(tmp)){
            int ranNum = (int) (Math.random()*100)+1;

            tmp = username+ranNum;
        }

        return tmp;
    }

    // 채팅방 유저 리스트 삭제
    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);

        // hashmap 을 for 문을 돌린 후
        // value 값만 뽑아내서 list 에 저장 후 reutrn
        room.getUserlist().forEach((key, value) -> list.add(value));
        return list;
    }
}

 

6) ChatRoomController

- 전체적으로 채팅방을 조회, 생성, 입장을 관리하는 Controller

package webChat.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import webChat.dao.ChatRepository;
import webChat.dto.ChatRoom;

@Controller
@Slf4j
public class ChatRoomController {

    // ChatRepository Bean 가져오기
    @Autowired
    private ChatRepository chatRepository;

    // 채팅 리스트 화면
    // / 로 요청이 들어오면 전체 채팅룸 리스트를 담아서 return
    @GetMapping("/")
    public String goChatRoom(Model model){

        model.addAttribute("list", chatRepository.findAllRoom());
//        model.addAttribute("user", "hey");
        log.info("SHOW ALL ChatList {}", chatRepository.findAllRoom());
        return "roomlist";
    }

    // 채팅방 생성
    // 채팅방 생성 후 다시 / 로 return
    @PostMapping("/chat/createroom")
    public String createRoom(@RequestParam String name, RedirectAttributes rttr) {
        ChatRoom room = chatRepository.createChatRoom(name);
        log.info("CREATE Chat Room {}", room);
        rttr.addFlashAttribute("roomName", room);
        return "redirect:/";
    }

    // 채팅방 입장 화면
    // 파라미터로 넘어오는 roomId 를 확인후 해당 roomId 를 기준으로
    // 채팅방을 찾아서 클라이언트를 chatroom 으로 보낸다.
    @GetMapping("/chat/room")
    public String roomDetail(Model model, String roomId){

        log.info("roomId {}", roomId);
        model.addAttribute("room", chatRepository.findRoomById(roomId));
        return "chatroom";
    }

}

 

7) ChatController

- 채팅을 수신(sub) 하고, 송신(pub) 하기 위한 Controller

- @MessageMapping : 이 어노테이션은 Stomp 에서 들어오는 message 를 서버에서 발송(pub) 한 메시지가 도착하는 엔드포인트이다. 여기서 "/chat/enterUser" 로 되어있지만 실제로는 앞에 "/pub" 가 생략되어있다라고 생각하면 된다. 즉 클라이언트가 "/pub/chat/enterUser"로 메시지를 발송하면 @MessageMapping 에 의해서 아래의 해당 어노테이션이 달린 메서드가 실행된다.

- convertAndSend() : 이 메서드는 매개변수로 각각 메시지의 도착 지점과 객체를 넣어준다. 이를 통해서 도착 지점 즉 sub 되는 지점으로 인자로 들어온 객체를 Message 객체로 변환해서 해당 도작지점을 sub 하고 있는 모든 사용자에게 메시지를 보내주게 된다.

package webChat.controller;

// 임포트는 생략


@Slf4j
@RequiredArgsConstructor
@Controller
public class ChatController {

    private final SimpMessageSendingOperations template;

    @Autowired
    ChatRepository repository;

    // MessageMapping 을 통해 webSocket 로 들어오는 메시지를 발신 처리한다.
    // 이때 클라이언트에서는 /pub/chat/message 로 요청하게 되고 이것을 controller 가 받아서 처리한다.
    // 처리가 완료되면 /sub/chat/room/roomId 로 메시지가 전송된다.
    @MessageMapping("/chat/enterUser")
    public void enterUser(@Payload ChatDTO chat, SimpMessageHeaderAccessor headerAccessor) {

        // 채팅방 유저+1
        repository.plusUserCnt(chat.getRoomId());

        // 채팅방에 유저 추가 및 UserUUID 반환
        String userUUID = repository.addUser(chat.getRoomId(), chat.getSender());

        // 반환 결과를 socket session 에 userUUID 로 저장
        headerAccessor.getSessionAttributes().put("userUUID", userUUID);
        headerAccessor.getSessionAttributes().put("roomId", chat.getRoomId());

        chat.setMessage(chat.getSender() + " 님 입장!!");
        template.convertAndSend("/sub/chat/room/" + chat.getRoomId(), chat);

    }

    // 해당 유저
    @MessageMapping("/chat/sendMessage")
    public void sendMessage(@Payload ChatDTO chat) {
        log.info("CHAT {}", chat);
        chat.setMessage(chat.getMessage());
        template.convertAndSend("/sub/chat/room/" + chat.getRoomId(), chat);

    }

    // 유저 퇴장 시에는 EventListener 을 통해서 유저 퇴장을 확인
    @EventListener
    public void webSocketDisconnectListener(SessionDisconnectEvent event) {
        log.info("DisConnEvent {}", event);

        StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());

        // stomp 세션에 있던 uuid 와 roomId 를 확인해서 채팅방 유저 리스트와 room 에서 해당 유저를 삭제
        String userUUID = (String) headerAccessor.getSessionAttributes().get("userUUID");
        String roomId = (String) headerAccessor.getSessionAttributes().get("roomId");

        log.info("headAccessor {}", headerAccessor);

        // 채팅방 유저 -1
        repository.minusUserCnt(roomId);

        // 채팅방 유저 리스트에서 UUID 유저 닉네임 조회 및 리스트에서 유저 삭제
        String username = repository.getUserName(roomId, userUUID);
        repository.delUser(roomId, userUUID);

        if (username != null) {
            log.info("User Disconnected : " + username);

            // builder 어노테이션 활용
            ChatDTO chat = ChatDTO.builder()
                    .type(ChatDTO.MessageType.LEAVE)
                    .sender(username)
                    .message(username + " 님 퇴장!!")
                    .build();

            template.convertAndSend("/sub/chat/room/" + roomId, chat);
        }
    }

    // 채팅에 참여한 유저 리스트 반환
    @GetMapping("/chat/userlist")
    @ResponseBody
    public ArrayList<String> userList(String roomId) {

        return repository.getUserList(roomId);
    }

    // 채팅에 참여한 유저 닉네임 중복 확인
    @GetMapping("/chat/duplicateName")
    @ResponseBody
    public String isDuplicateName(@RequestParam("roomId") String roomId, @RequestParam("username") String username) {

        // 유저 이름 확인
        String userName = repository.isDuplicateName(roomId, username);
        log.info("동작확인 {}", userName);

        return userName;
    }
}

 

8) Socket.js ; 매우매우매우 중요

- 사실상 채팅의 핵심인 Socket.js 

- 스프링 채팅의 70% 는 얘가 다 해준다고 생각하면 된다. 백엔드단에서 할 것은 오히려 적은 편이라고 생각될 정도

- 처음 웹 통신 시작시 지정된 엔드포인트로 소켓 통신을 시작하고, 지정된 주소를 지속으로 sub(구독) 하게 된다. 또한 지정한 주소로 pub(발송) 하는 역할도 한다.

- 여기는 내 코드에 따라서 수정한 부분이 많다. 다만 소켓 통신에서 가장 중요한 부분인 connect, onConnected, onError, sendMessage, onMessageReceived 정도는 거의 변경하지 않았으니, 이 부분은 꼭 확인하자!

- 자세한 내용은 주석 참고 부탁드립니다! 여기는 제가 잘 모르는 JS 영역이기도 하고, 주석을 보면서 코드를 보면서 해야 더 이해가 쉬울 것 같습니다. 코드 자체를 어렵지 않거든요!

'use strict';

// document.write("<script src='jquery-3.6.1.js'></script>")
document.write("<script\n" +
    "  src=\"https://code.jquery.com/jquery-3.6.1.min.js\"\n" +
    "  integrity=\"sha256-o88AwQnZB+VDvE9tvIXrMQaPlFFSUTR+nldQm1LuPXQ=\"\n" +
    "  crossorigin=\"anonymous\"></script>")


var usernamePage = document.querySelector('#username-page');
var chatPage = document.querySelector('#chat-page');
var usernameForm = document.querySelector('#usernameForm');
var messageForm = document.querySelector('#messageForm');
var messageInput = document.querySelector('#message');
var messageArea = document.querySelector('#messageArea');
var connectingElement = document.querySelector('.connecting');

var stompClient = null;
var username = null;

var colors = [
    '#2196F3', '#32c787', '#00BCD4', '#ff5652',
    '#ffc107', '#ff85af', '#FF9800', '#39bbb0'
];

// roomId 파라미터 가져오기
const url = new URL(location.href).searchParams;
const roomId = url.get('roomId');

function connect(event) {
    username = document.querySelector('#name').value.trim();

    // username 중복 확인
    isDuplicateName();

    // usernamePage 에 hidden 속성 추가해서 가리고
    // chatPage 를 등장시킴
    usernamePage.classList.add('hidden');
    chatPage.classList.remove('hidden');

    // 연결하고자하는 Socket 의 endPoint
    var socket = new SockJS('/ws-stomp');
    stompClient = Stomp.over(socket);

    stompClient.connect({}, onConnected, onError);


    event.preventDefault();


}

function onConnected() {

    // sub 할 url => /sub/chat/room/roomId 로 구독한다
    stompClient.subscribe('/sub/chat/room/' + roomId, onMessageReceived);

    // 서버에 username 을 가진 유저가 들어왔다는 것을 알림
    // /pub/chat/enterUser 로 메시지를 보냄
    stompClient.send("/pub/chat/enterUser",
        {},
        JSON.stringify({
            "roomId": roomId,
            sender: username,
            type: 'ENTER'
        })
    )

    connectingElement.classList.add('hidden');

}

// 유저 닉네임 중복 확인
function isDuplicateName() {

    $.ajax({
        type: "GET",
        url: "/chat/duplicateName",
        data: {
            "username": username,
            "roomId": roomId
        },
        success: function (data) {
            console.log("함수 동작 확인 : " + data);

            username = data;
        }
    })

}

// 유저 리스트 받기
// ajax 로 유저 리스를 받으며 클라이언트가 입장/퇴장 했다는 문구가 나왔을 때마다 실행된다.
function getUserList() {
    const $list = $("#list");
    
    $.ajax({
        type: "GET",
        url: "/chat/userlist",
        data: {
            "roomId": roomId
        },
        success: function (data) {
            var users = "";
            for (let i = 0; i < data.length; i++) {
                //console.log("data[i] : "+data[i]);
                users += "<li class='dropdown-item'>" + data[i] + "</li>"
            }
            $list.html(users);
        }
    })
}


function onError(error) {
    connectingElement.textContent = 'Could not connect to WebSocket server. Please refresh this page to try again!';
    connectingElement.style.color = 'red';
}

// 메시지 전송때는 JSON 형식을 메시지를 전달한다.
function sendMessage(event) {
    var messageContent = messageInput.value.trim();

    if (messageContent && stompClient) {
        var chatMessage = {
            "roomId": roomId,
            sender: username,
            message: messageInput.value,
            type: 'TALK'
        };

        stompClient.send("/pub/chat/sendMessage", {}, JSON.stringify(chatMessage));
        messageInput.value = '';
    }
    event.preventDefault();
}

// 메시지를 받을 때도 마찬가지로 JSON 타입으로 받으며,
// 넘어온 JSON 형식의 메시지를 parse 해서 사용한다.
function onMessageReceived(payload) {
    //console.log("payload 들어오냐? :"+payload);
    var chat = JSON.parse(payload.body);

    var messageElement = document.createElement('li');

    if (chat.type === 'ENTER') {  // chatType 이 enter 라면 아래 내용
        messageElement.classList.add('event-message');
        chat.content = chat.sender + chat.message;
        getUserList();

    } else if (chat.type === 'LEAVE') { // chatType 가 leave 라면 아래 내용
        messageElement.classList.add('event-message');
        chat.content = chat.sender + chat.message;
        getUserList();

    } else { // chatType 이 talk 라면 아래 내용용
        messageElement.classList.add('chat-message');

        var avatarElement = document.createElement('i');
        var avatarText = document.createTextNode(chat.sender[0]);
        avatarElement.appendChild(avatarText);
        avatarElement.style['background-color'] = getAvatarColor(chat.sender);

        messageElement.appendChild(avatarElement);

        var usernameElement = document.createElement('span');
        var usernameText = document.createTextNode(chat.sender);
        usernameElement.appendChild(usernameText);
        messageElement.appendChild(usernameElement);
    }

    var textElement = document.createElement('p');
    var messageText = document.createTextNode(chat.message);
    textElement.appendChild(messageText);

    messageElement.appendChild(textElement);

    messageArea.appendChild(messageElement);
    messageArea.scrollTop = messageArea.scrollHeight;
}


function getAvatarColor(messageSender) {
    var hash = 0;
    for (var i = 0; i < messageSender.length; i++) {
        hash = 31 * hash + messageSender.charCodeAt(i);
    }

    var index = Math.abs(hash % colors.length);
    return colors[index];
}

usernameForm.addEventListener('submit', connect, true)
messageForm.addEventListener('submit', sendMessage, true)

 

 

9) chatroom && roollist html

- chatroom 과 roomlist html 는 설명에서 빼두었습니다. 코드 길이도 엄청 길고, 사실 저도 이 부분들은 검색&다른 사람들것 가져와서 살짝 수정한 정도라 설명하기 어렵기도 하고 자칫 잘못 설명할까봐 부끄럽기도 해서요ㅠㅠ

- 뭣보다 제가 html 을 정말 정말...못하는지라ㅠㅠ 이 부분들은 git 참고해서 확인 부탁드립니다!


- Reference

https://velog.io/@qkrqudcks7/STOMP%EB%9E%80

 

STOMP란?

websocket 위에서 동작하는 문자 기반 메세징 프로토콜로써 클라이언트와 서버가 전송할 메세지의 유형, 형식, 내용들을 정의하는 매커니즘이다.TCP와 웹소켓과 같은 신뢰할 수 있는 양방향 스트리

velog.io

https://wecandev.tistory.com/105

 

[Spring] @PostConstruct와 @PreDestroy 어노테이션

@PostConstruct와 @PreDestroy Annotation 사용하기 1. 언제 쓰는가 최근에 개발하다가 딱 한번만 받아오면 되는 값을 사용 시 마다 요청을 새로 호출하는 실수를 했다. 아니 누가 이따위로 개발했는가 하고

wecandev.tistory.com

https://velog.io/@ha0kim/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%9D%B8-%EC%95%A1%EC%85%98-8.%EB%B9%84%EB%8F%99%EA%B8%B0-%EB%A9%94%EC%8B%9C%EC%A7%80-%EC%A0%84%EC%86%A1%ED%95%98%EA%B8%B0

 

[스프링 인 액션] 8.비동기 메시지 전송하기

이 장에서 배우는 내용비동기 메시지 전송JMS, RabbitMQ, 카프카를 사용해서 메시지 전송하기보로커에서 메시지 가져오기메시지 리스닝하기이전 장에서는 REST를 사용한 동기화 통신을 알아보았다.

velog.io

 

HashMap vs LinkedHashMap 성능 비교

https://stackoverflow.com/questions/12998568/hashmap-vs-linkedhashmap-performance-in-iteration-over-values

 

HashMap vs LinkedHashMap performance in iteration over values()

Is there any performance difference between HashMap and LinkedHashMap for traversal through values() function?

stackoverflow.com

 

댓글