🗂️ 개인프로젝트/IDOL

[개인프로젝트/IDOL] 4. 사용자를 입장 큐에 등록하기

케로⸝⸝◜࿀◝ ⸝⸝ 2024. 6. 20. 14:19

1. 사용자를 입장 큐에 등록

설명 HTTP 메서드와 URL
사용자를 입장 가능 상태로 전환 POST /api/v1/queue/allow
사용자가 입장 가능한 상태인지 조회 GET /api/v1/queue/allowed

 

AllowUserResponse

package me.progfrog.idol.flow.dto;

public record AllowUserResponse(
        Long requestCount,
        Long allowedCount
) {
}

 

AllowedUserResponse

package me.progfrog.idol.flow.dto;

public record AllowedUserResponse(
        Boolean isAllowed
){
}

 

UserQueueService

/**
 * 사용자를 입장 가능 상태로 전환
 * 1. 대기 큐에서 사용자 제거
 * 2. 입장 큐에 해당 사용자를 추가
 *
 * @param queue 대기 큐 이름
 * @param count 대기 큐에서 가져올 사용자 수
 * @return 입장 큐에 등록된 사용자 수
 */
public Mono<Long> allowUser(final String queue, final Long count) {
    var unixTimestamp = Instant.now().getEpochSecond();
    return reactiveRedisTemplate.opsForZSet().popMin(USER_QUEUE_WAIT_KEY.formatted(queue), count)
            .flatMap(queueEntry -> Optional.ofNullable(queueEntry.getValue())
                    .map(userId -> reactiveRedisTemplate.opsForZSet()
                            .add(USER_QUEUE_ALLOW_KEY.formatted(queue), userId, unixTimestamp))
                    .orElse(Mono.empty()))
            .count();
}

/**
 * 사용자가 입장 가능한 상태인지 조회
 *
 * @param queue 입장 큐 이름
 * @param userId 사용자 ID
 * @return 입장 가능 여부
 */
public Mono<Boolean> isAllowed(final String queue, final Long userId) {
    return reactiveRedisTemplate.opsForZSet().rank(USER_QUEUE_ALLOW_KEY.formatted(queue), userId.toString())
            .defaultIfEmpty(-1L)
            .map(rank -> rank >= 0);
}

 

UserQueueController

/**
 * 사용자를 입장 가능 상태로 전환
 *
 * @param queue 대기 큐 이름
 * @param count 대기 큐에서 가져올 사용자 수
 * @return 요청 수와 처리 수가 담긴 dto
 */
@PostMapping("/allow")
public Mono<AllowUserResponse> allowUser(@RequestParam(name = "queue", defaultValue = "default") String queue,
                                         @RequestParam(name = "count") Long count) {
    return userQueueService.allowUser(queue, count)
            .map(allowedCount -> new AllowUserResponse(count, allowedCount));
}

/**
 * 사용자가 입장 가능한 상태인지 조회
 *
 * @param queue 입장 큐 이름
 * @param userId 사용자 ID
 * @return 입장 가능 여부가 담긴 dto
 */
@GetMapping("/allowed")
public Mono<AllowedUserResponse> isAllowedUser(@RequestParam(name = "queue", defaultValue = "default") String queue,
                                               @RequestParam(name = "user-id") Long userId) {
    return userQueueService.isAllowed(queue, userId)
            .map(AllowedUserResponse::new);
}

 

2. 테스트 코드 추가

테스트 코드 전용의 임베디드 레디스 셋팅

의존성 추가

testImplementation 'com.github.codemonstur:embedded-redis:1.0.0'

 

테스트 코드용 application.properties 추가

spring.data.redis.host=127.0.0.1
spring.data.redis.port=63790

 

EmbeddedRedisConfig 추가

package me.progfrog.idol.flow;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.boot.test.context.TestConfiguration;
import redis.embedded.RedisServer;

import java.io.IOException;

@TestConfiguration
public class EmbeddedRedisConfig {

    private final RedisServer redisServer;

    public EmbeddedRedisConfig() throws IOException {
        this.redisServer = new RedisServer(63790);
    }

    @PostConstruct
    public void start() throws IOException {
        this.redisServer.start();
    }

    @PreDestroy
    public void stop() throws IOException {
        this.redisServer.stop();
    }
}

 

UserQueueServiceTest

package me.progfrog.idol.flow.service;

import me.progfrog.idol.flow.EmbeddedRedisConfig;
import me.progfrog.idol.flow.exception.ApplicationException;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.data.redis.connection.ReactiveRedisConnection;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import reactor.test.StepVerifier;

@SpringBootTest
@Import(EmbeddedRedisConfig.class)
class UserQueueServiceTest {

    @Autowired
    private UserQueueService userQueueService;

    @Autowired
    private ReactiveRedisTemplate<String, String> reactiveRedisTemplate;

    @AfterEach
    void afterEach() {
        ReactiveRedisConnection redisConnection = reactiveRedisTemplate.getConnectionFactory().getReactiveConnection();
        redisConnection.serverCommands().flushAll().subscribe();
    }

    @Test
    @DisplayName("registerWaitQueue: 대기 큐에 사용자 등록하기")
    void registerWaitQueue() {
        StepVerifier.create(userQueueService.registerWaitQueue("default", 100L))
                .expectNext(1L)
                .verifyComplete();

        StepVerifier.create(userQueueService.registerWaitQueue("default", 101L))
                .expectNext(2L)
                .verifyComplete();

        StepVerifier.create(userQueueService.registerWaitQueue("default", 102L))
                .expectNext(3L)
                .verifyComplete();
    }

    @Test
    @DisplayName("alreadyRegisterWaitQueue: 대기 큐에 이미 등록된 사용자 다시 등록 시도하기")
    void alreadyRegisterWaitQueue() {
        StepVerifier.create(userQueueService.registerWaitQueue("default", 100L))
                .expectNext(1L)
                .verifyComplete();

        StepVerifier.create(userQueueService.registerWaitQueue("default", 100L))
                .expectError(ApplicationException.class)
                .verify();
    }

    @Test
    @DisplayName("emptyAllowUser: 대기 큐가 비어있을 때 입장 큐에 사용자 넣기 시도")
    void emptyAllowUser() {
        StepVerifier.create(userQueueService.allowUser("default", 1L))
                .expectNext(0L)
                .verifyComplete();
    }

    @Test
    @DisplayName("allowUser: 대기 큐에 사용자가 있을 때, 입장 큐에 사용자 넣기 시도")
    void allowUser() {
        StepVerifier.create(userQueueService.registerWaitQueue("default", 100L)
                        .then(userQueueService.registerWaitQueue("default", 101L))
                        .then(userQueueService.registerWaitQueue("default", 102L))
                        .then(userQueueService.allowUser("default", 2L)))
                .expectNext(2L)
                .verifyComplete();
    }

    @Test
    @DisplayName("allowUser2: 대기 큐에 존재하는 사용자 수보다 더 많이 입장 큐에 사용자 넣기 시도")
    void allowUser2() {
        StepVerifier.create(userQueueService.registerWaitQueue("default", 100L)
                        .then(userQueueService.registerWaitQueue("default", 101L))
                        .then(userQueueService.registerWaitQueue("default", 102L))
                        .then(userQueueService.allowUser("default", 5L)))
                .expectNext(3L)
                .verifyComplete();
    }

    @Test
    @DisplayName("allowUserAfterRegisterWaitQueue: 입장 큐 처리 후 다시 대기 큐에 사용자 등록하기")
    void allowUserAfterRegisterWaitQueue() {
        StepVerifier.create(userQueueService.registerWaitQueue("default", 100L)
                        .then(userQueueService.registerWaitQueue("default", 101L))
                        .then(userQueueService.registerWaitQueue("default", 102L))
                        .then(userQueueService.allowUser("default", 3L))
                        .then(userQueueService.registerWaitQueue("default", 200L)))
                .expectNext(1L)
                .verifyComplete();
    }

    @Test
    @DisplayName("isNotAllowed: 입장 허용 안된 사용자 1")
    void isNotAllowed() {
        StepVerifier.create(userQueueService.isAllowed("default", 100L))
                .expectNext(false)
                .verifyComplete();
    }

    @Test
    @DisplayName("isNotAllowed2: 입장 허용 안된 사용자 2")
    void isNotAllowed2() {
        StepVerifier.create(userQueueService.registerWaitQueue("default", 100L)
                        .then(userQueueService.allowUser("default", 3L))
                        .then(userQueueService.isAllowed("default", 101L)))
                .expectNext(false)
                .verifyComplete();
    }

    @Test
    @DisplayName("isAllowed: 입장 허용된 사용자")
    void isAllowed() {
        StepVerifier.create(userQueueService.registerWaitQueue("default", 100L)
                        .then(userQueueService.allowUser("default", 3L))
                        .then(userQueueService.isAllowed("default", 100L)))
                .expectNext(true)
                .verifyComplete();
    }
}

 

반응형