프로젝트에서 채팅을 구현해야 하는 일이 생겨서 추후 기능 고도화를 위해 미리 연습하기로 하였습니다.
이전에 socket은 구현해 본적이 있으나 webSocket은 한번도 사용해 보지 않아 인터넷에 올라와 있는 예제를 가지고 실습해 보려고 합니다.
WebSocket이란
WebSocket은 transport protocol의 일종으로 웹 버전의 TCP또는 Socket이라고 생각하면 됩니다. 서버와 클라이언트 사이에 socket connection을 유지해서 언제든지 양방향 통신이 가능하도록 하는 기술입니다. 실시간 웹애플리케이션(Real-Time web application) 구현을 위해 널리 사용되고 있습니다.(SNS, 멀티플레이어 게임, 구글 Doc, 화상 채팅....)
특징
웹애플리케이션에서 기존의 서버와 클라이언트간의 통시는 대부분 HTTP를 통해 이루어지고 Request/Respons기반의 Stateless protocol입니다. 즉 항상 connection을 유지하고 있는것이 아니라 필요할 때만 연결하여 리소를 얻어옵니다. 하지만 WebSocket은 처음 연결은 HTTP를 통해 이루어지고 이후 서버와 클라이언트간의 통신에는 webSocket 프로토콜만 사용합니다.
TextWebSocketHandler를 이용한 방식
build.gradle
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-websocket'
compileOnly 'org.projectlombok:lombok'
WebSocketConfig
package com.example.websocketchatpoc.config;
//import 생략
@RequiredArgsConstructor
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
private final ChatHandler chatHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(chatHandler,"/ws/chat").setAllowedOrigins("*");
}
}
ChatHandler
package com.example.websocketchatpoc.handler;
//import 생략
@Log4j2
@Component
public class ChatHandler 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 server!!");
session.sendMessage(textMessage);
}
}
WebSocket Test
아직 클라이언트를 만들지 않았기 때문에 chrome extention에서 simple websocket client를 실행하여 테스트를 진행하였습니다.
https://chrome.google.com/webstore/detail/websocket-test-client/fgponpodhbmadfljofbimhhlengambbn
WebSocket Test Client
A Simple tool to help test WebSocket Service
chrome.google.com

고도화
위에서 작성한 코드는 특정 클라이언트에서 ws/chat에 접속을 하게되면 HTTP 프로토콜을 이용하여 HandShake과정을 거친 후 성공하면 서버에서 101 Status Code를 보내주어 webSocket 프로토콜로 바뀜을 알려줍니다. 이후 클라이언트에서 webSocket 인스턴스의 send() 메서드를 통해 메세지를 전송합니다. 서버에서는 클라이언트가 ws/chat에 접속할때 마다 webSocketSession을 생성하고 이 세션은 Set에 저장하게 됩니다.
여기서 사용하는 webSocketSession은 일반 웹세션(서버에 클라이언트 별 정보를 저장하는 오브젝트)과 차이가 있습니다. 웹 세션은 브라우저가 Web Request를 할 때 마다, 쿠키의 세션 키로 구분하여 유저의 정보를 가져오는 것을 말합니다. 반면 webSocketSession은 브라우저가 Websocket을 접속했을 때의 커넥션 정보가 있는 것입니다. 즉 WebSocket의 커넥션 정보가 들어있는 인스턴스입니다.
ChatHandler
package com.example.websocketchatpoc.handler;
//import 생략
@Log4j2
@Component
@RequiredArgsConstructor
public class ChatHandler 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);
ChatMessageDTO chatMessageDTO = objectMapper.readValue(payload,ChatMessageDTO.class);
ChatRoomDTO room = chatService.findRoomById(chatMessageDTO.getRoomId());
room.handleActions(session,chatMessageDTO,chatService);
}
}
ChatService
package com.example.websocketchatpoc.application;
//import 생략
@Log4j2
@RequiredArgsConstructor
@Service
public class ChatService {
private final ObjectMapper objectMapper;
private Map<String, ChatRoomDTO> chatRooms;
@PostConstruct
private void init(){
this.chatRooms = new LinkedHashMap<>();
}
public List<ChatRoomDTO> findAllRoom(){
return new ArrayList<>(this.chatRooms.values());
}
public ChatRoomDTO findRoomById(String roomId){
return this.chatRooms.get(roomId);
}
public ChatRoomDTO createRoom(String name){
String randomId = UUID.randomUUID().toString();
ChatRoomDTO chatRoomDTO = new ChatRoomDTO(randomId,name);
this.chatRooms.put(randomId,chatRoomDTO);
return chatRoomDTO;
}
public <T> void sendMessage(WebSocketSession session, T message){
try{
session.sendMessage(new TextMessage(objectMapper.writeValueAsString(message)));
}
catch (IOException e){
log.error(e.getMessage(),e);
}
}
}
RoomDTO
DTO라고 하기에는 service인스턴스를 주입 받아 호출하는 이상한 모양이긴 하지만 예제 연습중이므로 실제 구현시 이렇게 하지 않을것 같다.
package com.example.websocketchatpoc.application.dto;
//import 생략
@Getter
public class ChatRoomDTO {
private String roomId;
private String name;
private Set<WebSocketSession> sessions = new HashSet<>();
public ChatRoomDTO(String roomId, String name){
this.roomId=roomId;
this.name=name;
}
public void handleActions(WebSocketSession session, ChatMessageDTO messageDTO, ChatService chatService){
if(messageDTO.getType().equals(MessageType.ENTER)){
this.sessions.add(session);
messageDTO.setMessage(messageDTO.getSender() +"님이 입장하였습니다.");
}
//실제로 제일 중요한 부분이다. 유저가 퇴장하거나 채팅창을 닫았을 경우 세션에서 제거를 해주어야 할것 같다.
//클라이언트에서 세션을 강제로 닫게되면 서버에서는 세션이 존재하여 메시지를 전송할때 끊긴 세션에 메시지를 전송하게 되어
//에러를 발생 시킨다.
//또 한가지 이유는 세션을 닫지 않았을경우 네트워크 리소스를 계속해서 잡아먹게된다.
if(messageDTO.getType().equals(MessageType.LEAVE)){
this.sessions.remove(session);
messageDTO.setMessage(messageDTO.getSender() +"님이 나갔습니다.");
}
sendMessage(messageDTO.getMessage(),chatService);
}
public <T> void sendMessage(T message, ChatService chatService){
this.sessions.parallelStream().forEach(session -> chatService.sendMessage(session,message));
}
}
ChatController
package com.example.websocketchatpoc.controller;
//import 생략
@RestController
@RequiredArgsConstructor
public class ChatController {
private final ChatService chatService;
@PostMapping(value = "/chat")
public ChatRoomDTO createRoom(@RequestParam String name){
return chatService.createRoom(name);
}
@GetMapping(value = "/chat")
public List<ChatRoomDTO> finaAllRoom(){
return chatService.findAllRoom();
}
}
위와 같이 고도화를 진행하면 채팅방끼리 대화가 가능해 집니다. 다음번에는 Stomp를 이용하여 채팅을 구현해 보도록 하겠습니다.
'Java & Spring' 카테고리의 다른 글
Spring Security를 이용한 JWT 인증(1) (0) | 2022.02.09 |
---|---|
Spring vs Spring Boot (0) | 2022.02.07 |