현재 진행중인 프로젝트에 들어갈 채팅 서버를 만들어야 한다.
웹소켓으로 간단하게 구현하는 1:1 채팅 서버는 몇번 본 적 있는데 수 많은 사람들이 사용하는 채팅 서버는 어떻게 구성됐는지, 어떤 기술이 필요한지 잘 몰랐다. 이번 연재 시리즈를 통해 알아가 보도록 하겠다.
먼저, 웹소켓
웹소켓은 http의 stateless함을 보완하기 위해 나온 기술이며 이를 보완하기 위해 등장했다. http스펙 상에서 동작한다.
일단 프로젝트 생성
바로 본론으로 들어와서, 고객의 요청을 처리해 줄 핸들러를 작성한다.
일반적으로 스프링에서 핸들러는 프론트 컨트롤러(서블릿)가 정한 고객의 요청을 처리해주는 거라고 보면 된다.
// 많은 종류의 WebSocketHandler중에 TexxtWebSocketHandler를 상속받고 component를 이용해 빈에 등록, 이후 config를 통해 핸들러로써 등록해야함.
@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);
}
}
이후 config에서 핸들러 레지스트리에 웹소켓핸들러를 등록해준다. 이렇게하면 ws: ~ /ws/chat으로 들어오는 request에 대한 정보를 핸들러를 통해 session과 message로 맵핑해서 받아올 수 있다.
@RequiredArgsConstructor
@Configuration
@EnableWebSocket
public class WebSockConfig implements WebSocketConfigurer {
private final WebSocketHandler webSocketHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(webSocketHandler, "/ws/chat").setAllowedOrigins("*");
}
}
구글의 플러그인을 이용해서 간단하게 ws 통신을 해볼 수 있다.
Simple WebSocket Client
Construct custom Web Socket requests and handle responses to directly test your Web Socket services.
chrome.google.com
근데 이렇게하면 사실 그냥 1:1로 연결해서 메시지를 보내는 수준밖에 안된다. 채팅기능과는 거리가 멂..
여러명의 유저가 존재하고, 그 유저들이 여러 톡방중 골라 들어가는 기능을 개발할 필요가 있다.
그러기 위해서,, 1. 메시지 관련 DTO 2. 메시지 서비스 3. 채팅방 DTO 4. Controller를 만들어 줄 필요가 있다.
1. 메시지 DTO
@Getter
@Setter
public class ChatMessage {
// 메시지 타입 : 입장, 채팅
public enum MessageType {
ENTER, TALK
}
private MessageType type; // 메시지 타입
private String roomId; // 방번호
private String sender; // 메시지 보낸사람
private String message; // 메시지
}
2. 메시지 서비스
@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 {
session.sendMessage(new TextMessage(objectMapper.writeValueAsString(message)));
} catch (IOException e) {
log.error(e.getMessage(), e);
}
}
}
3. 채팅방 DTO
@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) {
// 메시지 타입이 enter면 입장
if (chatMessage.getType().equals(ChatMessage.MessageType.ENTER)) {
//sessions 라는 변수에 저장... (동시성 이슈가 있을것 같다)
sessions.add(session);
chatMessage.setMessage(chatMessage.getSender() + "님이 입장했습니다.");
}
// 메시지 타입이 enter가 아니면, sendMessage
sendMessage(chatMessage, chatService);
}
public <T> void sendMessage(T message, ChatService chatService) {
//받아온 sessions에서 session을 하나씩 가져와서 사용자가 작성한 메시지를 보낸다.
sessions.parallelStream().forEach(session -> chatService.sendMessage(session, message));
}
}
4. 컨트롤러
@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();
}
}