ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [KB IT’s Your Life 7기] 서버사이드 컴포넌트 백엔드 개발 후기1
    카테고리 없음 2026. 6. 29. 01:57

     

     

    안녕하세요.

    오늘은  6월 4주차 기술블로그를 작성하고자 합니다.
    이번주부터는 서버사이드 컴포넌트 백엔드 개발 후기를 작성할 것인데요.

    Spring 환경에서 웹소켓(WebSocket) STOMP 결합하여 채팅 시스템을 구축하는 방법을 중심으로

    글을 작성해보려고 합니다.

     

     

     

     


    1. HTTP의 한계와 웹소켓의 등장

     

    우리가 익숙한 HTTP 통신은 단방향성(Unidirectional)과 비연결성(Stateless)이라는 명확한 특징을 갖습니다.

    클라이언트가 요청을 보내야만 서버가 응답할 수 있고, 응답 후에는 연결이 즉시 종료됩니다.

    이런 구조에서는 서버에 새로운 메시지가 쌓여도 클라이언트가 다시 물어보기 전까지는 데이터를 전달할 방법이 없습니다.

     

     

    이 문제를 해결하기 위해 등장한 것이 웹소켓(WebSocket) 프로토콜입니다.

    웹소켓은 한 번의 연결로 클라이언트와 서버가 양방향 연결을 유지(Full-Duplex)하게 해줍니다.

     

    연결 과정은 Handshake 단계로 시작됩니다.

    클라이언트가 HTTP 요청에 Upgrade: websocket 헤더를 담아 보내면, 서버는 101 Switching Protocols 응답을 보냅니다.

     

    이 순간부터 프로토콜은 http://에서 ws://로 전환되며, 실시간 통로가 개방됩니다.

     

     

     

     


    2. STOMP: 웹소켓에 질서를 부여하는 프로토콜

     

    웹소켓은 단순히 데이터를 주고받는 '형식 없는 통로'일 뿐입니다.

    데이터가 텍스트인지, JSON인지, 혹은 누구에게 가야 하는지 정의되어 있지 않죠.

    여기에 규격을 입혀주는 것이 바로 STOMP(Simple Text Oriented Messaging Protocol)입니다.

    STOMP는 발행-구독(Pub-Sub) 모델을 따릅니다. 이를 유튜브 채널에 비유하면 아주 쉽습니다.

     

     

    • Publisher(발행자): 특정 경로(채널)로 메시지를 보내는 사람.
    • Subscriber(구독자): 특정 경로(채널)를 구독하여 메시지를 기다리는 사람.
    • Broker(브로커): 발행자가 보낸 메시지를 가로채어, 해당 경로를 구독 중인 모든 이들에게 배달하는 중계자.

     

    STOMP를 사용하면 메시지 헤더에 'destination'을 명시할 수 있어, 서버에서 메시지를 선별적으로 처리하기 매우 유리해집니다.

     

     


    3. Spring 웹소켓 서버 설정 (WebSocketConfig)


    Spring에서 STOMP를 활성화하려면 @EnableWebSocketMessageBroker 설정을 작성해야 합니다.

     

     

    @Configuration
    @EnableWebSocketMessageBroker
    public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    
        @Override
        public void configureMessageBroker(MessageBrokerRegistry config) {
            // 메시지 브로커가 구독자들에게 메시지를 전달할 때 사용하는 접두어 (Broadcast)
            config.enableSimpleBroker("/topic", "/queue");
            
            // 클라이언트가 서버(Controller)로 메시지를 보낼 때 사용하는 접두어
            config.setApplicationDestinationPrefixes("/app");
        }
    
        @Override
        public void registerStompEndpoints(StompEndpointRegistry registry) {
            // 최초 웹소켓 연결을 위한 엔드포인트 URL: ws://localhost:8080/chat-app
            registry.addEndpoint("/chat-app").setAllowedOrigins("*");
        }
    }

     

     

    Spring Legacy 환경(XML-less)에서는 위 클래스만 작성한다고 동작하지 않습니다.

    반드시 WebConfig(AbstractAnnotationConfigDispatcherServletInitializer 상속 클래스)의

    getServletConfigClasses() 메서드에 WebSocketConfig.class를 등록해야 합니다.

    이 단계를 누락하면 웹소켓 연결 시 404 에러를 마주하게 됩니다.

     


    4. 메시지 처리를 위한 컨트롤러 구현 (@MessageMapping)

     

     

    이제 클라이언트가 보낸 메시지를 가로채 비즈니스 로직을 수행할 컨트롤러를 작성합니다.

    @Controller
    @Log4j2
    public class ChatController {
    
        // 클라이언트가 /app/chat으로 보낸 메시지를 수신
        @MessageMapping("/chat")
        // 로직 처리 후 /topic/chat을 구독 중인 모든 클라이언트에게 브로드캐스트
        @SendTo("/topic/chat")
        public ChatMessage chat(ChatMessage message) {
            log.info("수신 메시지: " + message);
            return message; 
        }
    }

     

     

    브로커 경로 관례 단순히 모든 사람에게 뿌리는 채팅은 /topic 경로를 사용하지만,

    특정 사용자에게만 보내는 1:1 통신은 /queue 경로와 함께 @SendToUser 어노테이션을 활용합니다.

    또한 구독 시점에 초기 데이터를 밀어주고 싶다면 @SubscribeMapping을 검토해 보세요.

     


    5. 데이터 전송의 마법: JSON과 DTO 자동 변환

     

    STOMP를 통해 오가는 메시지는 보통 JSON 형태입니다. Spring은 내부적으로 Jackson 라이브러리와 HttpMessageConverter 원리를 활용해 이 JSON을 자바 객체(DTO)로 변환합니다.

     

    주의사항!

    1. Lombok 활용: DTO 작성 시 @Data, @NoArgsConstructor, @AllArgsConstructor를 사용하여 기본 생성자와 Getter/Setter를 반드시 갖춰야 매핑이 원활합니다.
    2. 필드명 일치: JSON의 키값과 DTO의 필드명이 1:1로 정확히 일치해야 합니다. (예: JSON {"content":"hi"} ↔ DTO private String content;)


    6. 클라이언트 구현: stomp.js 활용하기

     

    브라우저 환경에서는 stomp.js 라이브러리를 사용하여 서버에 연결합니다.

     

    // 1. STOMP 클라이언트 생성 (엔드포인트 주소 확인 필수)
    const stompClient = new StompJs.Client({
        brokerURL: 'ws://localhost:8080/chat-app'
    });
    
    // 2. 연결 성공 시 콜백
    stompClient.onConnect = (frame) => {
        // 3. 토픽 구독 (서버의 @SendTo와 매칭)
        stompClient.subscribe('/topic/chat', (chat) => {
            const data = JSON.parse(chat.body);
            showMessage(`${data.name}: ${data.content}`);
        });
    };
    
    // 4. 활성화
    stompClient.activate();
    
    // 5. 메시지 발행 (서버의 @MessageMapping과 매칭)
    function sendMessage() {
        stompClient.publish({
            destination: '/app/chat',
            body: JSON.stringify({ name: "개발자", content: "안녕하세요!" })
        });
    }

     


    7. 채팅 화면 구성 및 동작 연결 (index.jsp)

     

    화면은 단순한 HTML 구조로 작성하되, 수신된 데이터를 동적으로 렌더링하는 로직이 핵심입니다.

    • UI 구조: 이름 입력 필드(id="name"), 메시지 입력창(id="content"), 그리고 메시지가 누적될 테이블 바디(id="chat-messages")를 준비합니다.
    • 동적 업데이트: showMessage() 함수 내부에서 innerHTML += '<tr><td>' + message + '</td></tr>'와 같은 방식으로 수신 메시지를 실시간으로 하단에 추가합니다.

     

     


    8. 실전 응용: 보안(Security)과 예외 처리

     

    실제 운영 환경에서는 아무나 채팅에 참여해서는 안 됩니다. Spring Security를 적용하여 isAuthenticated() 조건으로 접근을 제한해야 합니다.

    중요 설정: 보안 필터를 활성화하기 위해 반드시 AbstractSecurityWebApplicationInitializer를 상속받은 SecurityInitializer 클래스를 작성해야 합니다. 이것이 등록되어야 보안 필터 체인이 웹소켓 요청을 적절히 검사할 수 있습니다.

    또한, 메시지 처리 중 발생하는 런타임 예외는 @MessageExceptionHandler를 사용하여 클라이언트에게 에러 정보를 STOMP 메시지로 안전하게 전달할 수 있습니다.

     

     

     


    9. 마무리: 실시간 통신의 정석

     

     

    지금까지 살펴본 데이터의 흐름을 정리하면 다음과 같습니다.

    1. 발행: 클라이언트가 /app/chat으로 JSON 전송.
    2. 처리: 서버 컨트롤러가 DTO로 변환하여 수신 후 로직 처리.
    3. 중계: 서버가 결과를 /topic/chat 경로로 브로커에 전달.
    4. 수신: 해당 경로를 구독 중인 모든 클라이언트가 JSON을 받아 화면 갱신.

     

    오늘은 여기까지!

     



     

Designed by Tistory.