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);
}
동작 확인
개선 전
개선 후
반응형