웹 소켓이란 두 프로그램 간의 메세지 교환을 위한 통신 방법 중 하나이다.
웹 소켓은 서버와 클라이언트간에 연결을 유지하여 언제든 양방향 통신 또는 데이터 전송이 가능하게 하는 기술이다.
Real-Time Web application 구현을 위해 널리 사용되어지고 있다.
실시간 채팅 앱과 같은 요구사항들을 반영하기 위해 클라이언트에서 매번 HTTP 프로토콜을 통해 서버에 요청하는 것은 비효율적이다.
클라이언트에서 Ajax 통신 등으로 일부 보완할 수 있지만, Ajax도 결국 HTTP를 사용하기 때문에 여전히 문제가 되며, 웹 소켓을 통해 이를 해결할 수 있다.
implementation 'org.springframework.boot:spring-boot-starter-websocket'
채팅 메세지를 주고받기 위한 DTO를 생성해준다.
입장, 대화 두가지의 상황이 있으므로 enum으로 선언한다.
import lombok.*;
@Builder
@Getter
@Setter
@RequiredArgsConstructor
@AllArgsConstructor
public class ChatMessage {
private MessageType type;
private String roomId;
private String sender;
private String message;
public enum MessageType {
ENTER, TALK
}
}
채팅방을 위한 DTO를 생성해준다.
채팅방은 입장한 클라이언트들의 정보를 가지고 있어야 하므로 WebsocketSession 정보 리스트를 갖는다. 그리고 필요한 필드를 선언해준다.
handleAction 메서드의 If문으로 입장과 대화를 분기 처리한다.
sendMessage 메서드로 해당 채팅방에 있는 session에 메세지를 전송한다.
findAllRoom의 응답에 sessions가 들어가면 오류가 나므로 @JsonIgnore를 추가했다.
import com.ichubtou.websocket.service.ChatService;
import lombok.Builder;
import lombok.Getter;
import org.springframework.web.socket.WebSocketSession;
import java.util.HashSet;
import java.util.Set;
@Getter
public class ChatRoom {
private String roomId;
private String name;
@JsonIgnore
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));
}
}
생성, 조회, 하나의 세션에 메세지를 전송하는 기능을 가진 채팅 서비스를 생성한다.
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ichubtou.websocket.entity.ChatRoom;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import javax.annotation.PostConstruct;
import java.io.IOException;
import java.util.*;
@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);
}
}
}
채팅방의 생성 및 조회는 Rest API로 구현한다.
import com.ichubtou.websocket.entity.ChatRoom;
import com.ichubtou.websocket.service.ChatService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@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();
}
}
socket 통신은 서버와 클라이언트가 1:N으로 관계를 맺는다. 따라서 한 서버에 여러 클라이언트가 접속할 수 있으며, 서버에는 여러 클라이언트가 발송한 메세지를 받아 처리해줄 Handler의 작성이 필요하다.
TextWebSocketHandler를 상속받아 Handler를 작성해준다.
Client로부터 받은 메세지를 Console Log에 출력하고 입장했을때 입장 메세지를 출력한다.
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.ichubtou.websocket.service.ChatService;
import lombok.Builder;
import lombok.Getter;
import org.springframework.web.socket.WebSocketSession;
import java.util.HashSet;
import java.util.Set;
@Getter
public class ChatRoom {
private String roomId;
private String name;
@JsonIgnore
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));
}
}
지금까지 웹소켓으로 간단한 채팅 서비스를 만들어 보았다 그러나 아직 실제 채팅 서비스가 어떻게 돌아가는지 궁금한 부분이 많다.
데이터베이스와 어떻게 상호작용을 하는지 더 공부해봐야겠다.