[SpringBoot] Web Socket을 이용한 실시간 주식 데이터

HandMK·2023년 12월 27일
1

SpringBoot

목록 보기
3/6
post-thumbnail
post-custom-banner

💻 버전 관리

2023.12.27
SpringBoot 2.5.6V
JDK 11
Maven

🤔 어쩌다 만들게 되었을까?

모의 주식 거래 웹 서비스 프로젝트에 참여하면서 서비스 내부의 주식장을 형성해야 하는 역할을 맡았습니다.
처음에 RESTFul 방식으로 쓰레드를 엄청 짧게 줘서 하려고 생각 해봤으나 이는 서버에 너무 큰 부하를 줘서 다른 방법을 생각해보다 WebSocket 을 알게되어 이를 통해 구현하였습니다.

WebSocket 이란?

저희가 자주 사용하는 HTTP 통신 즉, CRUD와 같은 API 는 REST한 API 로 클라이언트에서 서버로 reqeust를 하게 되면 서버에서는 이에 대한 요청이 유효한 지 확인 후 response 줍니다. 그 후에 바로 연결이 끊겨버립니다. 이런식으로 말이죠.

물론 HTTP 방식으로도 실시간을 유사하게 구현할 수 있습니다.
Polling

이 방식의 핵심은 클라이언트에서 서버로 주기적으로 '데이터 변경이 있나요?' 요청을 하고 변경(서버 이벤트)이 있다면 해당 데이터를 가져오는 것입니다. 만약 실시간이 아니라 데이터의 업데이트 간격이 길다면? 괜찮습니다.

Streaming

스트리밍은 영상에서 많이 나오는 단어인데, 이 방법은 클라이언트가 서버로부터 한번에 다 받는 것이 아니라, 데이터를 작은 조각으로 나누어 순차적으로 받는 방식입니다. 입구가 열린 큐에 계속해서 데이터가 새로 들어오는 것이죠.

WebSocket

웹소켓은 클라이언트가 서버로 연결 Request(이땐, HTTP!) 를 하고 서버에서 유효성 검사 후 승인 하면 세션값이 바뀌지 않는 이상 현재의 세션 그대로 연결이 끊기지 않고 연결됩니다. 연결 된 상태에서는 클라이언트와 서버 간의 양방향 데이터 교류가 가능합니다.

WebSocket 구현

Dependency 설정

Maven

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

Gradle

implementation group: 'org.springframework.boot', name: 'spring-boot-starter-websocket'

WebSocketHandler

웹소켓 통신은 서버와 클라이언트가 1:N 관계를 맺습니다. 하나의 서버에 여러 클라이언트가 동시에 접근 할 수 있다는 소리죠. 이런 상황에서 연결이 에러가 나는 경우가 생기는데 이래서 필요한 것이 WebSocketHandler 입니다.
클라이언트와 서버 간의 연결을 관리하고 데이터를 수신, 전송하는 역할을 합니다.

package org.springframework.web.socket;

public interface WebSocketHandler {
    void afterConnectionEstablished(WebSocketSession session) throws Exception;
	// 웹소켓 연결이 된 후에 호출
    
    void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception;
	// 웹소켓 메세지가 도착하면 호출
    
    void handleTransportError(WebSocketSession session, Throwable exception) throws Exception;
	// 웹소켓 메세지 오류 처리
    
    void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception;
    // 웹소켓 연결이 해제된 후에 호출

    boolean supportsPartialMessages();
    // 웹소켓 핸들러가 부분 메세지를 처리하는 지 여부
}

저는 텍스트 데이터를 기반으로 송수신할꺼라 WebSocketHandler 인터페이스를 구현하고 있는 TextWebSocketHandler 를 상속받아 구현하였습니다.

☕️ StockWebSocketHandler.java

@Slf4j
@Component
public class StockWebSocketHandler extends TextWebSocketHandler {
    private StockWebSocketService stockWebSocketService;
    private final Map<WebSocketSession, String> sessionStockCodeMap = new ConcurrentHashMap<>();

    @Autowired
    public StockWebSocketHandler(StockWebSocketService stockWebSocketService){
        this.stockWebSocketService = stockWebSocketService;
    }
    Map<String, WebSocketSession> sessionMap = new HashMap<>(); /*웹소켓 세션을 담아둘 맵*/

    /* 클라이언트로부터 메시지 수신시 동작 */
    @Override
    public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String stockCode = message.getPayload(); /*stockCode <- 클라이언트에서 입력한 message*/
        log.info("===============Message=================");
        log.info("Received stockCode : {}", stockCode);
        log.info("===============Message=================");
        synchronized (sessionMap) {
            sessionStockCodeMap.put(session, stockCode);
        }
    }

    /* 클라이언트가 소켓 연결시 동작 */
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        log.info("Web Socket Connected");
        log.info("session id : {}",session.getId());
        super.afterConnectionEstablished(session);
        synchronized (sessionMap) { // 여러 클라이언트의 동시 접근하여 Map의 SessionID가 변경되는 것을 막기위해
            sessionMap.put(session.getId(), session);
        }
        System.out.println("sessionMap :" + sessionMap.toString());

        JSONObject jsonObject = new JSONObject();
        jsonObject.put("sessionId",session.getId());

        session.sendMessage(new TextMessage(jsonObject.toString()));
    }

    /* 클라이언트가 소켓 종료시 동작 */
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        log.info("Web Socket DisConnected");
        log.info("session id : {}", session.getId());
        synchronized (sessionMap) { // 여러 클라이언트의 동시 접근하여 Map의 SessionID가 변경되는 것을 막기위해
            sessionMap.remove(session.getId());
        }
        super.afterConnectionClosed(session,status); /*실제로 closed*/
    }

    @Scheduled(fixedRate = 5000)
    public void sendStockCode() throws JSONException, IOException {
        synchronized (sessionMap){
            for (WebSocketSession session : sessionMap.values()){
                String stockCode = sessionStockCodeMap.get(session);
                if(stockCode!=null) {
                    try { // 주식 데이터를 가져오는 로직이 길어 service 단에 설계
                        StockPriceResponseDto stockPriceResponseDto = stockWebSocketService.getStock(stockCode);
                        if (stockPriceResponseDto != null) {
                            String response = new ObjectMapper().writeValueAsString(stockPriceResponseDto);
                            log.info("Sending stock data : {}", response);
                            try {
                                session.sendMessage(new TextMessage(response));
                                // Message 보내기
                            }catch (IllegalStateException ex){
                                log.warn("Failed to send message, ignoring: {}",ex.getMessage());
                            }
                        } else {
                            log.warn("No stock data found for stockCode : {}", stockCode);
                        }
                    } catch (Exception e) {
                        log.error("Error while sending stock data : {}", e.getMessage());
                    }
                }
            }
        }
    }

}

여기서의 핵심은 HashMap을 생성하여 웹소켓 연결 접근 시 sessionID 를 Map에 넣어주고 연결 / 해제 시 sessionID가 동시에 접근해오는 클라이언트들에 의해 변경되지 않기 위해 synchronized() 하여 lock을 거는 것입니다.

WebSocketConfig

앞서 봤던 WebSocketHandler 가 데이터를 처리하는 역할이였다고 한다면, WebSocketConfig는 WebSocket의 설정을 담당하는 역할이라고 생각하시면 편해요.
Handler를 이용해 WebSocket을 활성화 하기 위해 WebSocketConfigurer 인터페이스를 구현하여 만들었습니다.

@Configuration
@EnableWebSocket // 웹소켓 활성화 어노테이션
public class StockWebSocketConfig implements WebSocketConfigurer {
    private final StockWebSocketHandler stockWebSocketHandler;
    @Autowired
    public StockWebSocketConfig(StockWebSocketHandler stockWebSocketHandler) {
        this.stockWebSocketHandler = stockWebSocketHandler;
    }
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry){
        /*webSocketHandler 를 추가*/
        registry.addHandler(stockWebSocketHandler, "/stock").setAllowedOrigins("*"); // endpoint 설정과 CORS 설정(*)
    }
}

✏️ Test

저는 포스트맨을 사용하여 WebSocket을 테스트 하였고, 테스트 시 WebSocket 프로토콜을 사용하여 테스트 하셔야 합니다!(기껏 만들어 놓고 삽질해서.....ㅎ)

영롱하다...
업로드중..

profile
몫을 다하는 개발자
post-custom-banner

0개의 댓글