접속자 대기열 시스템 #8- 대기열 이탈

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

이번 단계에서는 접속 대기열 시스템에서 사용자의 이탈을 관리하고, 이를 테스트하는 방법을 다룹니다. 이를 통해 사용자가 대기열에서 벗어나거나 접속을 유지하는 로직을 구현하고 검증할 수 있습니다.

1. UserQueueController

UserQueueController는 사용자의 대기열 정보를 처리하고, 쿠키를 사용해 접속 정보를 유지합니다.

touch 메서드

@GetMapping("/touch")
Mono<?> touch(
        @RequestParam(name = "queue", defaultValue = "default") String queue,
        @RequestParam(name = "user_id") Long userId,
        ServerWebExchange exchange
) {
    return Mono.defer(() -> userQueueService.generateToken(queue, userId))
            .map(token -> {
                exchange.getResponse().addCookie(
                        ResponseCookie.from("user-queue-%s-token".formatted(queue), token)
                                .maxAge(Duration.ofSeconds(300))
                                .path("/")
                                .build()
                );

                return token;
            });
}

1. 기능 설명:

  • 사용자가 대기열에 접속할 때 호출되는 메서드입니다.
  • generateToken 메서드를 호출하여 사용자의 토큰을 생성하고, 이를 쿠키에 저장합니다.

2. 쿠키 설정:

  • ResponseCookie를 사용해 "user-queue-%s-token" 형식의 쿠키를 설정합니다. 쿠키는 300초(5분) 동안 유효하며, 대기열 접속 상태를 유지합니다.

2. WaitingRoomController

WaitingRoomController는 대기 중인 사용자의 접속을 확인하고, 입장이 허용되었는지 여부를 검사합니다.

waitingRoomPage 메서드

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,
            ServerWebExchange exchange
    ) {
        var key = "user-queue-%s-token".formatted(queue);
        var cookieValue = exchange.getRequest().getCookies().getFirst(key);
        var token = (cookieValue == null) ? "" : cookieValue.getValue();

        return checkIfUserIsAllowed(queue, userId, token, redirectUrl)
                .switchIfEmpty(registerUserAndShowWaitingPage(queue, userId));
    }

    private Mono<Rendering> checkIfUserIsAllowed(String queue, Long userId, String token, String redirectUrl) {
        return userQueueService.isAllowedByToken(queue, userId, token)
                .filter(Boolean::booleanValue)
                .flatMap(allowed -> Mono.just(Rendering.redirectTo(redirectUrl).build()));
    }
}

1. 기능 설명:

  • 사용자가 대기 중인지 확인하고, 쿠키를 통해 접속 상태를 검증합니다.
  • 입장이 허용된 경우, redirectUrl로 리다이렉트합니다.

2. 쿠키 검증:

  • 대기열 토큰이 쿠키에 저장되어 있는지 확인합니다. 만약 토큰이 없다면, 사용자는 등록되지 않은 상태로 간주됩니다.

3. 허용 여부 검사:

  • checkIfUserIsAllowed 메서드를 통해 사용자가 접속 허용 상태인지 검증합니다. 허용된 경우 리다이렉트 응답을 반환합니다.

3. UserQueueService

UserQueueService는 대기열 시스템의 비즈니스 로직을 처리하며, 토큰을 생성하고 검증합니다.

isAllowedByToken 메서드

public Mono<Boolean> isAllowedByToken(final String queue, final Long userId, final String token) {
    return this.generateToken(queue, userId)
            .filter(gen -> gen.equalsIgnoreCase(token))
            .map(i -> true)
            .defaultIfEmpty(false);
}

1. 기능 설명:

  • 사용자가 제공한 토큰이 서버에서 생성한 토큰과 일치하는지 검사합니다.
  • 토큰이 일치하면 true, 그렇지 않으면 false를 반환합니다.

2. 토큰 검증 로직:

  • generateToken 메서드를 통해 생성된 토큰과 사용자가 보낸 토큰을 비교하여 일치 여부를 확인합니다.

generateToken 메서드

public Mono<String> generateToken(final String queue, final Long userId) {
    MessageDigest digest = null;
    try {
        digest = MessageDigest.getInstance("SHA-256");
        var input = "user-queue-%s-%d".formatted(queue, userId);
        byte[] encodeHash = digest.digest(input.getBytes(StandardCharsets.UTF_8));

        StringBuilder hexString = new StringBuilder();
        for (byte aByte : encodeHash) {
            hexString.append(String.format("%02x", aByte));
        }
        return Mono.just(hexString.toString());
    } catch (NoSuchAlgorithmException e) {
        throw new RuntimeException(e);
    }
}

1. 기능 설명:

  • SHA-256 해시 알고리즘을 사용해 토큰을 생성합니다.
  • 토큰은 "user-queue-<queue_name>-<user_id>" 형식의 문자열을 해싱하여 생성됩니다.

2. 토큰의 용도:

  • 생성된 토큰은 사용자가 대기열에 등록된 상태를 검증하는 데 사용됩니다.

4. UserQueueServiceTest

테스트 클래스는 UserQueueService의 토큰 생성 및 검증 로직을 테스트합니다.

isAllowedByToken 테스트

@Test
void isNotAllowedByToken() {
    StepVerifier.create(userQueueService.isAllowedByToken("default", 100L, ""))
            .expectNext(false)
            .verifyComplete();
}

@Test
void isAllowedByToken() {
    StepVerifier.create(userQueueService.isAllowedByToken("default", 100L, "d333a5d4eb24f3f5cdd767d79b8c01aad3cd73d3537c70dec430455d37afe4b8"))
            .expectNext(true)
            .verifyComplete();
}

1. 설명:

  • isNotAllowedByToken 테스트는 빈 토큰이 제공된 경우 허용되지 않음을 확인합니다.
  • isAllowedByToken 테스트는 올바른 토큰이 제공된 경우 허용됨을 확인합니다.

generateToken 테스트

@Test
void generateToken() {
    StepVerifier.create(userQueueService.generateToken("default", 100L))
            .expectNext("d333a5d4eb24f3f5cdd767d79b8c01aad3cd73d3537c70dec430455d37afe4b8")
            .verifyComplete();
}

1. 설명:

  • generateToken 테스트는 queue와 userId 값에 따라 일관된 토큰이 생성되는지 검증합니다.

5. waiting-room.html

fetchWaitingRank 함수

응답이 성공적으로 반환되면 JSON 형식으로 파싱된 데이터를 사용해 대기열 정보를 업데이트합니다.

function fetchWaitingRank() {
    const queue = `[[${queue}]]`;
    const userId = `[[${userId}]]`;
    const queryParam = new URLSearchParams({queue: queue, user_id: userId});

    fetch('/api/v1/queue/rank?' + queryParam)
        .then(response => response.json())
        .then(data => {
            const numberElement = document.querySelector('#number');
            const timeElement = document.querySelector('#waiting-time');

            if (data.rank < 0) {
                fetch('/api/v1/queue/touch?' + queryParam)
                    .then(() => {
                        window.location.reload();
                    })
                    .catch(error => console.error(error));
                return;
            }

            // 대기 인원 업데이트
            if (numberElement) {
                numberElement.innerHTML = data.rank + '명 이상';
            }

            // 예상 대기 시간 업데이트
            if (timeElement) {
                timeElement.innerHTML = calculateEstimatedTime(data.rank);
            }
        })
        .catch(error => console.error(error));
}

1. 순위가 0보다 작은 경우 처리:

  • 만약 data.rank가 0보다 작으면, 사용자는 아직 대기열에 등록되지 않은 상태로 간주합니다.
  • 이 경우 /api/v1/queue/touch 엔드포인트에 요청을 보내어 접속 상태를 유지하고 페이지를 새로고침합니다.

2. 대기 인원 업데이트:

  • numberElement가 존재하면, 대기 인원 숫자를 업데이트합니다. 예를 들어, 100명 이상과 같이 표시합니다.

3. 예상 대기 시간 업데이트:

  • timeElement가 존재하면, calculateEstimatedTime 함수를 호출하여 계산된 예상 대기 시간을 업데이트합니다.

calculateEstimatedTime 함수

이 함수는 현재 대기열의 순위를 기반으로 예상 대기 시간을 계산합니다.

function calculateEstimatedTime(rank) {
    // 대기열에서 10초마다 100명씩 제거
    const removalRatePerSecond = 100 / 10;
    const estimatedTimeInSeconds = rank / removalRatePerSecond;

    const minutes = Math.floor(estimatedTimeInSeconds / 60);
    const seconds = Math.floor(estimatedTimeInSeconds % 60);
    return `${minutes}분 ${seconds < 10 ? '0' : ''}${seconds}초`;
}

1. 제거 속도 설정:

  • 대기열에서 10초마다 100명이 제거된다고 가정합니다. 따라서 초당 제거 속도는 100 / 10 = 10명입니다.

2. 예상 대기 시간 계산:

  • rank 값을 제거 속도로 나누어 총 예상 대기 시간을 초 단위로 계산합니다.

3. 분과 초로 변환:

  • 계산된 초를 분(minutes)과 나머지 초(seconds)로 변환하여 "분 초" 형식의 문자열로 반환합니다.

주기적인 fetchWaitingRank 호출

setInterval(fetchWaitingRank, 3000);

1. setInterval 함수를 사용하여 fetchWaitingRank 함수를 3초마다 호출합니다.
2. 이를 통해 3초마다 대기열 순위와 예상 대기 시간을 업데이트하여 사용자에게 실시간 정보를 제공합니다.

6. QnA

1. UserQueueController

Q1. touch 메서드의 역할은 무엇인가요?
A1. touch 메서드는 사용자가 대기열에 접속할 때 호출되어 사용자의 접속 상태를 유지합니다. 이 메서드는 사용자 ID와 대기열 이름을 기반으로 토큰을 생성하고, 이를 쿠키에 저장하여 5분 동안 유효하게 합니다.

Q2. 쿠키에 저장된 토큰의 유효기간은 어떻게 설정되나요?
A2. ResponseCookie를 사용하여 쿠키의 maxAge를 300초(5분)로 설정합니다. 이 기간 동안 사용자는 대기 상태를 유지하며, 이후에는 토큰이 만료됩니다.

Q3. 쿠키를 사용하여 대기열 접속 정보를 유지하는 이유는 무엇인가요?
A3. 쿠키를 사용하면 서버가 사용자 상태를 관리하기 쉬워지며, 사용자가 새로고침하거나 재접속할 때에도 동일한 접속 상태를 유지할 수 있습니다.

2. WaitingRoomController

Q1. waitingRoomPage 메서드의 기능은 무엇인가요?
A1. waitingRoomPage 메서드는 사용자가 대기 중인지 확인하고, 접속이 허용된 경우에는 지정된 리다이렉트 URL로 이동시키는 역할을 합니다. 접속이 허용되지 않은 경우 대기열 페이지를 표시합니다.

Q2. 대기열 토큰은 어디에서 확인하나요?
A2. 대기열 토큰은 ServerWebExchange에서 가져온 쿠키 값에서 확인합니다. 쿠키의 이름은 "user-queue-<queue_name>-token" 형식으로 설정됩니다.

Q3. checkIfUserIsAllowed 메서드는 어떤 경우에 리다이렉트를 하나요?
A3. checkIfUserIsAllowed 메서드는 사용자가 접속이 허용된 상태일 때, 지정된 redirectUrl로 리다이렉트합니다. 허용되지 않은 경우, 대기열 페이지를 표시합니다.

3. UserQueueService

Q1. isAllowedByToken 메서드는 어떤 기능을 하나요?
A1. isAllowedByToken 메서드는 사용자가 제공한 토큰이 서버에서 생성한 토큰과 일치하는지 검증합니다. 일치하면 true, 그렇지 않으면 false를 반환하여 접속 허용 여부를 결정합니다.

Q2. generateToken 메서드는 어떤 방식으로 토큰을 생성하나요?
A2. generateToken 메서드는 SHA-256 해시 알고리즘을 사용하여 토큰을 생성합니다. "user-queue-<queue_name>-<user_id>" 형식의 문자열을 해싱하여 고유한 토큰을 만듭니다.

Q3. 토큰 검증 시 대소문자 구분은 어떻게 하나요?
A3. isAllowedByToken 메서드는 equalsIgnoreCase를 사용하여 대소문자 구분 없이 토큰을 비교합니다.

4. UserQueueServiceTest

Q1. 테스트에서 StepVerifier는 어떤 역할을 하나요?
A1. StepVerifier는 리액티브 스트림의 결과를 검증하는 도구로, 예상되는 결과와 실제 결과가 일치하는지 단계별로 확인합니다. 예를 들어, expectNext와 같은 메서드를 사용하여 반환값을 검증할 수 있습니다.

Q2. isAllowedByToken 테스트에서 사용된 토큰 값은 어떻게 생성되었나요?
A2. 테스트에서는 generateToken 메서드를 사용하여 미리 생성된 토큰 값을 사용합니다. 이를 통해 예측 가능한 결과로 검증할 수 있습니다.

Q3. generateToken 메서드의 테스트는 무엇을 검증하나요?
A3. generateToken 테스트는 특정 queue와 userId 값에 대해 일관된 토큰이 생성되는지 확인합니다. 이를 통해 토큰 생성 로직이 예상대로 작동하는지 검증할 수 있습니다.

5. waiting-room.html

Q1. fetchWaitingRank 함수는 어떤 역할을 하나요?
A1. fetchWaitingRank 함수는 주기적으로 대기열의 순위를 확인하고, 대기 인원과 예상 대기 시간을 업데이트합니다. 이를 통해 실시간으로 사용자에게 정보를 제공합니다.

Q2. 순위가 0보다 작은 경우 어떻게 처리하나요?
A2. fetchWaitingRank 함수에서 순위가 0보다 작을 경우, /api/v1/queue/touch를 호출하여 접속 상태를 유지하도록 합니다. 이후 페이지를 새로고침하여 사용자 정보를 갱신합니다.

Q3. calculateEstimatedTime 함수는 어떤 방식으로 예상 대기 시간을 계산하나요?
A3. calculateEstimatedTime 함수는 대기열의 제거 속도를 기반으로 남은 순위를 초 단위로 변환하여 예상 대기 시간을 계산합니다. 결과는 "분 초" 형식으로 반환됩니다.

6. index.html (waiting-web)

Q1. index 메서드의 역할은 무엇인가요?
A1. index 메서드는 사용자의 대기열 상태를 확인하고, 접속이 허용된 경우 메인 페이지로 이동합니다. 허용되지 않은 경우 대기 페이지로 리다이렉트합니다.

Q2. 쿠키에서 토큰을 추출하는 이유는 무엇인가요?
A2. 쿠키에서 토큰을 추출하여 사용자의 대기 상태를 인증합니다. 이를 통해 사용자가 대기열에 등록된 상태인지 확인할 수 있습니다.

Q3. RestTemplate을 사용하여 API 요청을 보내는 이유는 무엇인가요?
A3. RestTemplate을 사용하면 다른 서비스와 쉽게 HTTP 통신을 할 수 있습니다. 여기서는 사용자 대기열 상태를 확인하기 위해 외부 서비스에 API 요청을 보내는 데 사용됩니다.

저작자표시 (새창열림)

'프레임워크 > 자바 스프링' 카테고리의 다른 글

접속자 대기열 시스템 #9- 테스트  (0) 2024.10.10
접속자 대기열 시스템 #7- 대기열 스케줄러 개발  (0) 2024.10.10
접속자 대기열 시스템 #6- 접속 대기 웹페이지 개발  (1) 2024.10.10
접속자 대기열 시스템 #5- Redis를 이용한 대기열 관리 및 웹페이지 진입 API 구현  (1) 2024.10.10
접속자 대기열 시스템 #3- 셋업  (2) 2024.10.10
'프레임워크/자바 스프링' 카테고리의 다른 글
  • 접속자 대기열 시스템 #9- 테스트
  • 접속자 대기열 시스템 #7- 대기열 스케줄러 개발
  • 접속자 대기열 시스템 #6- 접속 대기 웹페이지 개발
  • 접속자 대기열 시스템 #5- Redis를 이용한 대기열 관리 및 웹페이지 진입 API 구현
hyeseong-dev
hyeseong-dev
안녕하세요. 백엔드 개발자 이혜성입니다.
  • hyeseong-dev
    어제 오늘 그리고 내일
    hyeseong-dev
  • 전체
    오늘
    어제
    • 분류 전체보기 (282)
      • 여러가지 (107)
        • 알고리즘 & 자료구조 (72)
        • 오류 (4)
        • 이것저것 (29)
        • 일기 (1)
      • 프레임워크 (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)
      • 재태크 (4)
        • 암호화폐 (4)
        • 기타 (0)
  • 블로그 메뉴

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

  • 공지사항

  • 인기 글

  • 태그

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

  • 최근 글

  • hELLO· Designed By정상우.v4.10.0
hyeseong-dev
접속자 대기열 시스템 #8- 대기열 이탈
상단으로

티스토리툴바