Spring/개인공부_실습
[Spring]WebSocket과 STOMP을 이용한 실시간 채팅 및 채팅방 생성
빙응이
2024. 10. 11. 17:11
📝STOMP???
STOMP는 WebSocket 상에서 메시지를 전송하기 위한 프로토콜이다.
실시간 채팅, 알림, 주식 거래 등 실시간 통신이 필요한 앱에서 활용된다.
📌간단한 동작 로직
- STOMP는 간단하게 5개의 단계로 나눌 수 있다.
1. 연결
- 클라이언트가 서버에 WebSocket 연결을 요청한다.
2. 구독
- 클라이언트는 STOMP를 통해 특정 채팅 방(예: /chat/{roomId})을 구독한다.
3. 메시지 전송
- 클라이언트가 방에 메시지를 전송하면, 서버는 해당 방에 속한 모든 사용자에게 메시지를 브로드캐스트한다.
4. 메시지 수신
- 구독한 클라이언트가 서버로부터 실시간 메시지를 수신하고 화면에 표시한다.
5. 연결 해제
- 채팅이 끝나면 클라이언트는 WebSocket 연결을 해제하여 세션을 종료한다.
이러한 과정을 통해 STOMP는 데이터베이스 기반으로 채팅방의 참여자를 유지하고 채팅방에 자유 참여가 가능하다.
로직적으로 들어갈때 구독을 바꾸는 형식으로 할 수 있다,
📝예시 프로젝트 만들어보기
프로젝트 구조
- Backend: Spring Boot, JPA, STOMP (WebSocket), Lombok, MariaDB
- Frontend: Thymeleaf (HTML 템플릿), JavaScript (STOMP 클라이언트)
📌 의존성 설정
dependencies {
// Spring WebSocket & STOMP
implementation 'org.springframework.boot:spring-boot-starter-websocket'
// Spring JPA & H2 Database (or MySQL/Postgres if needed)
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.h2database:h2'
// Thymeleaf
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
// Lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
}
📌 WebSocket , STOMP 설정하기
- WebSocketConfig 설정
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/chat"); // 메시지를 구독할 경로 설정
config.setApplicationDestinationPrefixes("/app"); // 클라이언트에서 메시지 보낼 경로
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws").withSockJS(); // WebSocket 엔드포인트 설정
}
}
하나하나씩 알아보자
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/chat");
config.setApplicationDestinationPrefixes("/app");
}
- 해당 부부은 STOMP 메시지 브로커의 설정을 담당한다.
- enableSimpleBroker("/chat")
- SimpleBroker는 서버에서 클라이언트로 메시지를 브로드캐스트할 경로를 설정한다.
- 말 그대로 구독이다.
- setApplicationDestinationPrefixes("/app")
- 클라이언트가 서버로 메시지를 보낼 때 사용하는 경로의 prefix(접두사)를 설정한다.
- 간단히 말하면 메시지를 보낼 경로이다.
- enableSimpleBroker("/chat")
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws").withSockJS();
}
- 해당 부분은 WebSocket 연결 엔드포인트 설정이다.
- addEndpoint("/ws")
- 클라이언트는 이 /ws 엔드포인트를 통해 WebSocket 연결을 시작한다.
- 테스트 환경에서는 http://localhost:8080/ws로 연결을 시도하면 된다.
- withSockJS()
- SockJS는 WebSocket을 사용할 수 없는 브라우저에서 대체 통신 방식(예: HTTP 폴링, XHR 등)을 제공하는 라이브러리이다.
- 중요한 것은 WebSocket이 지원되지 않는 환경에서도 클라이언트가 SockJS를 사용하여 통신할 수 있도록 한다.
- addEndpoint("/ws")
📌 채팅방, 채팅 메시지 Entity 설정하기
채팅방 ID와 방 이름을 저장하는 채팅방 엔티티
@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ChatRoom {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; // 채팅방 ID
private String roomId; // STOMP에서 사용할 채팅방 식별자
private String name; // 채팅방 이름
public static ChatRoom create(String name) {
return ChatRoom.builder()
.roomId(UUID.randomUUID().toString()) // 랜덤 ID 생성
.name(name)
.build();
}
}
각 채팅방에 대한 메시지를 저장하는 엔티티
@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ChatMessage {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; // 메시지 ID
@ManyToOne
@JoinColumn(name = "chat_room_id")
private ChatRoom chatRoom; // 어떤 채팅방의 메시지인지
private String sender; // 메시지 보낸 사람
private String message; // 메시지 내용
private LocalDateTime timestamp; // 메시지 전송 시간
public static ChatMessage create(ChatRoom chatRoom, String sender, String message) {
return ChatMessage.builder()
.chatRoom(chatRoom)
.sender(sender)
.message(message)
.timestamp(LocalDateTime.now())
.build();
}
}
서비스 및 리포지토리는 db에 저장하는 방식 그대로 이기에 적지 않겠습니다.
📝 요청 컨트롤러
@Controller
@RequiredArgsConstructor
public class ChatController {
private final ChatRoomService chatRoomService;
private final ChatMessageService chatMessageService;
@MessageMapping("/chat/sendMessage/{roomId}")
@SendTo("/chat/{roomId}")
public ChatMessage sendMessage(@DestinationVariable String roomId, ChatMessageRequest request) {
ChatRoom chatRoom = chatRoomService.findRoomById(roomId);
return chatMessageService.saveMessage(chatRoom, request.sender(), request.message());
}
}
- 해당 컨트롤러는 STOMP 기반의 실시간 채팅 기능을 제공하는 컨트롤러이다.
- @MessageMapping
- STOMP 메시지를 처리하는 메서드임을 나타내며, 클라이언트가 해당 경로로 전송한 메시지를 이 메서드가 처리한다.
- @SendTo
- 해당 메서드는 반환한 메시지를 구독 중인 클라이언트에게 보낼 경로를 설정한다.
- 즉. /chat/{roomId}를 통해 구독한 사용자 모두 메시지를 받는다.
- @MessageMapping
📝테스트 HTML
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>채팅 애플리케이션</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.5.0/sockjs.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
<style>
body { font-family: Arial, sans-serif; }
#messages { border: 1px solid #ccc; height: 300px; overflow-y: scroll; margin-bottom: 10px; padding: 5px; }
#messageInput { width: 80%; }
</style>
</head>
<body>
<h1>채팅 애플리케이션</h1>
<div>
<label for="roomId">방 ID:</label>
<input type="text" id="roomId" placeholder="방 ID 입력">
<button id="joinBtn">입장</button>
</div>
<div id="messages"></div>
<input type="text" id="messageInput" placeholder="메시지를 입력하세요">
<button id="sendBtn">전송</button>
<script>
let stompClient = null;
let roomId = '';
function connect() {
const socket = new SockJS('/ws'); // WebSocket 엔드포인트
stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
console.log('연결됨: ' + frame);
stompClient.subscribe('/chat/' + roomId, function (message) {
showMessage(JSON.parse(message.body));
});
});
}
function joinRoom() {
roomId = document.getElementById('roomId').value;
connect();
document.getElementById('messages').innerHTML = ''; // 메시지 초기화
}
function sendMessage() {
const messageInput = document.getElementById('messageInput');
const message = {
sender: '사용자',
message: messageInput.value
};
stompClient.send('/chat/sendMessage/' + roomId, {}, JSON.stringify(message));
messageInput.value = ''; // 입력창 비우기
}
function showMessage(message) {
const messagesDiv = document.getElementById('messages');
messagesDiv.innerHTML += '<div><strong>' + message.sender + ':</strong> ' + message.message + '</div>';
messagesDiv.scrollTop = messagesDiv.scrollHeight; // 스크롤을 맨 아래로
}
document.getElementById('joinBtn').onclick = joinRoom;
document.getElementById('sendBtn').onclick = sendMessage;
</script>
</body>
</html>
my_Lab/WebSocket at main · quddaz/my_Lab (github.com)
my_Lab/WebSocket at main · quddaz/my_Lab
✔ 실습을 통한 샘플 코드 만들기. Contribute to quddaz/my_Lab development by creating an account on GitHub.
github.com
자세한 코드는 해당 리포지토리에서 확인 가능합니다.