실시간 좌석 응답 기능 도입

smj_716·2025년 8월 17일

한이음 드림업

목록 보기
9/9
post-thumbnail

먼저 결과부터 보자!!
아래 영상은 JMeter 도구를 활용해 500명의 사용자가 특정 courseId에 대해 수강신청 → 수강취소를 반복하도록 테스트한 장면이다.

사용자 화면에서도 좌석 수가 즉시 반영되는 것을 확인할 수 있다.
❓우리는 어떻게 이런 실시간 좌석 반영 기능을 만들었을까❓


1. 고민 과정

💭 실시간 여석 수를 MySQL에서 가져와야 할까?

초기에 수강신청 여석은 MySQL 컬럼에서 관리했다.
하지만 실시간성 요구가 커질수록 디스크 기반 DB에서 잦은 조회는 부하가 크고 응답 속도도 한계가 있었다.
앞서 프로젝트에서 Redis를 사용했던 경험이 있었기 때문에 “좌석 수만큼은 Redis에서 관리하면 더 빠르게 실시간 처리가 되지 않을까?”라는 생각이 들었다.

🌟결국 MySQL은 정합성을 보장하는 저장소로, Redis는 실시간 여석 수를 관리하는 역할로 분리하게 되었다.

💭 그렇다면 어떻게 실시간을 전송할까?

✔️ Polling 기반 계획
처음에는 프론트에서 일정 주기로 잔여 좌석 조회 API를 호출하는 것을 계획했었다.
하지만 이 방식은 사용자 수가 늘어날수록 서버에 불필요한 요청이 폭주했고 실시간성도 “주기적 갱신”이라는 한계에 부딪혔다.

✔️ WebSocket 고려
양방향 통신이 가능해 강력했지만 이번 요구사항은
단방향 알림(서버→클라이언트)만 필요했다.
인프라 관리와 구현 난이도를 고려했을 때 과도한 선택이라고 판단했다.

✔️ SSE(Server-Sent Events) 선택
HTTP 기반 단방향 스트리밍이라 브라우저 기본 지원이 가능했고
프론트 적용도 단순했다.
서버는 필요한 시점에만 알림을 push하면 되었기 때문에 Polling 대비 서버 부하를 크게 줄일 수 있었다.
무엇보다 Redis와 함께 사용했을 때 빠른 조회 + 실시간 전송이라는 요구사항을 가장 깔끔하게 충족할 수 있었다.


2. 설계와 구현

실시간 기능을 적용하기 위해 아래와 같은 구조를 도입했다.

👉 Redis

  • 좌석 수는 course:{id}:remaining 키에 저장
  • 수강신청 성공 시 decrement(), 취소 시 increment()로 즉시 반영
  • Redis는 메모리 기반이라 조회/갱신 속도가 매우 빠름

👉 SseEmitterManager

private final Map<Long, SseEmitter> emitters = new ConcurrentHashMap<>();
private final Map<Long, List<Long>> subscribedCourses = new ConcurrentHashMap<>();
  • 사용자당 Emitter는 단 하나만 유지
  • 각 emitter에 사용자가 구독한 courseId 리스트를 매핑
  • 불필요한 중복 연결을 막아 서버 자원 효율성을 확보

👉 SseSeatService

  • 구독 시점에 Redis 값을 조회 → 프론트에 초기 여석 값 전달
  • 이후 좌석이 변하면 notifyRemainingChanged()에서 해당 courseId를 구독한 사용자에게만 이벤트 push

👉 EnrollmentService

  • 수강신청/취소가 성공하면 Redis 좌석 변경
  • 최신 좌석 값을 조회 후 구독자들에게 즉시 전송

📌 페이징을 사용하지 않은 이유

처음에는 강의 수가 많으니 페이징을 도입하는 것이 맞을까 고민했다.
하지만 몇가지 이유로 최종적으로는 페이징을 적용하지 않기로 결정했다.

  • Redis 조회 속도는 매우 빠르기 때문에 많은 강의 여석을 한 번에 내려주더라도 서버 부하가 크지 않았다.
  • 페이징을 적용하면 한 사용자가 여러 페이지(course 리스트)를 구독해야 하므로 사용자 1명당 여러 개의 SSE 채널이 발생하게 된다.
  • 이는 오히려 서버 부하를 증가시키고 클라이언트 측에서도 연결 관리가 복잡해진다.

그래서 전체 강의 구독 + 사용자당 단일 SSE emitter 유지라는 구조로 선택했다.


3. 느낀점

이번 기능을 구현하면서 단순히 새로운 기술을 학습한 것이 아니라,
💡“이전에 배운 기술들을 실제 문제 상황에 맞게 도입하고 적용한 경험”을 얻을 수 있었다.

  • 처음에는 MySQL만으로 좌석 수를 관리했지만 실시간 조회라는 요구사항 앞에서 한계를 느꼈다.
  • 그때 앞서 학습하고 이미 사용해본 Redis의 특성을 떠올려 여석 수를 Redis로 옮겼고
  • 마찬가지로 Polling 방식의 비효율을 개선하기 위해 과거 학습했던 SSE(Server-Sent Events)를 실서비스에 도입했다.

즉, 단순히 새로운 기술을 도입하는 것이 아니라
이전에 습득한 기술들을 문제 해결의 맥락에 맞게 해석하고 실제 기능에 녹여냈다는 점에서 의미가 컸다.

0개의 댓글