🗂️ 개인프로젝트/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();
}
}
반응형