접속자 대기열 시스템 #6- 접속 대기 웹페이지 개발

2024. 10. 10. 17:02·프레임워크/자바 스프링

접속자 대기열 시스템 #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
'프레임워크/자바 스프링' 카테고리의 다른 글
  • 접속자 대기열 시스템 #8- 대기열 이탈
  • 접속자 대기열 시스템 #7- 대기열 스케줄러 개발
  • 접속자 대기열 시스템 #5- Redis를 이용한 대기열 관리 및 웹페이지 진입 API 구현
  • 접속자 대기열 시스템 #3- 셋업
hyeseong-dev
hyeseong-dev
안녕하세요. 백엔드 개발자 이혜성입니다.
  • hyeseong-dev
    어제 오늘 그리고 내일
    hyeseong-dev
  • 전체
    오늘
    어제
    • 분류 전체보기 (284)
      • 여러가지 (108)
        • 알고리즘 & 자료구조 (72)
        • 오류 (4)
        • 이것저것 (29)
        • 일기 (2)
      • 프레임워크 (39)
        • 자바 스프링 (39)
        • React Native (0)
      • 프로그래밍 언어 (38)
        • 파이썬 (30)
        • 자바 (3)
        • 스프링부트 (5)
      • 운영체제 (0)
      • DB (17)
        • SQL (0)
        • Redis (17)
      • 클라우드 컴퓨팅 (2)
        • 도커 (2)
        • AWS (0)
      • 스케쥴 (65)
        • 세미나 (0)
        • 수료 (0)
        • 스터디 (24)
        • 시험 (41)
      • 트러블슈팅 (1)
      • 자격증 (0)
        • 정보처리기사 (0)
      • 재태크 (5)
        • 암호화폐 (5)
        • 기타 (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    ecs
    자바
    항해99
    그리디
    Spring Boot
    WebFlux
    시험
    프로그래머스
    Docker-compose
    java
    백준
    docker
    Redis
    spring
    celery
    OOP
    reactor
    AWS
    DP
    파이썬
    mybatis
    Python
    EC2
    완전탐색
    취업리부트
    FastAPI
    SAA
    Spring WebFlux
    RDS
    #개발자포트폴리오 #개발자이력서 #개발자취업 #개발자취준 #코딩테스트 #항해99 #취리코 #취업리부트코스
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.0
hyeseong-dev
접속자 대기열 시스템 #6- 접속 대기 웹페이지 개발
상단으로

티스토리툴바