SSE 기반 실시간 주문 처리 최적화: 이벤트 로그로 성능 극대화하기

2025. 2. 12.

SSE 기반 실시간 주문 처리 최적화: 이벤트 로그로 성능 극대화하기

개요

이 글에서는 매장 주문 시스템의 실시간 처리 문제를 해결하기 위해 Server-Sent Events(SSE)와 이벤트 로그 테이블을 활용하여 성능을 최적화하는 방법을 설명합니다.
기존 방식의 한계, SSE 선택 이유, 이벤트 기반 최적화 방법, 그리고 실제 성능 개선 수치를 구체적으로 다룹니다.

이 문서를 통해 얻을 수 있는 것:

  • 실시간 데이터 처리 시스템 설계 방법
  • SSE를 활용한 네트워크 트래픽 최소화 전략
  • 대규모 주문 데이터 처리 성능 최적화 경험 공유

실시간 주문 시스템에서 성능 병목을 해결하고, 수십만 건의 데이터에서도 서버 부하 없이 빠르게 반응하는 구조를 만들고 싶다면 이 글이 도움이 될 것입니다.

실시간 주문, 왜 이렇게 어려운가?

매장 주문 시스템을 개발하면서 주문이 들어왔는데 화면에는 바로 표시되지 않는 문제를 확인했다. 문제는 단순히 페이지를 새로고침하면 해결되기도 했지만, 이 방식은 실시간 시스템이라고 부르기엔 너무 부족했다.

목표:

  • 주문이 들어오면 페이지를 새로고침하지 않고도 바로 반영되게 만들자.
  • 서버와 클라이언트 모두 부하를 최소화하자.

이를 해결하기 위해 다양한 실시간 통신 기술을 검토했다.

실시간 통신 기술 비교

방식 장점 단점 사용 예시
Polling 구현이 간단함, 모든 브라우저 지원 불필요한 요청 발생, 실시간성 저하 간단한 알림, 새로고침 기능
WebSocket 양방향 통신 지원 별도 서버 구축 필요, 설정 복잡 채팅, 게임, 다중 사용자 협업
SSE (Server-Sent Events) HTTP 기반, 단방향 실시간 푸시, 간단한 구현 단방향 통신만 가능 실시간 주문 관리, 금융 데이터 스트리밍

SSE를 선택한 이유

Polling은 트래픽이 과도하게 발생하고 실시간 반응성이 부족했다.
WebSocket은 양방향 통신이 필요할 때 강력하지만, 이번 프로젝트는 서버에서 클라이언트로 "주문 상태 알림"만 보내면 충분했다.

따라서, SSE(Server-Sent Events)를 도입하기로 결정했다.

SSE 적용 후 발생한 문제

초기에는 SSE를 통해 주문 상태를 실시간으로 스트리밍하면 모든 문제가 해결될 것으로 예상했다. 하지만 곧 몇 가지 심각한 문제가 드러났다.

주요 문제점

  1. 초기 접속 시 데이터 부재

    • SSE는 이벤트가 발생해야 데이터가 전송된다.
    • 새로운 주문이 발생하기 전까지는 아무런 데이터가 표시되지 않아, 사용자 입장에서는 빈 화면만 보였다.
  2. 불필요한 전체 데이터 전송

    • 주문 하나가 변경되어도 전체 주문 목록을 다시 전송했다.
    • 주문량이 많아질수록 네트워크 트래픽과 서버 부하가 급격히 증가했다.
  3. 성능 저하

    • 수백, 수천 건까지는 견딜 수 있었지만, 수십만 건 이상으로 증가하면 서버가 과부하로 다운되었다.

해결 방법: 이벤트 로그로 최적화하기

문제 해결을 위해 SSE의 역할을 "데이터 전송"이 아닌 "변경 알림"으로 제한하는 방향으로 접근했다.

개선 전략

  1. SSE는 주문 변경 신호만 전송
  2. 변경이 발생하면 클라이언트가 필요한 데이터만 API로 별도 요청

이벤트 로그 테이블 도입

또한, 주문 데이터 자체를 감지하는 대신, 별도의 이벤트 로그 테이블(order_events)을 도입해 주문 변경 사항을 기록하고 감지하도록 설계했다.

CREATE TABLE public.order_events (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    order_id UUID REFERENCES public.orders(id) ON DELETE CASCADE,
    event_type TEXT NOT NULL,
    created_at TIMESTAMPTZ DEFAULT now()
);
event_type 설명
order_paid 결제 완료
order_updated 주문 상태 변경

→ orders 테이블 대신 order_events 테이블만 감지하면, 변경이 발생했을 때만 최소한의 데이터로 실시간 업데이트 가능해졌다.

코드: 개선 전후 비교

기존 방식 (비효율적)

supabase
  .channel('orders')
  .on(
    'postgres_changes',
    { event: '*', schema: 'public', table: 'orders' },
    async (payload) => {
      const { data: orders } = await supabase.from('orders').select('*');
      writer.write(
        new TextEncoder().encode(`data: ${JSON.stringify(orders)}\n\n`),
      );
    },
  )
  .subscribe();
  • 주문 하나가 변경돼도 모든 주문 데이터를 다시 내려보냈다.

개선된 방식 (최적화 적용)

supabase
  .channel('order_events')
  .on(
    'postgres_changes',
    { event: 'INSERT', schema: 'public', table: 'order_events' },
    (payload) => {
      writer.write(
        new TextEncoder().encode(
          `data: ${payload.new.event_type}:${payload.new.order_id}\n\n`,
        ),
      );
    },
  )
  .subscribe();
  • "변경 알림"만 전송하고, 클라이언트가 해당 주문 데이터만 API를 통해 가져오게 변경했다.

성능 비교: 수치로 보는 개선 효과

주문 개수 기존 방식 (전체 refetch) 개선된 방식 (이벤트 기반)
100개 5ms 3ms
10,000개 150ms 5ms
1,000,000개 3.2s (서버 과부하 발생) 6ms

주문 수가 많아질수록 개선 효과가 극적으로 커졌다.

실시간 주문 업데이트 시연

realtime_order_gif

결론: 실시간 데이터 최적화는 선택이 아니라 필수다

  • 실시간 시스템이라고 해서 모든 데이터를 매번 전송할 필요는 없다.
  • 변경 알림과 데이터 조회를 분리하면 트래픽을 줄이고 서버 부하를 최소화할 수 있다.
  • 이벤트 로그 테이블을 활용하면 시스템 확장성도 크게 향상된다.

이번 경험을 통해, "빠르게 만드는 것""확실히 최적화하는 것" 사이의 균형을 다시금 고민하게 되었다.
실시간 데이터 처리 시스템을 설계할 때, 반드시 데이터 흐름과 부하 패턴을 고려해야 한다는 교훈을 얻었다.