WebSocket - 간단히 구현하기

Yu Seong Kim·2025년 5월 8일
0

SpringBoot

목록 보기
27/29
post-thumbnail

웹소켓 배경

HTTP의 한계

  • URL을 통한 요청: HTTP를 통해 서버로부터 데이터를 가져오는 유일한 방법입니다.
    • 따라서 HTTP는 사용자가 URL을 요청할 때에만 서버에서 해당 페이지를 꺼내는 방식입니다.
  • 사용자는 새로운 정보를 받기 위해서 서버로부터 반드시 새로운 URL을 요청해야 하는 문제가 있습니다.

Ajax 등장

  • HTTP를 효과적으로 이용하는 기술이며, 서버와 소통하기 위한 기술(약속 X)입니다.
  • 클라이언트에서 XMLHttpRequest 객체를 이용해 서버에 요청하면 서버가 응답하는 방식입니다.
  • 작동 방식
    1. 사용자의 이벤트로부터 Javascript는 사용자가 작성한 값이 쓰여진 DOM을 읽습니다.
    2. XMLHttpRequest 객체를 통해 웹 서버에 해당 값을 전송합니다.
    3. 웹 서버는 요청을 처리하고 XML, Test, JSON 등을 이용하여 XMLHttpRequest에 전송합니다.
    4. Javascript가 해당 응답 정보를 DOM에 씁니다.
  • 장점
    • 페이지 요청이 아닌 데이터 요청이라 부분적으로 정보를 갱신할 수 있게 됩니다.
      • 유저는 새로운 HTML을 서버로부터 받는 것이 아닌 동일한 페이지 내에서 DOM의 일부를 수정할 수 있게 됩니다.
      • HTML 페이지 전체를 바구는 것이 아닌 부분만 변경할 수 있게 됩니다.
    • 사용자 입장에서는 페이지 이동이 발생되지 않고, 페이지 내부 변화만 일어나게 해주어 그만큼이 자원과 시간을 아낄 수 있습니다.
  • 문제점
    - Ajax도 결국 HTTP로 서버와 통신하기 때문에 요청을 보내야 응답을 받을 수 있습니다.
    - 변경된 데이터를 가져오기 위해 버튼을 누르거나 일정 시간 주기로 요청(폴링 방식)을 보내게 되면 번거롭고 자원 낭비가 발생합니다.
    - 이러한 문제점을 해결하기 위해 웹 소켓이 탄생했습니다.

    웹 소켓이란?

    HTML5은 순수 웹 환경에서 실시간 양방향 통신이 가능해지게 만들었고, 이 명칭이 웹 소켓(Web Socket)입니다.

  • HTML5 표준 기술로, 클라이언트와 서버를 연결하고 실시간으로 통신이 가능하도록 하는 통신 프로토콜입니다.
  • Socket Connection을 유지한 상태로 실시간 양방향 통신 또는 데이터 전송이 가능한 프로토콜입니다.
  • 특징
    • 양방향 통신(Full-Duplex)
      • 동시에 데이터 송수신을 처리할 수 있는 통신 방법입니다.
      • 클라이언트와 서버가 원할 때 데이터를 주고받을 수 있습니다.
    • 실시간 네트워킹(Real Time-Networking)
      • 웹 환경속에서 연속된 데이터를 빠르게 노출합니다.
  • 동작 과정
    • 웹 소켓은 HTTP로 Handshake로 초기 통신을 연결한 후, 웹 소켓 프로토콜로 변환하여 데이터를 전송합니다.
    • 먼저 클라이언트가 서버에 HTTP 프로토콜로 Handshake 요청하면, 서버에서 응답 코드를 101로 응답 해주고 통신을 시작합니다.
  • 웹 소켓 테스트 도구
    • Postman
    • 크롬 Simple WebSocket Client → STOMP 테스트는 지원하지 않습니다.
    • wscat → 서버 또는 터미널에서 사용하기 좋습니다.
    • 등등

WebSocket 서버 구축하기

  • dependency 추가
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.10.1</version> <!-- 또는 최신 안정 버전 -->
        </dependency>

Gson
json 구조를 띄는 직렬화된 Data를 Java의 객체로 역직렬화, 직렬화 해주는 자바 라이브러리 입니다.
즉, JsonObject → JavaObject로 역직렬화


  • WebSocketConfig
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry){
        registry.addHandler(signalingSocketHandler(), "/ws/chat")
                .setAllowedOrigins("*");

    }

    @Bean
    public WebSocketHandler signalingSocketHandler() {
        return new SignalWebSocketHandler();
    }
}
  • @EnableWebSocket : 웹소켓 서버를 사용하도록 활성화합니다.
  • addHandler() : WebSocketHandler 클래스를 웹 소켓 핸들러로 정의습니다.
  • “/ws/chat” : 웹소켓 서버에 접속하기 위한 엔드포인트 설정(/ws/chat)합니다.
  • CORS : 클라이언트에서 웹소켓 서버에 요청 시 모든 요청을 수용습니다.(CORS)
  • setAllowedOrigins("*") : 메인이 다른 서버에도 접속 가능하도록 설정습니다.

  • ChatMessage
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ChatMessage {
    private String type;
    private String sender;
    private String receiver;
    private Object data;

    public void setSender(String sender) {
        this.sender = sender;
    }
    public void newConnect(){
        this.type = "new-connect";
    }

    public void closeConnect(){
        this.type = "close-connect";
    }

    @Override
    public String toString() {
        return "Message{"+
                "type='" + type +'\'' +
                ", sender='" + sender +'\'' +
                ", receiver='" + receiver +'\''+
                ", data='" + data +'}';
    }
}
  • 소켓 통신 시, 사용할 메시지 스펙입니다.

  • WebSocketHandler
@Slf4j
@Component
public class SignalWebSocketHandler extends TextWebSocketHandler {

    private final Map<String, WebSocketSession> sessionList = new ConcurrentHashMap<>();


    // 웹 소켓 연결
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        log.info("[WebSocket session established]" + session.getId());
        String userId = session.getId();
        sessionList.put(userId, session);

        ChatMessage chatMessage = ChatMessage.builder()
                .sender(userId)
                .receiver("all")
                .build();
        chatMessage.newConnect();

        sessionList.values().forEach(s -> {
            try{
                if(s.getId().equals(userId)){
                    s.sendMessage(new TextMessage(chatMessage.toString()));
                }
            }catch (Exception e){
                throw new RuntimeException(e);
            }
        });
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        ChatMessage chatMessage = getObject(message.getPayload());
        chatMessage.setSender(session.getId());

        WebSocketSession receiver = sessionList.get(chatMessage.getReceiver());
        if(receiver != null && receiver.isOpen()){
            receiver.sendMessage(new TextMessage(chatMessage.toString()));
            log.info("[WebSocket received]" + session.getId());
        }

    }

    private ChatMessage getObject(String textMessage){
        Gson gson = new Gson();
        ChatMessage message = gson.fromJson(textMessage, ChatMessage.class);
        return message;
    }

    // 소켓 연결 종료
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        String userId = session.getId();
        sessionList.remove(userId);

        final ChatMessage chatMessage = new ChatMessage();
        chatMessage.closeConnect();
        chatMessage.setSender(userId);

        sessionList.values().forEach(s -> {
            try {
                s.sendMessage(new TextMessage(chatMessage.toString()));
            }catch (Exception e){
                throw new RuntimeException(e);
            }
        });
    }

    // 소켓 통신 에러
    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        super.handleTransportError(session, exception);
    }
}
  • 소켓 통신은 서버와 클라이언트가 1:N 관계를 맺기 때문에 한 서버에 여러 클라이언트가 접속할 수 있습니다. 따라서 서버는 여러 클라이언트가 발송한 메시지를 받아 처리해줄 핸들러가 필요합니다.
  • 웹소켓을 통해 message를 전달하기 위해 WebSocketHandler 인터페이스를 상속받은 TextWebSocketHandler를 상속받아야 합니다.
    • afterConnectionEstablished: 웹소켓 연결
      • 최초로 웹소켓 서버에 연결하면, 웹소켓 서버에 연결된 다른 사용자에게 접속 여부를 전달해줍니다.
      • Map으로 세션 id를 key로, 세션을 value로 저장하고 있는 Map 자료구조를 이용합니다.
      • 본인을 제외한 나머지 세션에게 메시지를 보냅니다. (채팅방에 이미 들어와있던 사용자에게 신규 사용자가 들어왔다는 것을 알려주는 것)
    • handleTextMessage: 웹소켓을 통해서 받은 메세지를 처리하는 메소드(양방향 데이터 통신)
      • 메시지를 보낼 때는 메시지를 받을 타겟 사용자의 세션 아이디가 있어야 합니다. 메시지 스펙에 맞게 JSON으로 메시지를 전송합니다.
      • sessions 저장소에서 전달할 사용자를 찾고, 연결된 상태라면 메시지를 전송합니다.
    • afterConnectionClosed: 웹 소켓 연결 종료
      • 첫번째 사용자가 웹소켓을 종료하게 되면, 함께 통신하던 두번째 사용자에게 첫번째 사용자가 접속을 종료하였다는 메시지를 전송합니다.
    • handleTransportError: 웹소켓 통신 에러

결과

  1. postman 창 두개 띄워서 ws://localhost:8080/ws/chat 입력 후 Connect버튼을 눌러 두 사용자를 만들어 줍니다.

  2. 하나의 사용자에서 Message를 다른 사용자에 게 보냅니다.

  1. 다른사용자에서 앞에서 보낸 메세지를 확인 합니다. 그리고 반가워라고 다시 보냅니다.

4.그럼 Response에 반가워 라는 메세지가 도착했습니다.

마무리

간단하게 웹 소켓으로 채팅을 구현해보았습니다. 다음에는 채팅방을 추가하여 채팅의 고도화를 진행해보겠습니다.

참고
초록색거북이
MDN Web Docs
jmxx219

profile
인생을 코딩하는 남자.

0개의 댓글