실수로 삭제한 글 다시 써봅니다... (수정, 삭제 버튼 잘 구분해서 클릭해야겠어요...)
여행 관련 서비스르 개발하면서 WebSocket으로 채팅을 구현하면서 WebSocket에 대해서 공부할 겸 글을 써보려고 합니다.
- WebSocket만으로 채팅 구현
- STOMP를 활용한 채팅 구현
- Redis를 활용한 채팅 구현
- 채팅 고도화
이번 포스트는 WebSocket으로만 채팅을 구현하려고 합니다.
WebSocket이란?
개념
WebSocket은 클라이언트와 서버를 연결하고 실시간으로 통신이 가능하도록 하는 프로토콜이다.
웹소켓은 하나의 TCP 접속에 전이중 통신 채널을 제공한다.
쉽게 보면, WebSocket은 Socket Connection을 유지한 채로 실시간으로 양방향 통신 혹은 데이터 전송이 가능한 프로토콜이다.
또, WebSocket은 서버와 클라이언트 간에 Socket Connection을 유지해서 언제든 양방향 통신 또는 데이터 전송이 가능하도록 하는 기술이다.
Real-time web application 구현을 위해 널리 사용되어지고 있다.
작동원리
서버와 클라이언트 간의 웹소켓 연결을 HTTP 프로토콜을 통해 이루어진다.
연결이 정상적으로 이루어졌을 때, 서버와 클라이언트 간에 웹소켓 연결(TCP/IP 기반)이 이루어지고 일정 시간이 지나면 HTTP 연결은 자동으로 끊어진다.
기본적으로 웹소켓 API는 아주 간단한 기능들만으로 제공하기 때문에 대부분의 경우 SockJS나 Socket.IO와 같은 오픈 소스 라이브러리를 많이 사용하고 있으며 메시지 포맷 또한 STOMP 같은 프로토콜을 같이 이용한다.
WebSocket 사용 예시
- LOL과 같은 멀티플레이어 게임
- 위치 기반 APP
- 증권 거래 정보 사이트
- ...
프로젝트
구성
- Springboot 3.3.1
- java 17
- Gradle Dependencies
- web
- websocket
- lombok
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-websocket'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
WebSocketHandler
socket통신은 서버와 클라이언트가 1:N으로 관계를 맺는다.
따라서, 한 서버와 여러 클라이언트가 접속할 수 있으며, 서버에는 여러 클라이언트가 발송한 메시지를 받아 처리해줄 Handler의 작성이 필요하다.
@Slf4j
@Component
@RequiredArgsConstructor
public class WebSocketChatHandler extends TextWebSocketHandler {
private final ObjectMapper objectMapper;
private final ChatService chatService;
// 웹소켓 클라이언트로부터 채팅 메시지를 전달받아 채팅 메시지 객체로 변환
// 전달받은 메시지에 담긴 채팅방 id로 발송 대상 채팅방 정보를 조회
// 해당 채팅방에 입장해있는 모든 클라이언트들에게 타입에 따른 메시지 발송
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String payload = message.getPayload();
log.info("payload: {}", payload);
ChatMessage chatMessage = objectMapper.readValue(payload, ChatMessage.class);
ChatRoom room = chatService.findRoomById(chatMessage.getRoomId());
room.handleActions(session, chatMessage, chatService);
}
}
WebSocketConfig
handler를 이용하여 WebSocket을 활성화하기 위한 Config 파일을 작성한다.
@EnableWebSocket을 선언하여 WebSocket을 활성화한다.
WebSocket에 접속하기 위한 endpoint는 /ws/chat으로 설정하고, 도메인이 다른 서버에서도 접속 가능하도록 CORS(setAllowedOrigin("*"))설정을 추가한다.
클라이언트가 ws://localhost:8080/ws/chat으로 커넥션을 연결하고 메시지 통신을 할 수 있는 기본적인 준비가 끝났다.
@Configuration
@RequiredArgsConstructor
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
private final WebSocketHandler webSocketHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(webSocketHandler, "/ws/chat").setAllowedOrigins("*");
}
}
ChatMessage
댜음은 채팅 메시지를 위한 Dto이다.
상황에 따라 채팅방 입장, 채팅 발송 두가지에 대한 enum을 선언한다.
나머지는 채팅방 Id, 메시지 보낸 사람, 메시지로 구성한다.
@Getter
@Setter
public class ChatMessage {
// 채팅 타입 (채팅방 입장, 채팅 메시지 전송)
public enum MessageType {
ENTER, TALK
}
private MessageType type;
private String roomId;
private String sender;
private String message;
}
ChatRoom
다음은 채팅방 Dto 이다.
채팅방은 입장한 클라이언트들의 정보를 담아야 하므로 WebSocketSession 정보 리시트를 멤버 리스트로 갖는다.
나머지는 채팅방 id, 채팅방 이름으로 구성한다.
@Getter
public class ChatRoom {
private String roomId;
private String name;
private Set<WebSocketSession> sessions = new HashSet<>();
@Builder
public ChatRoom(String roomId, String name) {
this.roomId = roomId;
this.name = name;
}
// 채팅방 입장, 채팅에 대한 분기 처리
// 입장 시에는 session 정보에 클라이언트 session 추가
// 입장 후 채팅 시에는 채팅방의 모든 session에 메시지 발송
public void handleActions(WebSocketSession session, ChatMessage chatMessage, ChatService chatService) {
if (chatMessage.getType().equals(ChatMessage.MessageType.ENTER)) {
sessions.add(session);
chatMessage.setMessage(chatMessage.getSender() + "님이 입장했습니다.");
}
sendMessage(chatMessage, chatService);
}
public <T> void sendMessage(T message, ChatService chatService) {
sessions.parallelStream().forEach(session -> chatService.sendMessage(session, message));
}
}
ChatService
채팅방 Map을 통해 채팅방 정보를 모아둔다.
채팅방에 대한 정보 저장은 단순하게 Map을 통해 저장하도록 구현하였다.
@Service
@RequiredArgsConstructor
@Slf4j
public class ChatService {
private final ObjectMapper objectMapper;
private Map<String, ChatRoom> chatRooms;
@PostConstruct
private void init() {
chatRooms = new LinkedHashMap<>();
}
// 채팅방 map에 저장된 모든 채팅방 조회
public List<ChatRoom> findAllRoom() {
return new ArrayList<>(chatRooms.values());
}
// 채팅방 map에 저장된 특정 채팅방 조회
public ChatRoom findRoomById(String roomId) {
return chatRooms.get(roomId);
}
// 채팅방 생성 후 채팅방 map에 저장
public ChatRoom createRoom(String name) {
String randomId = UUID.randomUUID().toString();
ChatRoom chatRoom = ChatRoom.builder()
.roomId(randomId) // 채팅방 구별을 위한 UUID
.name(name)
.build();
chatRooms.put(randomId, chatRoom);
return chatRoom;
}
// 지정한 WebSocket 세션에 메시지 발송
public <T> void sendMessage(WebSocketSession session, T Message) {
try {
session.sendMessage(new TextMessage(objectMapper.writeValueAsString(Message)));
} catch (IOException e) {
log.error(e.getMessage(), e);
}
}
}
ChatController
RestAPI를 사용하므로 다음과 같이 controller를 생성한다.
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/chat")
public class ChatController {
private final ChatService chatService;
@PostMapping("/rooms")
public ChatRoom createRoom(@RequestParam String name) {
return chatService.createRoom(name);
}
@GetMapping("/rooms")
public List<ChatRoom> findAllRoom() {
return chatService.findAllRoom();
}
}
테스트
(잘못된 부분이나 누락된 부분이 있다면 댓글 달아주시면 감사하겠습니다.)
참고
https://www.daddyprogrammer.org/post/4077/spring-websocket-chatting/
Spring websocket chatting server(1) - basic websocket server
Spring에서 제공하는 Websocket을 이용하여 간단한 채팅 서버를 구현해 보도록 하겠습니다. 일반적인 http통신을 하는 서버들과 달리 채팅 서버는 socket통신을 하는 서버가 필요합니다. 통상적으로 htt
www.daddyprogrammer.org
[웹소켓] WebSocket의 개념 및 사용이유, 작동원리, 문제점
오늘은 웹소켓에 대해 알아보겠습니다. ● 웹소켓(WebSocket)의 배경 : 인터넷이 나오고 HTTP를 통해서 서버로부터 데이터를 가져오기 위해서는 오로지 URL을 통한 요청이 유일한 방법이었습니다. 때
nanbuja.com
'Series' 카테고리의 다른 글
[WebSocket] 채팅 2 - STOMP를 활용해서 채팅 구현하기 (0) | 2024.07.22 |
---|