Transport protocol의 일종
서버와 클라이언트 간에 Socket Connection 을 유지해서, 언제든 양방향 통신 또는 데이터 전송이 가능하도록 하는 기술
Real-time web application구현을 위해 널리 사용되고 있다.
HTTP 통신은 클라이언트 쪽에서 Request를 할 때만, 서버가 Response를 하는 방식으로 통신이 진행되는 한방향 통신이다.
이 경우, 서버 쪽 데이터가 업데이트 되더라도 클라이언트 쪽 화면에서 Refresh하지 않는 한 변경된 데이터가 업데이트 되지 않는 문제가 발생한다.
Web Socket 은 Stateful protocol(상태유지)이기 때문에, 클라이언트와 한 번 연결이 되면 계속 같은 라인을 사용해서 통신한다.
따라서, HTTP 사용에서 발생하는 HTTP와 TCP연결 트래픽을 피할 수 있다.
dependencies {
// WebSocket
implementation 'org.springframework.boot:spring-boot-starter-websocket'
}
@Configuration
@EnableWebSocket // WebSocket 활성화
@RequiredArgsConstructor
public class WebSocketConfig implements WebSocketConfigurer { // WebSocketConfigurer 인터페이스를 구현
private final WebSocketHandler webSocketHandler;
// 1. WebSocket 연결
@Override
public void registerWebSocketHandlers (WebSocketHandlerRegistry registry) {
registry.addHandler(webSocketHandler, "ws/chat")
.setAllowedOriginPatterns("*"); // 2.
}
}
1.
2.
메서드는 크게 3가지로 나뉜다.
@Component
@RequiredArgsConstructor
public class WebSocketHandler extends TextWebSocketHandler { // 1.
private final ObjectMapper objectMapper;
private final ChatService chatService;
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
// Json 형식으로 메세지를 웹소켓을 통해 서버로 보낸다
String payload = textMessage.getPayload();
// Handler 는 이를 받아, ObjectMapper 를 통해서 해당 Json 데이터를 MessageDto.class 에 맞게 파싱하여 MessageDto 객체로 변환
MessageDto messageDto = objectMapper.readValue(payload, MessageDto.class);
// 이 Json 데이터에 들어있는 roomId 를 이용해서, 해당 채팅방을 찾아 handlerAction() 이라는 메서드를 실행
messageService.findRoomById(messageDto.getRoomId());
// 2. 해당 참여자가 채팅방에 접속 상태인지, 이미 참여해 있는지에 따라 메시지 전송 방식 차별
handlerActions(session, messageDto, messageService);
}
public void handlerActions(WebSocketSession session, MessageDto messageDto, MessageService messageService) {
sessions.add(session);
sendMessage(messageDto, messageService);
}
// 메세지 전송
private <T> void sendMessage(T message, MessageService messageService) {
sessions.parallelStream()
.forEach(session -> messageService.sendMessage(session, message));
}
// 새로운 WebSocket 세션 연결 시, 세션을 sessions 집합에 추가
@Override
public void afterConnectionEstablished(WebSocketSession session) {
sessions.add(session);
}
// session 삭제
@Override
public void afterConnectionClosed(WebSocketSession webSocketSession, CloseStatus status) {
deleteSession(webSocketSession);
}
public void deleteSession(WebSocketSession webSocketSession) {
if (webSocketSession.isOpen()) {
try {
webSocketSession.close();
} catch (IOException e) {
log.error("WebSocket 세션을 닫는 중 오류가 발생했습니다: {}", e.getMessage());
}
}
sessions.remove(webSocketSession);
}
}
1.
2.
채팅에 관한 정보
@Getter
@Setter
public class MessageDto {
public enum MessageType { // 1.
ENTER, TALK
}
private MessageType type;
private String roomId;
private String sender;
private String message;
}
1.
채팅방에 관한 정보
@Getter
public class MessageRoomDto {
private String roomId;
@Builder
public MessageRoomDto(String roomId) {
this.roomId = roomId;
}
}
@RestController
@RequestMapping("/messageRoom")
@RequiredArgsConstructor
public class MessageRoomController {
private final MessageRoomService messageRoomService;
private final MessageService messageService;
// 1. 채팅방 생성
@PostMapping
public MessageRoomDto createRoom(@AuthenticationPrincipal UserDetailsImpl userDetails) {
return messageService.createRoom(userDetails.getUser());
}
// 채팅방 전체 조회
@GetMapping
public List<MessageRoomDto> findAllRoom() {
return messageService.findAllRoom();
}
// 채팅방 삭제
@DeleteMapping("/{id}")
public MsgResponseDto deleteRoom(@PathVariable Long id, @AuthenticationPrincipal UserDetailsImpl userDetails) {
return messageRoomService.deleteRoom(id, userDetails.getUser());
}
}
1.
@RequiredArgsConstructor
@Service
public class MessageService {
private final ObjectMapper objectMapper;
private final MessageRoomRepository messageRoomRepository;
private Map<String, MessageRoomDto> messageRoomDtoList; // key 는 roomId, value 는 채팅방 정보
@PostConstruct
private void init() {
messageRoomDtoList = new LinkedHashMap<>();
}
// 채팅방 전체 조회
public List<MessageRoomDto> findAllRoom() {
return new ArrayList<>(messageRoomDtoList.values());
}
// 채팅방 생성
public MessageRoomDto createRoom(User user) {
// roomId 는 UUID 를 통해 랜덤 생성
String randomId = UUID.randomUUID().toString();
MessageRoomDto messageRoomDto = MessageRoomDto.builder()
.roomId(randomId)
.name(user.getNickname())
.build();
// Map 에 채팅방 및 채팅 내용을 저장
messageRoomDtoList.put(randomId, messageRoomDto);
// DB 에 저장
messageRoomRepository.save(new MessageRoom(messageRoomDto.getName(), messageRoomDto.getRoomId(), user));
return messageRoomDto;
}
// 1. 채팅방의 webSocket session 에 메시지 전송
public <T> void sendMessage(WebSocketSession session, T message) {
try{
session.sendMessage(new TextMessage(objectMapper.writeValueAsString(message)));
} catch (IOException e) {
log.error(e.getMessage(), e);
}
}
}
1.
@Service
@RequiredArgsConstructor
public class MessageRoomService {
private final MessageRoomRepository messageRoomRepository;
// 채팅방 삭제
public MsgResponseDto deleteRoom(Long id, User user) {
MessageRoom messageRoom = messageRoomRepository.findByIdAndUser(id, user);
messageRoomRepository.delete(messageRoom);
return new MsgResponseDto("채팅방을 삭제했습니다.", HttpStatus.OK.value());
}
}
DB 에 message 와 messageRoom 을 저장하기 위해 엔티티를 만들어준다.
@Entity
@Getter
@Table(name = "message")
@NoArgsConstructor
public class Message extends Timestamped {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String sender;
@Column(name = "message")
private String message;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "messageRoomId", nullable = false)
private MessageRoom messageRoom;
@ManyToOne
@JoinColumn(name = "userId", nullable = false)
private User user;
}
@Entity
@Setter
@Getter
@Table(name = "messageRoom")
@NoArgsConstructor
public class MessageRoom {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Column(unique = true)
private String roomId;
@OneToMany(mappedBy = "messageRoom", cascade = CascadeType.REMOVE)
private List<Message> messageList = new ArrayList<>();
@ManyToOne
@JoinColumn(name = "userId", nullable = false)
private User user;
public MessageRoom(String name, String roomId, User user) {
this.name = name;
this.roomId = roomId;
this.user = user;
}
}
public interface MessageRepository extends JpaRepository<Message, Long> {
}
public interface MessageRoomRepository extends JpaRepository<MessageRoom, Long> {
MessageRoom findByIdAndUser(Long id, User user);
}
Simple Websocket Client 을 확장 프로그램으로 설치하거나 Postman 을 통해 websocket 채팅 기능을 테스트해 볼 수 있다.
{
"type":"ENTER",
"roomId":"e4f57c65-0b0f-4972-b20c-4a024a0a4f81",
"sender":"사용자1",
"message":"asd"
}
2번의 전송 내용도 보낸다.
{
"type":"TALK",
"roomId":"e4f57c65-0b0f-4972-b20c-4a024a0a4f81",
"sender":"사용자1",
"message":"asd"
}
참고: [Spring] webSocket으로 채팅서버 구현하기 (1)
참고: Spring websocket chatting server(1) - basic websocket server
참고: WebSocket 이란?
참고: [웹소켓] WebSocket의 개념 및 사용이유, 작동원리, 문제점
참고: 웹소켓과 일반 소켓의 차이는 뭔가요?