Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEAT] 대기열 구현 #32

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'

//jwt
implementation 'io.jsonwebtoken:jjwt-api:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5'


compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'com.mysql:mysql-connector-j'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
@EnableJpaAuditing
@SpringBootApplication
public class BookYourSeatApplication {

public static void main(String[] args) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example.book_your_seat.common.util;

public final class JwtConst {
private JwtConst() {}
public static final String INVALID_JWT = "유효하지 않은 JWT 토큰입니다.";
public static final String EXPIRED_JWT = "만료된 JWT 토큰입니다.";
public static final String UNSUPPORTED_JWT = "지원하지 않는 JWT 토큰 형식입니다.";
public static final String EMPTY_JWT = "JWT 토큰이 없습니다.";
}
69 changes: 69 additions & 0 deletions src/main/java/com/example/book_your_seat/common/util/JwtUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.example.book_your_seat.common.util;

import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.security.SignatureException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Date;

import static com.example.book_your_seat.common.util.JwtConst.*;

@Slf4j
@Component
public class JwtUtil {
private final SecretKey secretKey;
private final Integer expirationTime;
private static final String SIGNATURE_ALGORITHM = Jwts.SIG.HS256.key().build().getAlgorithm();

JwtUtil(@Value("${jwt.secret}") String secretKey, @Value("${jwt.expiration_time}") Integer expirationTime) {
this.secretKey = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), SIGNATURE_ALGORITHM);
this.expirationTime = expirationTime;
}
Comment on lines +23 to +30
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사실 저는 무슨 auth0-jwt 였나 하는 라이브러리만 써보고
jjwt 라이브러리를 처음 써봐서 엄청 엉성한데 한 수 배워갑니다.


public String createJwt(Long userId) {
final Instant now = Instant.now();
final Instant expiredAt = now.plusSeconds(expirationTime);

return Jwts.builder()
.claim("userId", userId.toString())
.issuedAt(Date.from(now))
.expiration(Date.from(expiredAt))
.signWith(secretKey)
.compact();
}

public boolean validateToken(String token) {
try {
Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token);
return true;
} catch (MalformedJwtException e) {
throw new IllegalStateException(INVALID_JWT);
} catch (ExpiredJwtException e) {
throw new IllegalStateException(EXPIRED_JWT);
} catch (UnsupportedJwtException | SignatureException e) {
throw new IllegalStateException(UNSUPPORTED_JWT);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(EMPTY_JWT);
}
}

public Long getUserIdByToken(String token) {
String userId = Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload()
.get("userId", String.class);

return Long.parseLong(userId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connec
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setValueSerializer(new StringRedisSerializer());
return template;
}
}
Expand Down
16 changes: 16 additions & 0 deletions src/main/java/com/example/book_your_seat/queue/QueueConst.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.example.book_your_seat.queue;

public final class QueueConst {
public static final String PROCESSING = "PROCESSING";
public static final String WAITING = "WAITING";
public static final String NOT_IN_QUEUE = "NOT_IN_QUEUE";

public static final Integer PROCESSING_QUEUE_SIZE = 1000;
public static final Integer PROCESSING_TOKEN_EXPIRATION_TIME = 30 * 60 * 1000; //30분
public static final Integer WAITING_TOKEN_EXPIRATION_TIME = 60 * 60 * 1000; //1시간

public static final String PROCESSING_QUEUE_KEY = "processing_queue_key";
public static final String WAITING_QUEUE_KEY = "waiting_queue_key";
public static final String ALREADY_ISSUED_USER = "이미 토큰을 발급받은 유저입니다.";

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.example.book_your_seat.queue.controller;

import com.example.book_your_seat.queue.controller.dto.QueueResponse;
import com.example.book_your_seat.queue.controller.dto.TokenResponse;
import com.example.book_your_seat.queue.service.QueueCommandService;
import com.example.book_your_seat.queue.service.QueueQueryService;
import com.example.book_your_seat.queue.service.facade.QueueService;
import com.example.book_your_seat.user.domain.User;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import static com.example.book_your_seat.common.SessionConst.LOGIN_USER;

@RestController
@RequestMapping("/api/v1/queues")
@RequiredArgsConstructor
public class QueueController {

private final QueueService queueService;

@PostMapping("/token")
public ResponseEntity<TokenResponse> issueTokenAndEnqueue(@SessionAttribute(LOGIN_USER) User user) {
return ResponseEntity
.status(HttpStatus.CREATED)
.body(queueService.issueTokenAndEnqueue(user.getId()));
}

@GetMapping("/queue")
public ResponseEntity<QueueResponse> getQueueInfoWithToken(@SessionAttribute(LOGIN_USER) User user,
@RequestParam("token") String token) {
return ResponseEntity.ok()
.body(queueService.findQueueStatus(user.getId(), token));
}

@PostMapping("/quit")
public ResponseEntity<Void> dequeueWaitingQueue(@SessionAttribute(LOGIN_USER) User user,
@RequestParam("token") String token) {
queueService.dequeueWaitingQueue(user.getId(), token);
return ResponseEntity.ok(null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.example.book_your_seat.queue.controller.dto;

public record QueueResponse(
String status,
Integer waitingQueueCount
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.example.book_your_seat.queue.controller.dto;

public record TokenResponse(
String token
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package com.example.book_your_seat.queue.repository;

import jakarta.annotation.Resource;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Repository;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import static com.example.book_your_seat.queue.QueueConst.*;

@Repository
@RequiredArgsConstructor
public class QueueRedisRepository implements Serializable {

@Resource(name = "redisTemplate")
private ZSetOperations<String, String> zSet;


public void enqueueProcessingQueue(Long userId, String token) {
String value = generateValue(userId, token);
zSet.add(PROCESSING_QUEUE_KEY, value, System.currentTimeMillis() + PROCESSING_TOKEN_EXPIRATION_TIME);
}

public void enqueueWaitingQueue(Long userId, String token) {
String value = generateValue(userId, token);
zSet.add(WAITING_QUEUE_KEY, value, System.currentTimeMillis() + WAITING_TOKEN_EXPIRATION_TIME);
}

/*
바로 Processing 가능한지 여부 반환
*/
public boolean isProcessableNow() {
Long pqSize = zSet.zCard(PROCESSING_QUEUE_KEY);
Long wqSize = zSet.zCard(WAITING_QUEUE_KEY);
if (pqSize == null) pqSize = 0L;
if (wqSize == null) wqSize = 0L;

return pqSize < PROCESSING_QUEUE_SIZE && wqSize == 0;
}

public boolean isInProcessingQueue(Long userId) {
Set<String> values = zSet.range(PROCESSING_QUEUE_KEY, 0, -1);
return values.stream().anyMatch(
value -> value.matches(userId + ":.*")
);
}

public boolean isInWaitingQueue(Long userId) {
Set<String> values = zSet.range(WAITING_QUEUE_KEY, 0, -1);
return values.stream().anyMatch(
value -> value.matches(userId + ":.*")
);
}

/*
몇번째로 대기하고 있는지를 반환
*/
public Integer getWaitingQueuePosition(Long userId, String token) {
String value = generateValue(userId, token);
Long rank = zSet.rank(WAITING_QUEUE_KEY, value);
return (rank == null) ? null : rank.intValue() + 1;
}

/*
대기열에 존재 인원 수 반환
*/
public Integer getProcessingQueueCount() {
Long count = zSet.zCard(PROCESSING_QUEUE_KEY);
return (count == null) ? 0 : count.intValue();
}

/*
업데이트 되어야 하는 인원을 반환
*/
public List<String> getFrontTokensFromWaitingQueue(int count) {
Set<String> tokens = zSet.range(WAITING_QUEUE_KEY, 0, count - 1);
return new ArrayList<>(tokens);
}

/*
대기열에서 처리열로 이동
*/
public void updateWaitingToProcessing(String value) {
zSet.remove(WAITING_QUEUE_KEY, value);
zSet.add(PROCESSING_QUEUE_KEY, value, System.currentTimeMillis() + PROCESSING_TOKEN_EXPIRATION_TIME);
}

/*
웨이팅 취소
*/
public void removeTokenInWaitingQueue(Long userId, String token) {
String value = generateValue(userId, token);
zSet.remove(WAITING_QUEUE_KEY, value);
}

/*
유효시간 만료 토큰 삭제
*/
public void removeExpiredToken(Long currentTime) {
zSet.removeRangeByScore(PROCESSING_QUEUE_KEY, Double.NEGATIVE_INFINITY, (double) currentTime);
zSet.removeRangeByScore(WAITING_QUEUE_KEY, Double.NEGATIVE_INFINITY, (double) currentTime);
}

/*
처리 완료된 토큰 삭제
*/
public void removeProcessingToken(Long userId) {
Set<String> values = zSet.range(PROCESSING_QUEUE_KEY, 0, -1);

values.stream()
.filter(value -> value.matches(userId + ":.*"))
.findFirst()
.ifPresent(value -> zSet.remove(PROCESSING_QUEUE_KEY, value));
}

private String generateValue(Long userId, String token) {
return userId.toString() + ":" + token;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.example.book_your_seat.queue.scheduler;

import com.example.book_your_seat.queue.service.QueueCommandService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Slf4j
@Component
@RequiredArgsConstructor
public class QueueScheduler {
private final QueueCommandService queueCommandService;

@Scheduled(fixedRate = 30 * 1000) //30초마다
public void updateWaitingToProcessing() {
queueCommandService.removeExpiredToken();
queueCommandService.updateWaitingToProcessing();
log.info("processing queue 가 update 되었습니다.");
}
Comment on lines +15 to +20
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 만료된 토큰 제거는 생각하지도 않았네요 ㅜㅠㅠㅠ

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.example.book_your_seat.queue.service;

public interface QueueCommandService {
String issueTokenAndEnqueue(Long userId);

void updateWaitingToProcessing();

void removeExpiredToken();

void removeTokenInWaitingQueue(Long userId, String token);

void completeProcessingToken(Long userId);
}
Loading
Loading