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

사용자 화면에서도 좌석 수가 즉시 반영되는 것을 확인할 수 있다.
❓우리는 어떻게 이런 실시간 좌석 반영 기능을 만들었을까❓
초기에 수강신청 여석은 MySQL 컬럼에서 관리했다.
하지만 실시간성 요구가 커질수록 디스크 기반 DB에서 잦은 조회는 부하가 크고 응답 속도도 한계가 있었다.
앞서 프로젝트에서 Redis를 사용했던 경험이 있었기 때문에 “좌석 수만큼은 Redis에서 관리하면 더 빠르게 실시간 처리가 되지 않을까?”라는 생각이 들었다.
🌟결국 MySQL은 정합성을 보장하는 저장소로, Redis는 실시간 여석 수를 관리하는 역할로 분리하게 되었다.
✔️ Polling 기반 계획
처음에는 프론트에서 일정 주기로 잔여 좌석 조회 API를 호출하는 것을 계획했었다.
하지만 이 방식은 사용자 수가 늘어날수록 서버에 불필요한 요청이 폭주했고 실시간성도 “주기적 갱신”이라는 한계에 부딪혔다.
✔️ WebSocket 고려
양방향 통신이 가능해 강력했지만 이번 요구사항은
단방향 알림(서버→클라이언트)만 필요했다.
인프라 관리와 구현 난이도를 고려했을 때 과도한 선택이라고 판단했다.
✔️ SSE(Server-Sent Events) 선택
HTTP 기반 단방향 스트리밍이라 브라우저 기본 지원이 가능했고
프론트 적용도 단순했다.
서버는 필요한 시점에만 알림을 push하면 되었기 때문에 Polling 대비 서버 부하를 크게 줄일 수 있었다.
무엇보다 Redis와 함께 사용했을 때 빠른 조회 + 실시간 전송이라는 요구사항을 가장 깔끔하게 충족할 수 있었다.
실시간 기능을 적용하기 위해 아래와 같은 구조를 도입했다.
👉 Redis
course:{id}:remaining 키에 저장decrement(), 취소 시 increment()로 즉시 반영👉 SseEmitterManager
private final Map<Long, SseEmitter> emitters = new ConcurrentHashMap<>();
private final Map<Long, List<Long>> subscribedCourses = new ConcurrentHashMap<>();
👉 SseSeatService
notifyRemainingChanged()에서 해당 courseId를 구독한 사용자에게만 이벤트 push👉 EnrollmentService
📌 페이징을 사용하지 않은 이유
처음에는 강의 수가 많으니 페이징을 도입하는 것이 맞을까 고민했다.
하지만 몇가지 이유로 최종적으로는 페이징을 적용하지 않기로 결정했다.
그래서 전체 강의 구독 + 사용자당 단일 SSE emitter 유지라는 구조로 선택했다.
이번 기능을 구현하면서 단순히 새로운 기술을 학습한 것이 아니라,
💡“이전에 배운 기술들을 실제 문제 상황에 맞게 도입하고 적용한 경험”을 얻을 수 있었다.
즉, 단순히 새로운 기술을 도입하는 것이 아니라
이전에 습득한 기술들을 문제 해결의 맥락에 맞게 해석하고 실제 기능에 녹여냈다는 점에서 의미가 컸다.