Skip to content

[QUEUE-220] 수강신청 여석 실시간 반영 기능(SSE + Redis) 구현#37

Merged
MinjiSeo16 merged 7 commits intodevelopfrom
feature/QUEUE-220-enrollment-seat-realtime
Aug 5, 2025
Merged

[QUEUE-220] 수강신청 여석 실시간 반영 기능(SSE + Redis) 구현#37
MinjiSeo16 merged 7 commits intodevelopfrom
feature/QUEUE-220-enrollment-seat-realtime

Conversation

@MinjiSeo16
Copy link
Copy Markdown
Collaborator

✅ PR Checklist

PR이 다음 요구 사항을 충족하는지 확인하세요.

  • 커밋 메시지 컨벤션에 맞게 작성했습니다.
  • 변경 사항에 대한 테스트를 했습니다.(버그 수정/기능에 대한 테스트).

📝 요약(Summary)

  • 수강신청 성공 → Redis의 여석 수 감소
  • 수강취소 성공 → Redis의 여석 수 증가
  • Redis 여석 변경 발생 시 → SSE를 통해 실시간 전송

📌 변경 사항

👉 SseEmitterManager

67f04ec

private final Map<Long, SseEmitter> emitters = new ConcurrentHashMap<>();
private final Map<Long, List<Long>> subscribedCourses = new ConcurrentHashMap<>();
  • studentId를 기준으로 emitter를 1개만 관리하고 해당 사용자가 구독 중인 courseId 리스트도 함께 저장
  • 이후 여석 변경 시 어떤 사용자에게 알림을 보내야 하는지 판단하기 위해 사용

👉 SseSeatService

d4628e1

public SseEmitter subscribe(Long studentId, List<Long> courseIds) {
		SseEmitter emitter = new SseEmitter(Long.MAX_VALUE);
		emitterManager.addEmitter(studentId, emitter, courseIds);

		for (Long courseId : courseIds) {
			String remaining = redisTemplate.opsForValue().get("course:" + courseId + ":remaining");
			asyncSender.send(emitter, "seat", Map.of(
				"courseId", courseId,
				"remaining", remaining
			));
		}
                ......
}
  • 프론트에서 전달한 courseId 리스트를 구독하고 해당 사용자에 대해 하나의 SSE emitter만 유지
  • 구독 시점에 Redis에 저장된 각 강의의 여석 수를 조회해 초기 값을 한 번 전송
public void notifyRemainingChanged(Long courseId, String remaining) {
		for (Map.Entry<Long, List<Long>> entry : emitterManager.getSubscribedCourses().entrySet()) {
			Long studentId = entry.getKey();
			List<Long> subscribed = entry.getValue();

			if (subscribed.contains(courseId)) {
				SseEmitter emitter = emitterManager.getEmitter(studentId);
				if (emitter != null) {
					asyncSender.send(emitter, "seat", Map.of(
						"courseId", courseId,
						"remaining", remaining
					));
				}
			}
		}
	}
  • 수강신청 또는 취소로 인해 여석이 변경되었을 때 호출되는 함수
  • 구독 중인 사용자 중 해당 courseId를 포함하고 있는 사용자에게만 남은 여석 수를 실시간으로 전송

👉 EnrollmentService

aea1ec1

String redisKey = "course:" + courseId + ":remaining";
		redisTemplate.opsForValue().decrement(redisKey);
		String remaining = redisTemplate.opsForValue().get(redisKey);
		sseSeatService.notifyRemainingChanged(courseId, remaining);
  • 수강신청 성공 시 Redis에 저장된 해당 강의의 여석 감소시킴
  • 이후 최신 여석 수를 다시 조회한 뒤, notifyRemainingChanged를 호출하여 해당 courseId를 구독 중인 사용자에게 SSE로 실시간 알림을 전송
  • 동일한 로직인 수강취소 시에는 increment()로 처리됨

👉 SseSeatController

23c0c89

  • 기존의 자리 수 조회 API는 단건 조회용이었고 polling 구조 POST /api/v1/enrollments/capacity
  • /api/v1/seats API는 프론트에서 courseId 리스트를 넘기면 해당 강의들의 여석을 실시간으로 push 받는 SSE 기반 구독 API GET /api/v1/seats
  • 사용자 인증 기반(@AuthenticationPrincipal)으로 사용자의 emitter를 구분

💬 공유사항 to 리뷰어

위에 언급하지 않은 SSE 관련 클래스나 코드는 queue서버와 동일하게 적용하였습니다.
아래 PR 기록 중 [QUEUE-100, 201, 204, 206] 에 설명해둔 내용 참고 후 질문 있으시면 해주세요!
https://github.com/QUEUE-SW/QUEUE-was/pulls?q=is%3Apr+is%3Aclosed

❓ 페이징을 도입하지 않은 이유

  • 수강신청화면에서 페이징 대신 한번에 전체 강의 리스트를 내려주는 방식을 유지했습니다.
  • Redis 조회 자체는 매우 빠르기 때문에 많은 강의 여석 수 조회로 인한 서버 부하는 크지 않다고 생각합니다.
  • 하지만 페이징을 도입하게 되면 한 사용자가 여러 페이지(course 리스트)를 구독해야 하기 때문에, 사용자 1명당 여러 개의 SSE 채널이 발생하게 됩니다.
  • 이 경우 오히려 전송 횟수가 증가해 서버 부하가 커질 수 있고 클라이언트 측에서도 연결 관리가 복잡해질 수 있습니다.

@MinjiSeo16 MinjiSeo16 self-assigned this Aug 4, 2025
@MinjiSeo16 MinjiSeo16 added ♻️ Refactoring 리팩토링 ✏️ Feat 기능 구현 labels Aug 4, 2025
@MinjiSeo16 MinjiSeo16 changed the title Feature/queue 220 enrollment seat realtime [QUEUE-220] 수강신청 여석 실시간 반영 기능(SSE + Redis) 구현 Aug 4, 2025
Copy link
Copy Markdown
Contributor

@mingking2 mingking2 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생하셨습니다~
아무리 생각해도 더 좋은 방법이 없는거 같네요 ㅜ

@MinjiSeo16 MinjiSeo16 merged commit 3747d8d into develop Aug 5, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✏️ Feat 기능 구현 ♻️ Refactoring 리팩토링

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants