이번 포스팅은 WebSocket
을 이용하여 간단한 채팅 실습을 진행하려고 합니다.
daddyprogrammer님의 WebSocket 채팅 서버 구현 시리즈의 내용을 보면서 클론 코딩하며 정리한 내용들입니다.
WebSocket
에 대해서 간단하게 공부하고 싶으신 분은 아래 링크를 참고하시면 됩니다.
웹소켓은 기존의 단방향 HTTP
프로토콜과 호환되어 양방향 통신을 제공하기 위해 개발된 프로토콜입니다.
일반 Socket
통신과 달리 HTTP 80 Port
를 이용하므로 방화벽에 제약이 없으며 접속까지는 HTTP
프로토콜을 이용하고 그 이후의 통신은 자체적인 WebSocket
프로토콜로 통신을 하게 됩니다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-websocket'
}
Socket
통신은 서버와 클라이언트가 1:N 관계가 됩니다. 따라서 한 서버에 여러 클라이언트가 접속할 수 있고, 서버에는 여러 클라이언트가 발송한 메시지를 받아 처리해줄 Handler
가 필요하게 됩니다.
TextWebSocketHandler
를 상속받아 아래와 같이 Handler
를 작성해줄 수 있습니다.
package com.websocket.chat.handler;
// import 생략....
@Slf4j
@Component
public class WebSocketChatHandler extends TextWebSocketHandler {
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String payload = message.getPayload();
log.info("payload {}", payload);
TextMessage textMessage = new TextMessage("Welcome chatting sever~^^");
session.sendMessage(textMessage);
}
}
WebSocketChatHandler
을 이용하여 WebSocket
을 활성화하기 위한 Config 작성이 필요합니다.
@EnableWebSocket
를 선언하여 WebSocket
을 활성화해줘야합니다. WebSocket
에 접속하기 위한 endpoint
를 설정하고 도메인이 다른 서버에서도 접속이 가능하도록 CORS 설정을 해줘야 합니다.
이후 클라이언트는 ws://localhost:8080/ws/chat
으로 커넥션을 연결하고 메시지 통신을 한다
@RequiredArgsConstructor
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
private final WebSocketHandler webSocketHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(webSocketHandler, "/ws/chat").setAllowedOrigins("*");
}
}
클라이언트들은 서버에 접속하면 개별의 WebSocket Sessions
을 가지게 됩니다.
채팅방에 입장 시 클라이언트들의 WebSocket Session
정보를 채팅방에 맵핑시켜서 보관하고 있으면 서버에 전달된 메시지를 특정방의 WebSocket
세션으로 보낼 수 있습니다.
@Getter
@Setter
public class ChatMessage {
// 메시지 타입 : 입장, 채팅
public enum MessageType {
ENTER, TALK
}
private MessageType type; // 메시지 타입
private String roomId; // 방번호
private String sender; // 메시지 보낸사람
private String message; // 메시지
}
채팅 메시지는 입장, 대화하기 상태를 가지는 MessageType
과 채팅방 구별 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;
}
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));
}
}
채팅방은 입장한 클라이언트들의 정보를 가지고 있어야 하므로 WebSocket Session
정보 리스트를 멤버 필드로 가지게 됩니다.
채팅방 id, 채팅방 이름 정보를 가지고 있고 채팅방에서 입장, 대화하기 기능을 handleAction
메서드를 통해 분기로 처리하고 있습니다.
입장 시에는 채팅방의 session
정보에 클라이언트의 session
을 추가하고 채팅방에 메시지가 도착할 경우 채팅방의 모든 session
에 메시지를 발송하게 됩니다.
@Slf4j
@RequiredArgsConstructor
@Service
public class ChatService {
private final ObjectMapper objectMapper;
private Map<String, ChatRoom> chatRooms;
@PostConstruct
private void init() {
chatRooms = new LinkedHashMap<>();
}
public List<ChatRoom> findAllRoom() {
return new ArrayList<>(chatRooms.values());
}
public ChatRoom findRoomById(String roomId) {
return chatRooms.get(roomId);
}
public ChatRoom createRoom(String name) {
String randomId = UUID.randomUUID().toString();
ChatRoom chatRoom = ChatRoom.builder()
.roomId(randomId)
.name(name)
.build();
chatRooms.put(randomId, chatRoom);
return chatRoom;
}
public <T> void sendMessage(WebSocketSession session, T message) {
try {
// ChatMessage 객체를 직렬화한 값으로 TextMessage 생성
session.sendMessage(new TextMessage(objectMapper.writeValueAsString(message)));
} catch (IOException e) {
log.error(e.getMessage(), e);
}
}
}
채팅 서비스는 채팅방을 생성, 조회하고 하나의 세션에 메시지를 발송하도록 구현하였습니다.
채팅방 리스트를 Map에 저장하여 서버에 생성된 모든 채팅방의 정보를 저장하게 됩니다.
@RequiredArgsConstructor
@RestController
@RequestMapping("/chat")
public class ChatController {
private final ChatService chatService;
@PostMapping
public ChatRoom createRoom(@RequestParam String name) {
return chatService.createRoom(name);
}
@GetMapping
public List<ChatRoom> findAllRoom() {
return chatService.findAllRoom();
}
}
컨트롤러에서 채팅방을 생성할 수 있고, 현재 존재하는 채팅방 목록을 조회할 수 있습니다.
@Slf4j
@RequiredArgsConstructor
@Component
public class WebSockChatHandler extends TextWebSocketHandler {
private final ObjectMapper objectMapper;
private final ChatService chatService;
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String payload = message.getPayload();
log.info("payload {}", payload);
// 삭제 TextMessage textMessage = new TextMessage("Welcome chatting sever~^^ ");
// 삭제 session.sendMessage(textMessage);
ChatMessage chatMessage = objectMapper.readValue(payload, ChatMessage.class);
ChatRoom room = chatService.findRoomById(chatMessage.getRoomId());
room.handleActions(session, chatMessage, chatService);
}
}
웹소켓 클라이언트로부터 채팅 메시지를 전달 받아 채팅 메시지 객체(DTO)로 변환하도록 수정하였습니다.
전달받은 메시지에 담긴 채팅방 Id로 발송 대상 채팅방 정보를 조회합니다.
해당 채팅방에 입장해있는 모든 클라이언트들(WebSocket Session
)에게 타입에 따른 메시지를 발송합니다.