Backend
home

2025-6-18 (수)

생성일
2025/06/17 15:50
태그
WebSocket
취업특강
Docker
MySQL

1. 네이버 새로고침

새로고침을 하면 계속 HTTP 요청을 하는 것을 의미 - 실시간 X

2. 통신 방식

3. WebSocket이란

WebSocket 은 양방향 통신이다.
클라이언트와 서버가 지속적으로 통신이 가능하다.
WebSocket도 연결을 하기 위해선 최초에 한번은 HTTP 요청을 보낸다.

4. Spring에서 WebSocket을 사용하는 두 가지

Spring WebSocket

Spring WebSocket + STOMP

STOMP 프로토콜이 해주는 일
메시지 목적지 헤더에 명시
명령어로 구분
헤더/바디 분리
브로커가 각 채널 구독자에게만 자동으로 메시지 분배
왜 STOMP?
생산성과 유지보수성
코드가 간결하고 실수 가능성이 적음
확장성/변경 용이

5. WebSocket vs STOMP over WebSocket

6. 실습

순수 WebSocket 채팅 서버 실습

ChatWebSocketHandler 생성
package org.example.backendproject.purewebsocket.handler; import com.fasterxml.jackson.databind.ObjectMapper; import java.util.Collections; import java.util.HashSet; import java.util.Set; import org.example.backendproject.purewebsocket.dto.ChatMessage; import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.handler.TextWebSocketHandler; public class ChatWebSocketHandler extends TextWebSocketHandler { // 동시성 문제를 해결 - 서버에 여러 클라이언트 접속 시에 발생할 수 있는 데이터 손실 고려 private final Set<WebSocketSession> sessions = Collections.synchronizedSet(new HashSet<>()); // json 문자열 -> 자바 객체로 변환 private final ObjectMapper objectMapper = new ObjectMapper(); // 클라이언트가 보낸 메세지를 서버가 받았을 때 호출 @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { super.handleTextMessage(session, message); // json 문자열 -> 자바 객체 ChatMessage chatMessage = objectMapper.readValue(message.getPayload(), ChatMessage.class); for (WebSocketSession s : sessions) { if (s.isOpen()) { // 자바 객체 -> json 문자열 s.sendMessage(new TextMessage(objectMapper.writeValueAsString(chatMessage))); System.out.println("전송된 메세지 = " + chatMessage.getMessage()); } } } // 클라이언트가 웹 소켓 서버에 접속했을 때 호출 @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { super.afterConnectionEstablished(session); sessions.add(session); // 연결된 클라이언트 저장 System.out.println("접속된 클라이언트 세션 ID = " + session.getId()); } // 클라이언트가 연결이 끊어졌을 때 호출 @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { super.afterConnectionClosed(session, status); sessions.remove(session); } }
Java
복사
ChatMessage 생성
package org.example.backendproject.purewebsocket.dto; import lombok.Getter; @Getter public class ChatMessage { private String message; private String from; }
Java
복사
WebSocketConfig 생성
package org.example.backendproject.purewebsocket.config; import org.example.backendproject.purewebsocket.handler.ChatWebSocketHandler; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.config.annotation.WebSocketConfigurer; import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; @Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(new ChatWebSocketHandler(), "/ws-chat") .setAllowedOriginPatterns("*"); // ws-chat 엔드포인트로 요청을 보낼 수 있는지 결정하는 보안 정책 설정 // * <- 모든 도메인에서 접근 가능 } }
Java
복사
HtmlController 생성
package org.example.backendproject.purewebsocket; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; @Controller public class HtmlController { @GetMapping("/") public String index() { return "redirect:/purechat1.html"; } }
Java
복사
static 폴더에 purechat1.html 추가
<!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8"> <title>Pure WebSocket Chat</title> <style> body { font-family: 'Segoe UI', sans-serif; background: #f7f8fa; } .container { width: 400px; margin: 60px auto; background: #fff; padding: 32px 30px; border-radius: 14px; box-shadow: 0 2px 8px rgba(0,0,0,0.07); } h2 { text-align: center; color: #2c3e50; margin-bottom: 24px; } #chatArea { width: 100%; height: 250px; border: 1px solid #aaa; border-radius: 8px; margin-bottom: 18px; overflow-y: auto; background: #fafdff; padding: 10px 7px; font-size: 15px; } .row { display: flex; gap: 10px; align-items: center; margin-bottom: 13px; } input[type="text"] { box-sizing: border-box; border: 1px solid #ccc; border-radius: 6px; font-size: 15px; padding: 9px; outline: none; background: #f9fafd; transition: border 0.2s; } input[type="text"]:focus { border-color: #4078c0; background: #fff; } #user { width: 110px; } #msg { flex: 1; min-width: 0; } button { background: #4078c0; color: white; font-weight: bold; border: none; border-radius: 6px; padding: 10px 20px; font-size: 15px; cursor: pointer; transition: background 0.2s; } button:hover { background: #285690; } .btn-disconnect { background: #eee; color: #285690; font-weight: bold; } .btn-disconnect:hover { background: #e0e8f5; } .sysmsg { color: #666; font-style: italic; margin: 7px 0 3px 0;} .msgrow { margin-bottom: 3px;} .from { font-weight: bold; color: #4078c0;} .hidden { display: none; } </style> </head> <body> <div class="container"> <h2>Pure WebSocket Chat</h2> <!-- 로그인 영역 --> <div class="row" style="margin-bottom: 15px;"> <input type="text" id="user" placeholder="닉네임"> <button onclick="connect()">Connect</button> <button class="btn-disconnect" onclick="disconnect()">Disconnect</button> </div> <!-- 채팅 영역 (처음엔 숨김) --> <div id="chatWrapper" class="hidden"> <div id="chatArea"></div> <div class="row"> <input type="text" id="msg" placeholder="메시지" onkeydown="if(event.key==='Enter'){sendMessage();}"> <button onclick="sendMessage()">Send</button> </div> </div> </div> <script> let ws = null; function connect() { const user = document.getElementById("user").value; if (!user) { alert("닉네임을 입력하세요!"); return; } ws = new WebSocket("/ws-chat"); ws.onopen = function () { showSysMsg('Connected!'); document.getElementById("chatWrapper").classList.remove("hidden"); }; ws.onmessage = function (event) { const msg = JSON.parse(event.data); showMessage(msg.from, msg.message); }; ws.onclose = function () { showSysMsg('Disconnected'); document.getElementById("chatWrapper").classList.add("hidden"); }; } function disconnect() { if (ws) { ws.close(); ws = null; } } function sendMessage() { const user = document.getElementById("user").value; const msg = document.getElementById("msg").value; if (!user || !msg) { alert("닉네임과 메시지를 모두 입력하세요!"); return; } ws.send(JSON.stringify({ from: user, message: msg })); document.getElementById("msg").value = ""; } function showMessage(from, message) { const chatArea = document.getElementById("chatArea"); chatArea.innerHTML += `<div class="msgrow"><span class="from">${from}:</span> ${message}</div>`; chatArea.scrollTop = chatArea.scrollHeight; } function showSysMsg(msg) { const chatArea = document.getElementById("chatArea"); chatArea.innerHTML += `<div class="sysmsg">${msg}</div>`; chatArea.scrollTop = chatArea.scrollHeight; } </script> </body> </html>
JavaScript
복사
서버 실행 후 localhost:8080에 접속 화면 확인
닉네임 입력 후 Connect 클릭
Postman에서 다음과 같이 입력 후 연결 시도
연결 확인
다음과 같이 입력 후 Send 버튼 클릭
메세지 내용 확인 + 헤더 내용 확인
entity → repository → service → controller 순서로 room개발 진행

순수 WebSocket 채팅 서버 실습 - room 생성

entity 생성 (room > entity)
package org.example.backendproject.purewebsocket.room.entity; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; import lombok.Getter; import lombok.Setter; @Getter @Setter @Entity @Table(name = "chat_room") public class ChatRoom { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(unique = true) private String roomId; }
Java
복사
repository 생성
package org.example.backendproject.purewebsocket.room.repository; import java.util.Optional; import org.example.backendproject.purewebsocket.room.entity.ChatRoom; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface RoomRepository extends JpaRepository<ChatRoom, Long> { Optional<ChatRoom> findByRoomId(String roomId); }
Java
복사
service 생성
package org.example.backendproject.purewebsocket.room.service; import java.util.List; import lombok.RequiredArgsConstructor; import org.example.backendproject.purewebsocket.room.entity.ChatRoom; import org.example.backendproject.purewebsocket.room.repository.RoomRepository; import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor public class RoomService { private final RoomRepository roomRepository; public ChatRoom createRoom(String roomId) { return roomRepository.findByRoomId(roomId) .orElseGet(() -> { ChatRoom chatRoom = new ChatRoom(); chatRoom.setRoomId(roomId); return roomRepository.save(chatRoom); }); } public List<ChatRoom> findAllRooms() { return roomRepository.findAll(); } }
Java
복사
controller 생성
package org.example.backendproject.purewebsocket.room.controller; import java.util.List; import lombok.RequiredArgsConstructor; import org.example.backendproject.purewebsocket.room.entity.ChatRoom; import org.example.backendproject.purewebsocket.room.service.RoomService; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequiredArgsConstructor @RequestMapping("/api/rooms") public class RoomController { private final RoomService roomService; @GetMapping public List<ChatRoom> getAllRoom() { return roomService.findAllRooms(); } @PostMapping("/{roomId}") public ChatRoom createRoom(@PathVariable String roomId) { return roomService.createRoom(roomId); } }
Java
복사
HtmlController 코드 변경
package org.example.backendproject.purewebsocket; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; @Controller public class HtmlController { @GetMapping("/") public String index() { return "redirect:/purechat2.html"; } }
Java
복사
html 파일 생성
<!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8"> <title>Room-Based WebSocket Chat</title> <style> body { font-family: 'Segoe UI', sans-serif; background: #f7f8fa; } .container { width: 540px; margin: 60px auto; background: #fff; padding: 32px 30px; border-radius: 14px; box-shadow: 0 2px 8px rgba(0,0,0,0.07); } h2 { text-align: center; color: #2c3e50; margin-bottom: 24px; } .row { display: flex; gap: 10px; align-items: center; margin-bottom: 13px; } input[type="text"] { box-sizing: border-box; border: 1px solid #ccc; border-radius: 6px; font-size: 15px; padding: 9px; outline: none; background: #f9fafd; transition: border 0.2s; width: 100%; max-width: 130px; } input[type="text"]:focus { border-color: #4078c0; background: #fff; } button { background: #4078c0; color: white; font-weight: bold; border: none; border-radius: 6px; padding: 10px 20px; font-size: 15px; cursor: pointer; transition: background 0.2s; } button:hover { background: #285690; } .btn-disconnect { background: #eee; color: #285690; font-weight: bold; } .btn-disconnect:hover { background: #e0e8f5; } #chatArea { width: 100%; height: 250px; border: 1px solid #aaa; border-radius: 8px; margin-bottom: 18px; overflow-y: auto; background: #fafdff; padding: 10px 7px; font-size: 15px; } .sysmsg { color: #666; font-style: italic; margin: 7px 0 3px 0; } .msgrow { margin-bottom: 3px; } .from { font-weight: bold; color: #4078c0; } .hidden { display: none; } #roomList { margin-bottom: 20px; } .room-item { padding: 6px 10px; border: 1px solid #ccc; border-radius: 6px; margin-bottom: 5px; cursor: pointer; } .room-item:hover { background: #e8f0ff; } </style> </head> <body> <div class="container"> <h2>Pure WebSocket Chat Room ADD</h2> <!-- 방 목록 표시 --> <div id="roomList"> <strong>방 목록:</strong> <div id="rooms"></div> </div> <!-- 닉네임 입력 및 disconnect --> <div class="row" id="nicknameRow"> <!-- <input type="text" id="user" placeholder="닉네임">--> <input type="text" id="room" placeholder="방 번호"> <button onclick="manualConnect()">Connect</button> </div> <!-- 채팅 영역 (처음엔 숨김) --> <div id="chatWrapper" class="hidden"> <div id="chatArea"></div> <div class="row"> <input type="text" id="msg" placeholder="메시지" onkeydown="if(event.key==='Enter'){sendMessage();}"> <button onclick="sendMessage()">Send</button> <button class="btn-disconnect" onclick="disconnect()">Disconnect</button> </div> </div> </div> <script> let ws = null; let roomId = ""; let nickname = ""; window.onload = function () { loadRoomList(); }; function loadRoomList() { fetch("/api/rooms") .then(response => response.json()) .then(data => { const roomsDiv = document.getElementById("rooms"); const roomListSection = document.getElementById("roomList"); roomsDiv.innerHTML = ""; if (data.length === 0) { roomListSection.classList.add("hidden"); } else { roomListSection.classList.remove("hidden"); data.forEach(room => { const div = document.createElement("div"); div.className = "room-item"; div.textContent = `방 번호: ${room.roomId}`; div.onclick = () => enterRoom(room.roomId); roomsDiv.appendChild(div); }); } }); } function enterRoom(id, inputNick = null) { if (!inputNick) { const nick = prompt("닉네임을 입력하세요:"); if (!nick) { alert("닉네임이 필요합니다."); return; } nickname = nick; } else { nickname = inputNick; } roomId = id; //방목록 데이터베이스에 저장하는 곳 //방목록 데이터베이스에 저장하는 곳 //방목록 데이터베이스에 저장하는 곳 fetch(`/api/rooms/${roomId}`, { method: "POST" }) // 주석 해제 ws = new WebSocket("/ws-chat"); ws.onopen = function () { showSysMsg(`[${nickname}]님이 방 [${roomId}]에 입장했습니다.`); document.getElementById("chatWrapper").classList.remove("hidden"); document.getElementById("roomList").classList.add("hidden"); document.getElementById("nicknameRow").classList.add("hidden"); }; ws.onmessage = function (event) { const msg = JSON.parse(event.data); showMessage(msg.from, msg.message, msg.roomId); }; ws.onclose = function () { showSysMsg("Disconnected"); document.getElementById("chatWrapper").classList.add("hidden"); document.getElementById("roomList").classList.remove("hidden"); document.getElementById("nicknameRow").classList.remove("hidden"); loadRoomList(); }; } function manualConnect() { const inputRoom = document.getElementById("room").value; if (!inputRoom) { alert("방 번호를 입력하세요."); return; } const inputNick = prompt("닉네임을 입력하세요:"); if (!inputNick) { alert("닉네임이 필요합니다."); return; } nickname = inputNick; enterRoom(inputRoom, inputNick); } function disconnect() { if (ws) { ws.close(); ws = null; } } function sendMessage() { const msg = document.getElementById("msg").value; if (!nickname || !msg || !roomId) { alert("모든 정보를 입력해주세요."); return; } ws.send(JSON.stringify({ from: nickname, message: msg, roomId: roomId })); document.getElementById("msg").value = ""; } function showMessage(from, message, room) { const chatArea = document.getElementById("chatArea"); chatArea.innerHTML += `<div class="msgrow"><span class="from">[${room}] ${from}:</span> ${message}</div>`; chatArea.scrollTop = chatArea.scrollHeight; } function showSysMsg(msg) { const chatArea = document.getElementById("chatArea"); chatArea.innerHTML += `<div class="sysmsg">${msg}</div>`; chatArea.scrollTop = chatArea.scrollHeight; } </script> </body> </html>
HTML
복사
결과 확인

Spring WebSocket + STOMP 채팅 서버 (1:1), 단체

ChatMessage 생성
package org.example.backendproject.stompwebsocket.dto; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @Getter @Setter @NoArgsConstructor @AllArgsConstructor public class ChatMessage { private String message; private String from; private String to; // 귓속말을 받을 사람 private String roomId; // 방 id }
Java
복사
WebSocketConfig 생성
package org.example.backendproject.stompwebsocket.config; import org.springframework.context.annotation.Configuration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableSimpleBroker("/topic", "/queue"); // 구독용 경로 서버 -> 클라이언트 registry.setApplicationDestinationPrefixes("/app"); // 전송용 경로 클라이언트 -> 서버 } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/ws-chat") .setAllowedOriginPatterns("*"); } }
Java
복사
ChatController 생성
package org.example.backendproject.stompwebsocket.controller; import org.example.backendproject.stompwebsocket.dto.ChatMessage; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.SendTo; import org.springframework.stereotype.Controller; @Controller public class ChatController { @MessageMapping("/chat.sendMessage") @SendTo("/topic/public") public ChatMessage sendMessage(ChatMessage message) { return message; } }
Java
복사
HtmlController 변경 - html 파일 추가
package org.example.backendproject.purewebsocket; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; @Controller public class HtmlController { @GetMapping("/") public String index() { return "redirect:/stompchat2.html"; } }
Java
복사
stormchat2.html
<!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8"> <title>WebSocket STOMP Chat</title> <style> body { font-family: 'Segoe UI', sans-serif; background: #f7f8fa; } .container { width: 400px; margin: 60px auto; background: #fff; padding: 32px 30px; border-radius: 14px; box-shadow: 0 2px 8px rgba(0,0,0,0.07); } h2 { text-align: center; color: #2c3e50; margin-bottom: 20px;} #chatArea { width: 100%; height: 250px; border: 1px solid #aaa; margin-bottom: 18px; overflow-y: auto; padding: 10px 7px; border-radius: 8px; background: #fafdff; font-size: 15px; } .row { display: flex; gap: 10px; align-items: center; margin-bottom: 12px; } input[type="text"] { box-sizing: border-box; border: 1px solid #ccc; border-radius: 6px; font-size: 15px; padding: 9px; outline: none; background: #f9fafd; transition: border 0.2s; } input[type="text"]:focus { border-color: #4078c0; background: #fff; } #user, #room { width: 110px; } #msg { flex: 1; min-width: 0; } button { background: #4078c0; color: white; font-weight: bold; border: none; border-radius: 6px; padding: 10px 20px; font-size: 15px; cursor: pointer; transition: background 0.2s; } button:hover { background: #285690; } .btn-disconnect { background: #eee; color: #285690; font-weight: bold; } .btn-disconnect:hover { background: #e0e8f5; } .sysmsg { color: #666; font-style: italic; margin: 7px 0 3px 0;} .msgrow { margin-bottom: 3px;} .from { font-weight: bold; color: #4078c0;} .hidden { display: none; } #roomList { margin-bottom: 20px; } .room-item { padding: 6px 10px; border: 1px solid #ccc; border-radius: 6px; margin-bottom: 5px; cursor: pointer; } .room-item:hover { background: #e8f0ff; } </style> </head> <body> <div class="container"> <h2>Spring WebSocket + STOMP Chat</h2> <!-- 방 목록 --> <div id="roomList"> <strong>방 목록:</strong> <div id="rooms"></div> </div> <!-- 입장 영역 --> <div class="row" id="enterRow"> <input type="text" id="user" placeholder="닉네임"> <input type="text" id="room" placeholder="방 번호"> <button onclick="connect()">Connect</button> </div> <!-- 귓속말 대상 --> <div class="row hidden" id="whisperRow"> <input type="text" id="whisperTo" placeholder="귓속말 대상 (닉네임)"> </div> <!-- 채팅 영역 (입장 후에만 표시) --> <div id="chatWrapper" class="hidden"> <div id="chatArea"></div> <div class="row"> <input type="text" id="msg" placeholder="메시지"> <button onclick="sendMessage()">Send</button> <button class="btn-disconnect" onclick="disconnect()">Disconnect</button> </div> </div> </div> <script src="https://cdn.jsdelivr.net/npm/stompjs@2.3.3/lib/stomp.min.js"></script> <script> let stompClient = null; let nickname = ""; let roomId = ""; window.onload = function () { loadRoomList(); }; function loadRoomList() { fetch("/api/rooms") .then(response => response.json()) .then(data => { const roomsDiv = document.getElementById("rooms"); const roomListDiv = document.getElementById("roomList"); roomsDiv.innerHTML = ""; if (data.length === 0) { roomListDiv.classList.add("hidden"); } else { roomListDiv.classList.remove("hidden"); data.forEach(room => { const div = document.createElement("div"); div.className = "room-item"; div.textContent = `방 번호: ${room.roomId}`; div.onclick = () => { const nick = prompt("닉네임을 입력하세요:"); if (!nick) return; document.getElementById("user").value = nick; document.getElementById("room").value = room.roomId; connect(); }; roomsDiv.appendChild(div); }); } }); } function connect() { nickname = document.getElementById("user").value; roomId = document.getElementById("room").value; if (!nickname || !roomId) { alert("닉네임과 방 번호를 입력하세요!"); return; } //방생성기능 //방생성기능 fetch(`/api/rooms/${roomId}`, { method: "POST" }) // const socket = new WebSocket('/ws-chat'); const socket = new WebSocket('/ws-chat?nickname=' + nickname); stompClient = Stomp.over(socket); stompClient.connect({}, function (frame) { showSysMsg(`[${nickname}]님이 방 [${roomId}]에 입장했습니다.`); document.getElementById("chatWrapper").classList.remove("hidden"); document.getElementById("enterRow").classList.add("hidden"); document.getElementById("roomList").classList.add("hidden"); // document.getElementById("whisperRow").classList.remove("hidden"); // ✅ 여기! 닉네임 작성 stompClient.subscribe(`/topic/${roomId}`, function (chat) { const message = JSON.parse(chat.body); showMessage(message.from, message.message); }); //귓속말 stompClient.subscribe('/user/queue/private', function (message) { const msg = JSON.parse(message.body); alert("💬 귓속말: " + msg.from + " - " + msg.message); }); }); } function disconnect() { if (stompClient !== null) { stompClient.disconnect(); } showSysMsg('Disconnected'); document.getElementById("chatWrapper").classList.add("hidden"); document.getElementById("enterRow").classList.remove("hidden"); document.getElementById("roomList").classList.remove("hidden"); document.getElementById("whisperRow").classList.add("hidden"); // ✅ 추가 loadRoomList(); } function sendMessage() { const msg = document.getElementById("msg").value; const toUser = document.getElementById("whisperTo").value.trim(); if (!nickname || !msg || !roomId) { alert("모든 정보를 입력해주세요!"); return; } const payload = { from: nickname, message: msg, roomId: roomId }; if (toUser) { payload.to = toUser; // 귓속말 대상이 있으면 to 추가 } stompClient.send("/app/chat.sendMessage", {}, JSON.stringify(payload)); document.getElementById("msg").value = ""; } function showMessage(from, message) { const chatArea = document.getElementById("chatArea"); chatArea.innerHTML += `<div class="msgrow"><span class="from">${from}:</span> ${message}</div>`; chatArea.scrollTop = chatArea.scrollHeight; } function showSysMsg(msg) { const chatArea = document.getElementById("chatArea"); chatArea.innerHTML += `<div class="sysmsg">${msg}</div>`; chatArea.scrollTop = chatArea.scrollHeight; } document.getElementById('msg').addEventListener('keydown', function(e) { if (e.key === 'Enter') sendMessage(); }); </script> </body> </html>
JavaScript
복사
결과 확인
⇒ 기존에 WebSocket에서 만들었던 방들도 같이 확인 가능
네임, 방 번호 입력 후 Connect 버튼 클릭
메시지 입력 후 Send 버튼 클릭 - 메시지 내용 확인

귓속말 기능 적용

ChatController 수정
package org.example.backendproject.stompwebsocket.controller; import lombok.RequiredArgsConstructor; import org.example.backendproject.stompwebsocket.dto.ChatMessage; import org.springframework.beans.factory.annotation.Value; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.SendTo; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.stereotype.Controller; @Controller @RequiredArgsConstructor public class ChatController { // 단일 브로드캐스트 (동적으로 방 생성이 안 됨) // @MessageMapping("/chat.sendMessage") // @SendTo("/topic/public") // public ChatMessage sendMessage(ChatMessage message) { // return message; // } @Value("${PROJECT_NAME:web Server}") private String instanceName; // 서버가 클라이언트에게 수동으로 메세지를 보낼 수 있도록 하는 클래스 private final SimpMessagingTemplate template; // 동적으로 방 생성 가능 @MessageMapping("/chat.sendMessage") public void sendMessage(ChatMessage message) { message.setMessage(instanceName + " " + message.getMessage()); if (message.getTo() != null && !message.getTo().isEmpty()) { // 귓속말 // 내 아이디로 귓속말 경로를 활성화 함 template.convertAndSendToUser(message.getTo(), "/queue/private", message); } else { // 일반 메시지 // message에서 roomId를 추출해서 해당 roomId를 구독하고 있는 클라이언트에게 메세지를 전달 template.convertAndSend("/topic/" + message.getRoomId(), message); } } }
Java
복사
WebSocketConfig 수정
package org.example.backendproject.stompwebsocket.config; import org.example.backendproject.stompwebsocket.handler.CustomHandshakeHandler; import org.springframework.context.annotation.Configuration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistry registry) { /** * 구독중 Prefix * /topic 일반 채팅을 받을 접두어 * /queue 귓속말을 받을 접두어 * 서버가 보내는 메시지를 클라이언트가 구독할 때 사용하는 경로 */ registry.enableSimpleBroker("/topic", "/queue"); // 구독용 경로 /** 전송용 Prefix **/ // 클라이언트가 서버에 메시지를 보낼 때 사용하는 경로 접두어 registry.setApplicationDestinationPrefixes("/app"); // 클라이언트 -> 서버 // 서버가 특정 사용자에게 메시지를 보낼 때, 클라이언트가 구독할 경로 접두어 registry.setUserDestinationPrefix("/user"); // 서버 -> 특정 사용자 } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/ws-chat") .setHandshakeHandler(new CustomHandshakeHandler()) .setAllowedOriginPatterns("*"); } }
Java
복사
handler 패키지 생성 후 StompPrincipal 생성
package org.example.backendproject.stompwebsocket.handler; import java.security.Principal; public class StompPrincipal implements Principal { private final String name; public StompPrincipal(String name) { this.name = name; } @Override public String getName() { return name; } }
Java
복사
CustomHandshakeHandler 생성
package org.example.backendproject.stompwebsocket.handler; import java.security.Principal; import java.util.Map; import org.springframework.http.server.ServerHttpRequest; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.server.support.DefaultHandshakeHandler; public class CustomHandshakeHandler extends DefaultHandshakeHandler { @Override protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, Map<String, Object> attributes) { String nickname = getNickName(request.getURI().getQuery()); return new StompPrincipal(nickname); } private String getNickName(String query) { if (query == null || !query.contains("nickname=")) { return "닉네임없음"; } else { return query.split("nicknames=")[1]; } } }
Java
복사
서로 다른 브라우저로 통신 여부 확인

Docker 이미지 생성 + 컨테이너 생성 및 시작

이미지 생성 (Dockerfile 위치한 경로에서 실행)
docker build -t backend .
Shell
복사
컨테이너 생성 및 시작 (docker-compose.yaml 위치한 경로에서 실행)
docker compose up -d
Shell
복사

7. 취업 특강

지원하고 싶은 회사의 채용 페이지 확인
인재상, 조직문화가 핵심
내가 어떤 개발을 했을 때 가장 만족을 느끼는지에 대해 고민을 할 필요가 있음
산업군, 직무의 선택이 중요!! (자기소개서 작성)
어떤 산업군으로 가야 할지 고민을 할 필요가 있음 - 왜 그 산업군인가?
왜 그 회사인가? 왜 그 경쟁사인가?
왜 그 진로를 선택했는가? (왜 그 직무?)
왜 다른 지원자가 아니라 나를 선택해야 하는 이유에 대해 분명하게 말할 수 있어야 한다.
나만의 강점!!
ex) 게임 중의 특정 카테고리… - 캐시 카우 RPG 게임
자기소개서에 나는 xxx한 사람입니다 라는 문장을 쓰는 건 자충수를 두는 것이다.
그 회사에 들어가기 위한 목적의식이 있어야 한다.
검색했을 때 정보가 많이 안 나오는 회사이면 면접 찾기가 쉽지 않다.
단점 설명
전 호기심이 너무 많은 사람이다.