접속자 대기열 시스템 #6 - 접속 대기 웹페이지 구현 및 순번 조회
이번 단계에서는 접속 대기 웹페이지를 개발하고, 사용자가 대기열에 등록된 순번을 확인할 수 있도록 하는 기능을 구현합니다. 이를 위해 사용자 순번을 조회하는 API와 대기 페이지를 제공하는 컨트롤러를 작성하며, 테스트를 통해 올바르게 동작하는지 검증합니다.
1. UserQueueController - 순번 조회 API
UserQueueController
는 사용자 대기 순번을 조회할 수 있는 API를 제공합니다.
@GetMapping("/rank")
public Mono<RankNumberResponse> getRankUser(
@RequestParam(name = "queue", defaultValue = "default") String queue,
@RequestParam(name = "user_id") Long userId
) {
return userQueueService.getRank(queue, userId)
.map(RankNumberResponse::new);
}
1. 기능 설명:
- 이 API는 특정 사용자에 대한 대기열 순번을 조회합니다.
UserQueueService
의getRank
메서드를 호출하여 사용자의 현재 순번을 반환합니다.
2. 매개변수:
@RequestParam(name = "queue", defaultValue = "default") String queue
: 순번을 조회할 대기열의 이름입니다. 기본값은 "default"입니다.@RequestParam(name = "user_id") Long userId
: 순번을 조회할 사용자의 ID입니다.
3. 작동 방식:
userQueueService.getRank(queue, userId)
:UserQueueService
를 호출하여 사용자의 순번을 비동기적으로 조회합니다..map(RankNumberResponse::new)
: 조회된 순번을RankNumberResponse
객체로 감싸서 반환합니다.
2. WaitingRoomController - 접속 대기 페이지 제공
WaitingRoomController
는 사용자의 대기 여부를 확인하고, 접속 대기 페이지를 제공합니다.
@Controller
@RequiredArgsConstructor
public class WaitingRoomController {
private final UserQueueService userQueueService;
@GetMapping("/waiting-room")
public Mono<Rendering> waitingRoomPage(
@RequestParam(name = "queue", defaultValue = "default") String queue,
@RequestParam(name = "user_id") Long userId,
@RequestParam(name = "redirect_url") String redirectUrl
) {
return checkIfUserIsAllowed(queue, userId, redirectUrl)
.switchIfEmpty(registerUserAndShowWaitingPage(queue, userId));
}
private Mono<Rendering> checkIfUserIsAllowed(String queue, Long userId, String redirectUrl) {
return userQueueService.isAllowed(queue, userId)
.filter(Boolean::booleanValue)
.flatMap(allowed -> Mono.just(Rendering.redirectTo(redirectUrl).build()));
}
private Mono<Rendering> registerUserAndShowWaitingPage(String queue, Long userId) {
return userQueueService.registerWaitngQueue(queue, userId)
.onErrorResume(ex -> userQueueService.getRank(queue, userId))
.map(rank -> Rendering.view("waiting-room.html")
.modelAttribute("number", rank)
.modelAttribute("userId", userId)
.modelAttribute("queue", queue)
.build());
}
}
1. 기능 설명:
- 사용자가 대기열에 있을 때 접속을 허용할지, 대기 페이지를 보여줄지 결정합니다.
2. 메서드 설명:
waitingRoomPage
: 사용자가 허용된 경우redirectUrl
로 리다이렉트하고, 그렇지 않으면 대기 페이지를 제공합니다.checkIfUserIsAllowed
:UserQueueService
의isAllowed
메서드를 호출하여 사용자가 접속이 허용되었는지 확인합니다.registerUserAndShowWaitingPage
: 대기열에 등록하고, 등록 실패 시 순번을 조회하여 대기 페이지를 렌더링합니다.
3. RankNumberResponse - 응답 DTO
public record RankNumberResponse(Long rank) {
}
RankNumberResponse
는 사용자의 대기 순번을 응답하기 위한 DTO입니다.- Java 17의
record
를 사용하여 간결하게 정의하였습니다.
4. UserQueueService - 순번 조회 로직
UserQueueService
는 Redis의 ZSet을 사용하여 사용자의 대기 순번을 조회합니다.
public Mono<Long> getRank(final String queue, final Long userId) {
return reactiveRedisTemplate.opsForZSet()
.rank(USER_QUEUE_WAIT_KEY.formatted(queue), userId.toString())
.defaultIfEmpty(-1L)
.map(rank -> rank >= 0 ? rank + 1 : rank);
}
1. 작동 방식:
rank
: ZSet에서 사용자 ID의 순위를 조회합니다. 순위는 0부터 시작합니다.defaultIfEmpty(-1L)
: 순위가 없으면 -1을 반환합니다.map(rank -> rank >= 0 ? rank + 1 : rank)
: 순위는 0부터 시작하므로, 1을 더하여 사용자에게 1부터 시작하는 순번으로 반환합니다.
5. UserQueueServiceTest - 테스트 케이스
테스트를 통해 대기열 순번 조회 기능을 검증합니다.
@Test
void getRank() {
StepVerifier.create(
userQueueService.registerWaitngQueue("default", 100L)
.then(userQueueService.getRank("default", 100L)))
.expectNext(1L)
.verifyComplete();
StepVerifier.create(
userQueueService.registerWaitngQueue("default", 101L)
.then(userQueueService.getRank("default", 101L)))
.expectNext(2L)
.verifyComplete();
}
@Test
void emptyRank() {
StepVerifier.create(userQueueService.getRank("default", 100L))
.expectNext(-1L)
.verifyComplete();
}
1. 설명:
getRank()
: 사용자를 대기열에 등록하고 순번을 조회합니다. 각 사용자에 대해 순차적으로 순번이 증가하는지 확인합니다.emptyRank()
: 대기열에 등록되지 않은 사용자의 순번을 조회했을 때, -1이 반환되는지 확인합니다.
6. 결론
이번 단계에서는 접속 대기 웹페이지와 대기 순번 조회 기능을 구현했습니다. 이를 통해 사용자 접속 관리 및 대기열 시스템의 UX를 개선할 수 있습니다.
7. QnA
Q1. 순번 조회 시 rank
값이 -1로 나오는 이유는 무엇인가요?
- 이는 해당 사용자가 Redis의 ZSet에 존재하지 않기 때문입니다. 사용자가 대기열에 등록되지 않았거나 이미 허용된 사용자 목록으로 이동된 경우, ZSet에서 순위를 조회할 수 없으므로 -1을 반환합니다.
Q2. 순번 조회 API를 호출할 때 성능에 영향을 줄 수 있나요?
- Redis의 ZSet은 순위 조회 연산이 O(log(N))의 시간 복잡도를 가지므로, 대기열에 매우 많은 사용자가 있더라도 빠르게 처리할 수 있습니다. 따라서 순번 조회 API는 일반적인 상황에서 성능에 큰 영향을 주지 않습니다.
Q3. 사용자 접속이 허용되었을 때, 자동으로 리다이렉트가 가능한가요?
WaitingRoomController
에서 접속이 허용된 경우,checkIfUserIsAllowed
메서드를 통해redirectUrl
로 자동 리다이렉트됩니다. 이는 사용자가 접속할 수 있는 상태가 되었음을 의미하며, 지정된 URL로 즉시 이동하게 됩니다.
Q4. 대기열에 등록된 사용자의 순번은 어떻게 계산되나요?
- Redis의 ZSet에서 사용자의
rank
를 조회하며, 이는 0부터 시작합니다. 사용자에게 표시할 순번은 1부터 시작하는 것이 일반적이므로, 조회된rank
값에 1을 더해 반환합니다.
Q5. 대기열에 등록된 사용자가 너무 많은 경우, 성능 문제는 없을까요?
- Redis의 ZSet은 대규모 데이터에도 효율적인 정렬 기능을 제공하므로, 수천 명 이상의 사용자를 대기열에 등록해도 성능에 큰 문제가 없습니다. 하지만 대기열 크기가 극도로 큰 경우에는 Redis 클러스터를 통해 성능을 확장할 수 있습니다.
Q6. 접속 허용 여부를 판단하는 isAllowed
메서드의 작동 원리는 무엇인가요?
isAllowed
메서드는 Redis의 ZSet에서 사용자가 접속 허용 목록(USER_QUEUE_PROCEED_KEY
)에 있는지 확인합니다. 사용자가 해당 ZSet에 포함되어 있는 경우, 접속이 허용된 것으로 간주합니다.
Q7. switchIfEmpty
를 사용하는 이유는 무엇인가요?
switchIfEmpty
는 비어 있는 Mono(즉, 값이 없는 경우)에 대해 대체 동작을 지정할 때 사용됩니다. 예를 들어, 사용자가 대기열에 등록되어 있지 않으면 대기열 순번을 조회하여 대기 페이지를 렌더링하는 동작을 수행합니다.
Q8. 테스트에서 flushAll()
을 사용하는 이유는 무엇인가요?
flushAll()
은 Redis의 모든 데이터를 삭제하여 초기 상태로 만듭니다. 각 테스트는 독립적으로 실행되어야 하므로, 데이터 간의 상호 의존성을 제거하기 위해 테스트 전에 데이터를 초기화합니다.
Q9. checkIfUserIsAllowed
메서드는 대기열 외에 다른 곳에서도 사용할 수 있나요?
- 네, 이 메서드는 접속 허용 여부를 판단할 때 유용하며, 예를 들어 접속 제한이 있는 서비스나 웹 애플리케이션의 인증 단계에서도 활용할 수 있습니다.
Q10. 대기열에서 제거된 사용자는 다시 대기열에 등록할 수 있나요?
- 네, 허용된 사용자는 다시 대기열에 등록할 수 있습니다. 하지만 중복 등록을 방지하기 위해 대기열에 등록할 때 이미 등록된 사용자인 경우 오류를 반환하도록 설정할 수 있습니다.
Q11. default
대기열 외에 여러 대기열을 사용할 수 있나요?
- 네,
queue
파라미터를 통해 다양한 이름의 대기열을 사용할 수 있습니다. 이를 통해 서로 다른 대기열에서 독립적으로 사용자를 관리할 수 있습니다.
Q12. 사용자 허용 시 어떤 방식으로 순번이 조정되나요?
- 허용된 사용자는 대기열 ZSet에서 제거되기 때문에 나머지 사용자들의 순번이 앞당겨집니다. ZSet의 자동 정렬 특성 덕분에 별도의 순번 조정 로직이 필요하지 않습니다.
Q13. 대기열이 만료되거나 초기화되었을 때, 대기열 데이터는 어떻게 되나요?
- Redis의 데이터는 수동으로 삭제하거나 서버가 재시작되지 않는 한 유지됩니다. 만약 만료 시간을 설정하거나 명시적으로 초기화할 경우, 해당 대기열 데이터는 모두 삭제됩니다.
Q14. 대기 순번 조회와 사용자 허용의 타이밍 이슈가 발생할 수 있나요?
- 대기열에서 동시에 여러 사용자를 허용하거나 순번을 조회할 때, 타이밍 이슈가 발생할 수 있습니다. 이를 방지하기 위해 순차적으로 대기열을 처리하거나, Redis의 트랜잭션 기능을 사용할 수 있습니다.
Q15. 대기열에 등록된 사용자의 순번이 변경될 수 있나요?
- 네, 다른 사용자가 허용되거나 새로운 사용자가 등록되면 대기열의 순번이 조정될 수 있습니다. 이는 Redis의 ZSet이 자동으로 순서를 유지하는 특성 때문입니다.
Q16. allowUser
메서드의 반환 값은 항상 요청한 수와 동일한가요?
- 아닙니다. 요청한 사용자 수가 실제 대기열에 있는 사용자 수보다 적을 경우, 허용 가능한 최대 수만큼 반환됩니다. 예를 들어, 대기열에 2명만 있을 때 5명을 허용하면 실제로는 2명만 허용됩니다.
Q17. 대기열 시스템을 확장 가능한 아키텍처로 만들기 위해 어떤 방식을 사용할 수 있나요?
- Redis 클러스터를 도입하여 데이터 분산을 통해 확장성을 높일 수 있습니다. 또한, 여러 인스턴스를 두고 로드 밸런서를 통해 부하를 분산할 수 있습니다.
Q18. 사용자 등록 시 중복 등록 방지 로직을 강화하려면 어떻게 해야 하나요?
- 사용자 등록 시 이미 대기열에 있는 사용자인지 먼저 확인한 후, 등록 작업을 진행할 수 있습니다. Redis의 트랜잭션 기능을 사용하여 원자적으로 처리하는 것도 좋은 방법입니다.
Q19. 대기열 순번 조회 결과가 항상 최신 상태를 반영하나요?
- Redis의 ZSet은 실시간으로 데이터 변경 사항을 반영하지만, 클라이언트에서 조회할 때 약간의 지연이 있을 수 있습니다. 따라서 아주 짧은 간격으로 조회할 경우 결과가 조금씩 달라질 수 있습니다.
Q20. 순번 조회가 허용된 사용자 목록에도 적용되나요?
- 순번 조회는 대기열에 있는 사용자에 한해서만 유효합니다. 허용된 사용자 목록에서의 순번 조회는 별도로 구현해야 하며, 이 경우 ZSet의 다른 키를 사용할 수 있습니다.
위와 같이 QnA를 통해 다양한 시나리오와 문제를 다루어, 대기열 시스템의 특성과 동작 원리를 이해하고 시스템 설계 시 고려해야 할 점들을 명확히 할 수 있습니다.
'프레임워크 > 자바 스프링' 카테고리의 다른 글
접속자 대기열 시스템 #8- 대기열 이탈 (0) | 2024.10.10 |
---|---|
접속자 대기열 시스템 #7- 대기열 스케줄러 개발 (0) | 2024.10.10 |
접속자 대기열 시스템 #5- Redis를 이용한 대기열 관리 및 웹페이지 진입 API 구현 (1) | 2024.10.10 |
접속자 대기열 시스템 #3- 셋업 (2) | 2024.10.10 |
접속자 대기열 시스템 #4- 대기열 등록 API 개발 (6) | 2024.10.09 |