🗂️ 개인프로젝트/IDOL

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

케로⸝⸝◜࿀◝ ⸝⸝ 2024. 6. 20. 10:18

1. flow 모듈 설정

build.gradle

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.2.6'
    id 'io.spring.dependency-management' version '1.1.5'
}

group = 'me.progfrog.idol'
version = '0.0.1-SNAPSHOT'

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
    implementation 'org.springframework.boot:spring-boot-starter-data-redis-reactive'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'io.projectreactor:reactor-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
    useJUnitPlatform()
}

 

FlowApplication 추가

package me.progfrog.idol.flow;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class FlowApplication {
    public static void main(String[] args) {
        SpringApplication.run(FlowApplication.class, args);
    }
}

 

application.properties

  • 포트 설정 9010
  • 웹 애플리케이션 타입 설정 reactive
 

[스프링] spring.main.web-application-type=reactive

1. 개요@RestControllerAdvice를 사용하여 전역 예외 처리를 하는 코드에서 원하는 응답값이 나오지 않고 다음과 같은 응답값이 지속적으로 나와서 해당 부분의 원인을 찾다가 spring.main.web-application-type

progfrog.tistory.com

server.port=9010

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

spring.main.web-application-type=reactive

 

2. 사용자를 대기 큐에 등록

설명 HTTP 메서드와 URL
사용자를 대기 큐에 등록 POST /api/v1/queue

 

RegisterUserResponse

package me.progfrog.idol.flow.dto;

public record RegisterUserResponse(
        Long rank
) {
}
 

[Java] 자바 16 레코드(record)를 DTO에 적용하기

1. 레코드(record)란?자바 16부터 정식으로 도입레코드는 데이터 중심의 클래스를 보다 간결하게 정의할 수 있도록 설계되었음불변 객체를 쉽게 만들 수 있고, 자동으로 생성자, 접근자(getter), equals

progfrog.tistory.com

 

UserQueueService

package me.progfrog.idol.flow.service;

import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;

import java.time.Instant;

@Service
@RequiredArgsConstructor
public class UserQueueService {

    private final ReactiveRedisTemplate<String, String> reactiveRedisTemplate;

    private final String USER_QUEUE_WAIT_KEY = "users:queue:%s:wait";

    /**
     * 사용자를 대기 큐에 등록
     * redis sorted set
     * key: userId, value: unix timestamp
     *
     * @param queue 대기 큐 이름
     * @param userId 사용자 ID
     * @return rank 대기 번호
     */
    public Mono<Long> registerWaitQueue(final String queue, final Long userId) {
        var unixTimestamp = Instant.now().getEpochSecond();
        return reactiveRedisTemplate.opsForZSet().add(USER_QUEUE_WAIT_KEY.formatted(queue), userId.toString(), unixTimestamp)
                .filter(isAdded -> isAdded)
                .switchIfEmpty(Mono.error(ErrorCode.QUEUE_ALREADY_REGISTERED_USER.build()))
                .flatMap(isAdded -> reactiveRedisTemplate.opsForZSet()
                        .rank(USER_QUEUE_WAIT_KEY.formatted(queue), userId.toString()))
                .map(rank -> rank >= 0 ? rank + 1 : rank);
    }
}

 

UserQueueController

package me.progfrog.idol.flow.controller;

import lombok.RequiredArgsConstructor;
import me.progfrog.idol.flow.dto.RegisterUserResponse;
import me.progfrog.idol.flow.service.UserQueueService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/queue")
public class UserQueueController {

    private final UserQueueService userQueueService;

    /**
     * 사용자를 대기 큐에 등록
     *
     * @param queue 대기 큐 이름
     * @param userId 사용자 ID
     * @return 대기 번호가 담긴 dto
     */
    @PostMapping
    public Mono<RegisterUserResponse> registerUser(@RequestParam(name = "queue", defaultValue = "default") String queue,
                                                   @RequestParam(name = "user-id") Long userId) {

        return userQueueService.registerWaitQueue(queue, userId)
                .map(RegisterUserResponse::new);
    }
}

 

동작 확인

Postman

 

curl

 

레디스

zscan: sorted set에서 요소를 스캔하는 제공, 0은 탐색을 시작할 위치를 나타내는 커서값

monitor: Redis 서버에서 발생하는 모든 명령어를 실시간으로 관찰하는 기능을 제공

 

3. 예외 처리 개선

ApplicationException 추가

package me.progfrog.idol.flow.exception;

import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@AllArgsConstructor
@Getter
public class ApplicationException extends RuntimeException {

    private HttpStatus httpStatus;
    private String code;
    private String reason;
}

 

ErrorCode 추가

package me.progfrog.idol.flow.exception;

import lombok.AllArgsConstructor;
import org.springframework.http.HttpStatus;

@AllArgsConstructor
public enum ErrorCode {

    QUEUE_ALREADY_REGISTERED_USER(HttpStatus.CONFLICT, "UQ-0001", "이미 대기열에 등록된 사용자 입니다.");

    private final HttpStatus httpStatus;
    private final String code;
    private final String reason;

    public ApplicationException build() {
        return new ApplicationException(httpStatus, code, reason);
    }
}

 

ApplicationAdvice 추가

package me.progfrog.idol.flow.exception;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import reactor.core.publisher.Mono;

@RestControllerAdvice
public class ApplicationAdvice {

    @ExceptionHandler({
            ApplicationException.class
    })
    public Mono<ResponseEntity<ServerExceptionResponse>> applicationExceptionHandler(ApplicationException ex) {
        return Mono.just(ResponseEntity
                .status(ex.getHttpStatus())
                .body(new ServerExceptionResponse(ex.getCode(), ex.getReason())));
    }

    public record ServerExceptionResponse(
            String code,
            String reason
    ) {
    }
}

 

UserQueueService에 적용

public Mono<Long> registerWaitQueue(final String queue, final Long userId) {
    var unixTimestamp = Instant.now().getEpochSecond();
    return reactiveRedisTemplate.opsForZSet().add(USER_QUEUE_WAIT_KEY.formatted(queue), userId.toString(), unixTimestamp)
            .filter(i -> i)
            .switchIfEmpty(Mono.error(ErrorCode.QUEUE_ALREADY_REGISTERED_USER.build()))
            .flatMap(i -> reactiveRedisTemplate.opsForZSet().rank(USER_QUEUE_WAIT_KEY.formatted(queue), userId.toString()))
            .map(rank -> rank >= 0 ? rank + 1 : rank);
}

 

동작 확인

개선 전

개선 후

반응형