복잡한 웹소켓 핸들러, Event로 깔끔하게 해결하기

2025. 3. 3. 23:23·삽질로그

1. 들어가며

최근 아두이노 기반 대여기 시스템을 구축하면서, 서버와 대여기가 WebSocket을 통해 실시간으로 메시지를 주고받는 구조를 만들게 되었다. 하지만 기존 방식으로 WebSocket을 처리하는 과정에서 여러 가지 문제점이 발생했다.

  • 메시지를 수신하고 처리하는 과정이 한 곳에서 집중적으로 이루어져 유지보수가 어려움
  • 새로운 메시지 타입이 추가될 때마다 핸들러를 계속 수정해야 하는 문제 발생

이를 해결하기 위해 Spring의 Event을 활용하여 WebSocket 메시지를 처리하는 구조로 개선해보았다.
이제 하나씩 기존 코드와 문제점을 분석한 뒤, 어떻게 Spring Event를 활용한 이벤트 중심 처리 방식을 적용했는지 살펴보자.


2. 기존 코드 분석

public class WebSocketHandler extends TextWebSocketHandler {
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String payload = message.getPayload();
        log.info("📩 [메시지 수신] 세션 ID: {} → 페이로드: {}", session.getId(), payload);

        if (payload.startsWith("register:")) {
            Long registerId = Long.parseLong(payload.split(":")[1]);
            registerDevice(session.getId(), registerId);
            return;
        }
        
        if (payload.startsWith("open")) {
            // 1. DB 업데이트 수행
            rentalService.processRental(session.getId());
            
            // 2. 아두이노에 메시지 전송
            session.sendMessage(new TextMessage("open"));
            return;
        }
    }
}
기존 코드의 동작 방식은 아래와 같다.
  1. handleTextMessage()에서 메시지를 수신하고 바로 비즈니스 로직을 실행
  2. processRental() 메서드가 DB를 업데이트
  3. session.sendMessage()가 아두이노에 메시지를 전송

 

모든 로직이 WebSocketHandler 내에서 실행된다는 것을 알 수 있다.

3. 문제점 분석

WebSocketHandler가 너무 많은 역할을 수행

메시지 수신 -> DB 업데이트 -> 아두이노 응답 전송까지 모든 로직을 WebSocketHandler 한 클래스에서 처리하고 있다.

하나의 책임이 너무 많아 유지보수가 어렵다.
또한 새로운 기능을 추가할 때마다 handleTextMessage()가 점점 커지는 문제가 발생한다.

 

확장성 부족

새로운 메시지 타입(예: pause, resume)이 추가될 때마다 handleTextMessage()를 계속 수정해야 한다.
모듈화가 부족해 다른 기능과 독립적으로 동작하지 못한다.


4. Spring Event

이벤트란?

이벤트(Event)란 특정 상황이 발생했을 때, 이를 다른 컴포넌트에 알리는 방식을 의미한다.
즉, 하나의 컴포넌트에서 이벤트를 발생시키면(Spring에서는 ApplicationEvent를 사용), 이벤트 리스너(Event Listener)가 이를 감지하고 특정 작업을 수행할 수 있다.

📌 예제: “사용자가 회원가입을 완료하면 환영 이메일을 보내야 한다!”
• 이벤트 발생: 회원가입 완료
• 이벤트 리스너가 실행: 이메일을 발송

 

Spring에서 이벤트와 이벤트 리스너는 어떻게 동작하는가?

Spring에서는 기본적으로 Observer 패턴을 기반으로 이벤트 시스템을 제공한다.
즉, 하나의 객체가 이벤트를 발생시키면(Observer), 이를 감지하는 리스너(Subscriber)가 등록된 이벤트를 처리하는 방식이다.

📌 Spring 이벤트 흐름

1️⃣ 이벤트 발행 (Publisher)
특정 작업이 완료되었을 때, 이벤트를 발행하여 다른 컴포넌트가 이를 감지하고 처리할 수 있도록 한다.

public class OrderService {
    private final ApplicationEventPublisher eventPublisher;

   public void completeOrder(Long orderId) {
        log.info("✅ 주문 완료: {}", orderId);
      
       // 주문 완료 이벤트 발행
        eventPublisher.publishEvent(new OrderCompletedEvent(orderId));
    }
}
  • ApplicationEventPublisher.publishEvent(new 이벤트())를 호출하면 이벤트가 발행된다.

 

2️⃣ 이벤트 리스너 (Listener)
이벤트가 발행되었을 때, 이를 감지하여 처리하는 리스너(Subscriber) 를 정의한다.

public class OrderEventListener {
   
    @EventListener
    public void handleOrderCompleted(OrderCompletedEvent event) {
        log.info("📦 주문 처리 시작: {}", event.getOrderId());
      
        // 비즈니스 로직 실행
        processOrder(event.getOrderId());
    }
}
  • @EventListener를 사용하여 특정 이벤트가 발생하면 자동으로 실행될 메서드를 정의한다.

 

3️⃣ 이벤트 전달 및 실행
Spring의 ApplicationEventMulticaster가 이벤트를 자동으로 리스너에게 전달하고 실행한다.
즉, 개발자가 직접 호출하지 않아도 Spring이 알아서 발행된 이벤트를 리스너에게 전달해준다.

 

📌 Spring Event 동작 방식

처리 방식 동작 방식
동기 처리 (기본값) 이벤트가 발행되면, 해당 이벤트 리스너가 즉시 실행됨
비동기 처리 (@Async 사용) @Async를 사용하면 별도의 스레드에서 실행되어 비동기 처리 가능

5. Spring Event 기반으로 개선하기

이러한 문제를 해결하기 위해 Spring의 이벤트 시스템을 활용하여 WebSocket 메시지를 처리하는 구조로 변경했다.

 

Step 1: WebSocketHandler에서 이벤트 발행

WebSocketHandler는 이제 이벤트를 발생시키는 역할만 담당한다.

public class WebSocketHandler extends TextWebSocketHandler {
    private final ApplicationEventPublisher eventPublisher;

    public WebSocketHandler(ApplicationEventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String payload = message.getPayload();
        log.info("📩 [메시지 수신] 세션 ID: {} → 페이로드: {}", session.getId(), payload);

        // 이벤트 발행
        eventPublisher.publishEvent(new WebSocketEvent(session.getId(), payload));
    }
}
  • DB 업데이트 및 아두이노 메시지 전송 로직을 제거하고, 이벤트를 발생시키도록 변경
  • 메시지를 수신하면 WebSocketEvent를 Spring의 이벤트 시스템을 통해 전달

Step 2: 이벤트 리스너에서 비즈니스 로직 처리

이벤트 리스너에서 DB 업데이트 및 아두이노 메시지 전송을 처리한다.

public class WebSocketEventHandler {
    private final RentalUseCase rentalUseCase;
    private final WebSocketConnectionManager connectionManager;

    public WebSocketEventHandler(RentalUseCase rentalUseCase, WebSocketConnectionManager connectionManager) {
        this.rentalUseCase = rentalUseCase;
        this.connectionManager = connectionManager;
    }

    @Order(1)
    @EventListener
    public void handleWebSocketMessage(WebSocketEvent event) {
        Long rentalId = event.getRentalId();
        String slotName = event.getSlotName();
        String payload = event.getMessage();

        log.info("📩 [메시지 수신] RENTAL ID: {}, 슬롯: {}, 메시지: {}", rentalId, slotName, payload);

        switch (payload) {
            case "open", "rental" -> {
                log.info("🔓 [대여 요청] RENTAL ID: {}, 슬롯: {} - 대여 시작", rentalId, slotName);
                rentalUseCase.updateRentalStatusFromDevice(rentalId, slotName, true);
            }
            case "return" -> {
                log.info("🔒 [반납 요청] RENTAL ID: {}, 슬롯: {} - 대여 반납", rentalId, slotName);
                rentalUseCase.updateRentalStatusFromDevice(rentalId, slotName, false);
            }
        }
    }

    @Order(2)
    @EventListener
    public void handleRentalEvent(WebSocketEvent event) {
        log.info("📡 [이벤트 수신] RENTAL ID: {}, 슬롯: {}, 메시지: {}",
                event.getRentalId(), event.getSlotName(), event.getMessage());
        connectionManager.sendMessageToSession(event.getRentalId(), event.getSlotName(), event.getMessage());
    }
}

 

@EventListener를 사용하여 WebSocket 메시지를 동기적으로 처리

  • @Order(1) → 먼저 DB 업데이트 수행
  • @Order(2) → DB 업데이트가 완료된 후 아두이노로 메시지 전송

6. 더 나아가기

위 코드에서는 @Order를 통해 이벤트 리스너의 실행 순서를 보장하고 있다.
하지만 동기 이벤트와 비동기 이벤트에서 동작 방식이 다르다.

동기 이벤트 처리 시 @Order 동작

@Component
public class SyncEventListener {
    @Order(1)
    @EventListener
    public void firstListener(WebSocketEvent event) {
        log.info("1️⃣ [동기] DB 업데이트: {}", event.getRentalId());
    }

    @Order(2)
    @EventListener
    public void secondListener(WebSocketEvent event) {
        log.info("2️⃣ [동기] 아두이노 전송: {}", event.getRentalId());
    }
}
🔹 출력 결과
1️⃣ [동기] DB 업데이트: 101
2️⃣ [동기] 아두이노 전송: 101
  • 동기 이벤트에서는 @Order의 순서대로 실행된다.

비동기 이벤트 처리 시 @Order 동작

@Component
public class AsyncEventListener {
    @Async
    @Order(1)
    @EventListener
    public void firstListener(WebSocketEvent event) {
        log.info("1️⃣ [비동기] DB 업데이트: {}", event.getRentalId());
    }

    @Async
    @Order(2)
    @EventListener
    public void secondListener(WebSocketEvent event) {
        log.info("2️⃣ [비동기] 아두이노 전송: {}", event.getRentalId());
    }
}
🔹 출력 결과 
2️⃣ [비동기] 아두이노 전송: 101
1️⃣ [비동기] DB 업데이트: 101
  • 비동기 이벤트에서는 @Order가 동작하지 않는다.
  • 각 이벤트 리스너가 별도의 스레드에서 동작하므로 실행 순서가 예측 불가능하다.

7. 결론

이번 글에서는 Spring Event를 활용한 WebSocket 메시지 처리 방식과 그 이점을 살펴보았다.

Spring Event와 비동기 처리 방식에 대해 깊이 알아볼 주제는 많지만, 현재 대여 시스템에서는 동기적인 처리가 더 적합하다고 판단하여 동기 방식으로 이벤트를 적용했다.

 

그 이유는 다음과 같다:

  1. 사용자가 동시에 대여 요청을 보내는 빈도가 낮다.
  2. 처리 순서가 중요한 비즈니스 로직이 포함되어 있다.
  3. 비동기 이벤트를 적용할 경우, 예기치 않은 동기화 문제나 순서 보장 이슈가 발생할 수 있다.

따라서 현재는 순차적인 실행이 중요한 시나리오이므로 동기 이벤트 방식을 유지하되, 추후 사용자 요청이 많아지거나 성능 최적화가 필요할 경우 비동기 이벤트 처리로 전환할 계획이다.

 

🚀 다음 글에서는 "여러 클라이언트가 동시에 요청이 오면 어떻게 되는가?"에 대해 다룰 예정이다.
🔗 👉 2025.03.09 - [삽질로그] - 여러 요청이 동시에 들어오면, Spring Boot는 어떻게 처리할까?

 

 

'삽질로그' 카테고리의 다른 글

Spring Boot 비동기 처리 = 스레드풀?  (0) 2025.03.16
여러 요청이 동시에 들어오면, Spring Boot는 어떻게 처리할까?  (2) 2025.03.09
RDS 보안 정책 때문에 접속 불가?  (0) 2025.03.04
Batch Insert로 API 응답 속도 175배 개선하기  (0) 2025.03.03
내 API 응답시간은 왜 이렇게 오래 걸릴까?  (0) 2025.03.03
'삽질로그' 카테고리의 다른 글
  • 여러 요청이 동시에 들어오면, Spring Boot는 어떻게 처리할까?
  • RDS 보안 정책 때문에 접속 불가?
  • Batch Insert로 API 응답 속도 175배 개선하기
  • 내 API 응답시간은 왜 이렇게 오래 걸릴까?
mingking2
mingking2
에러와 삽질 속에서 성장하는 응애 개발자의 성장 일지
  • mingking2
    Mingking의 삽질 기록
    mingking2
  • 전체
    오늘
    어제
    • 분류 전체보기 (19)
      • 삽질로그 (10)
      • 스타트업 응애기 (1)
      • 프로젝트 도전기 (2)
      • DB 모르는 백엔드 탈출기 (5)
      • 네트워크 (1)
  • 블로그 메뉴

    • 홈
    • 태그
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    Async
    MVC
    @JoinColumn
    연관관계
    JPA
    spring
    n:m
    server
    Sync
    RDB
    orm
    Tomcat
    플로우차트
    DB
    servlet
    Java
    WebSocket
    context
    CompletableFuture
    batch
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
mingking2
복잡한 웹소켓 핸들러, Event로 깔끔하게 해결하기
상단으로

티스토리툴바