[Spring Boot] Spring Boot & Web Socket 간단한 채팅 기능 구현

ichubtou·2024년 1월 14일
1

웹 소켓(Web Socket)


웹 소켓이란 두 프로그램 간의 메세지 교환을 위한 통신 방법 중 하나이다.

웹 소켓은 서버와 클라이언트간에 연결을 유지하여 언제든 양방향 통신 또는 데이터 전송이 가능하게 하는 기술이다.

Real-Time Web application 구현을 위해 널리 사용되어지고 있다.

  • 실시간 채팅, 주식 시세 업데이트, 실시간 통지 및 알림에 사용된다.

WebSocket을 사용하는 이유

실시간 채팅 앱과 같은 요구사항들을 반영하기 위해 클라이언트에서 매번 HTTP 프로토콜을 통해 서버에 요청하는 것은 비효율적이다.

클라이언트에서 Ajax 통신 등으로 일부 보완할 수 있지만, Ajax도 결국 HTTP를 사용하기 때문에 여전히 문제가 되며, 웹 소켓을 통해 이를 해결할 수 있다.

WebSocket & HTTP의 특징과 차이점

  • WebSocket은 HTTP를 통해 최초 연결되며, 이후 일정 시간이 지나면 HTTP 연결은 자동으로 끊어지고 webSocket connection은 유지된다.
  • HTTP와 달리 WebSocket은 stateful 프로토콜이다.
  • HTTP는 stateless하기 때문에 서버에 변경사항이 생겨도 클라이언트에서 요청을 하지 않으면 변경사항이 적용되지 않지만, WebSocket은 지속적으로 connection을 유지하기 때문에 실시간으로 변경사항이 적용된다.
  • WebSocket은 HTTP와 동일한 port(80)를 사용한다.
  • 웹어플리케이션에서 기존의 서버와 클라이언트 간의 통신은 대부분 HTTP를 통해 이루어 졌으며 HTTP는 Request/response기반의 Stateless protocol이다.
    즉, 서버와 클라이언트 간의 Socket connection같은 영구적인 연결이 되어있지 않고 클라이언트 쪽에서 필요할때 Request를 할때만 서버가 Response를 하는 방식으로 통신이 진행되는 한방향 통신이다. 이럴경우 서버쪽 데이터가 업데이트 되더라도 클라이언트 쪽에는 화면은 Refresh하지 않는한 변경된 데이터가 업데이트 되지 않는 문제가 발생한다.
    대체 방법으로 Polling, Long Polling, Streaming 등이 있으나 이 모든 방법들이 HTTP를 통해 통신하기 때문에 Request, Response 둘 다 Header가 불필요하게 크다.
  • Web Socket은 Stateful protocol이기 때문에 클라이언트와 한 번 연결이 되면 계속 같은 라인을 사용해서 통신하기 때문에 HTTP 사용시 필요없이 발생되는 HTTP와 TCP연결 트래픽을 피할 수 있다. 마지막으로 Web Socket은 HTTP와 같은 포트(80)을 사용하기에 기업용 어플리케이션에 적용할 때 방화벽은 재설정 하지 않아도 되는 장점이 있다.

WebSocket 사용 시 주의점

  • WebSocket은 stateful 프로토콜로써 connection을 항상 유지해야하기 때문에 트래픽이 많은 경우 서버에 부담이 될 수 있다.
  • 연결이 계속 유지되어야 하기 때문에, 연결이 끊어졌을 때 적절히 대응할 수 있어야 한다.

Spring boot WebSocket 서버 구축


build.gradle에 라이브러리 추가

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();
    }
}

WebSocket Handler 구현


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));
    }
}

테스트


채팅방 생성

채팅방 입장

채팅방 조회

지금까지 웹소켓으로 간단한 채팅 서비스를 만들어 보았다 그러나 아직 실제 채팅 서비스가 어떻게 돌아가는지 궁금한 부분이 많다.

데이터베이스와 어떻게 상호작용을 하는지 더 공부해봐야겠다.

0개의 댓글