From f1c14fc1729246136fcfedc6a831206354922b17 Mon Sep 17 00:00:00 2001
From: Lim JinKeon <0907john24@gmail.com>
Date: Wed, 6 May 2026 14:19:44 +0900
Subject: [PATCH 01/16] =?UTF-8?q?Refactor:=20application.yml=20=EC=84=A4?=
=?UTF-8?q?=EC=A0=95=20=EC=88=98=EC=A0=95,=20Spring=20Security=20=EC=84=A4?=
=?UTF-8?q?=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../config/QueueSecurityConfig.java | 31 +++++++++++++++++++
src/main/resources/application.yml | 29 +++++++++++++----
2 files changed, 54 insertions(+), 6 deletions(-)
create mode 100644 src/main/java/org/ticketing/queue/infrastructure/config/QueueSecurityConfig.java
diff --git a/src/main/java/org/ticketing/queue/infrastructure/config/QueueSecurityConfig.java b/src/main/java/org/ticketing/queue/infrastructure/config/QueueSecurityConfig.java
new file mode 100644
index 0000000..883c798
--- /dev/null
+++ b/src/main/java/org/ticketing/queue/infrastructure/config/QueueSecurityConfig.java
@@ -0,0 +1,31 @@
+package org.ticketing.queue.infrastructure.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.annotation.Order;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.web.SecurityFilterChain;
+
+@Configuration
+public class QueueSecurityConfig {
+
+ // MVP 통합 테스트용 임시 SecurityConfig
+ @Bean
+ @Order(1)
+ public SecurityFilterChain queueFilterChain(HttpSecurity http) throws Exception {
+ return http
+ .securityMatcher("/**")
+ .csrf(AbstractHttpConfigurer::disable)
+ .formLogin(AbstractHttpConfigurer::disable)
+ .httpBasic(AbstractHttpConfigurer::disable)
+ .sessionManagement(session ->
+ session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
+ )
+ .authorizeHttpRequests(auth -> auth
+ .anyRequest().permitAll()
+ )
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 8b4dc03..92c6228 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -3,10 +3,27 @@ spring:
name: queue-service
config:
- import: optional:configserver:${CONFIG_SERVER_URL:http://localhost:10002}
+ import: optional:configserver:http://localhost:10002
- cloud:
- config:
- fail-fast: false # config-server 없어도 로컬 기동 가능
- import-check:
- enabled: false
+ kafka:
+ bootstrap-servers: ${KAFKA_BOOTSTRAP_SERVERS:localhost:19092}
+ consumer:
+ group-id: queue-service
+ auto-offset-reset: latest
+ key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
+ value-deserializer: org.springframework.kafka.support.serializer.ErrorHandlingDeserializer
+ properties:
+ spring.deserializer.value.delegate.class: org.springframework.kafka.support.serializer.JsonDeserializer
+ spring.json.trusted.packages: "org.ticketing.queue.domain.event,java.util,java.lang"
+ spring.json.use.type.headers: false
+ spring.json.value.default.type: org.ticketing.queue.domain.event.MatchApprovedEvent
+
+management:
+ tracing:
+ enabled: false
+ sampling:
+ probability: 0.0
+ zipkin:
+ tracing:
+ export:
+ enabled: false
\ No newline at end of file
From e4fff743a3a6661c5eceb3507623a8748f3f28e1 Mon Sep 17 00:00:00 2001
From: Lim JinKeon <0907john24@gmail.com>
Date: Wed, 6 May 2026 15:05:09 +0900
Subject: [PATCH 02/16] =?UTF-8?q?Refactor:=20application-docker.yml=20?=
=?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/main/resources/application-docker.yml | 32 +++++++++++------------
1 file changed, 16 insertions(+), 16 deletions(-)
diff --git a/src/main/resources/application-docker.yml b/src/main/resources/application-docker.yml
index c9007c0..41e4395 100644
--- a/src/main/resources/application-docker.yml
+++ b/src/main/resources/application-docker.yml
@@ -1,3 +1,12 @@
+server:
+ tomcat:
+ threads:
+ max: 500
+ min-spare: 100
+ accept-count: 1000
+ max-connections: 8192
+ connection-timeout: 5000
+
spring:
config:
activate:
@@ -6,7 +15,7 @@ spring:
data:
redis:
host: ticketing-redis
- port: 6379
+ port: ${REDIS_PORT:6379}
timeout: 5000ms
lettuce:
shutdown-timeout: 100ms
@@ -19,9 +28,6 @@ spring:
hikari:
maximum-pool-size: 30
minimum-idle: 10
- connection-timeout: 3000
- idle-timeout: 600000
- max-lifetime: 1800000
jpa:
hibernate:
@@ -41,6 +47,9 @@ eureka:
defaultZone: ${EUREKA_DEFAULT_ZONE:http://eureka-server:10001/eureka/} # 도커 내부 서비스명
management:
+ metrics:
+ tags:
+ application: ${spring.application.name} # Grafana 필터링 기준
endpoints:
web:
exposure:
@@ -52,15 +61,6 @@ management:
tracing:
endpoint: ${ZIPKIN_URL:http://localhost:9411/api/v2/spans}
-# data:
-# redis:
-# password: ${REDIS_PASSWORD}
-# sentinel:
-# master: mymaster
-# nodes:
-# - redis-sentinel-1:26379
-# - redis-sentinel-2:26379
-# - redis-sentinel-3:26379
-# timeout: 5000ms
-# lettuce:
-# shutdown-timeout: 100ms
+queue:
+ token:
+ secret: ${QUEUE_TOKEN_SECRET}
From d2d7199b4ffd2e5e170e018ffa63ad1c44d6d39a Mon Sep 17 00:00:00 2001
From: Lim JinKeon <0907john24@gmail.com>
Date: Wed, 6 May 2026 21:36:17 +0900
Subject: [PATCH 03/16] =?UTF-8?q?Refactor:=20application-docker.yml=20?=
=?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/main/resources/application-docker.yml | 10 +++----
src/main/resources/logback.xml | 36 ++++++++++++++++++-----
2 files changed, 33 insertions(+), 13 deletions(-)
diff --git a/src/main/resources/application-docker.yml b/src/main/resources/application-docker.yml
index 41e4395..560a1f0 100644
--- a/src/main/resources/application-docker.yml
+++ b/src/main/resources/application-docker.yml
@@ -14,7 +14,7 @@ spring:
data:
redis:
- host: ticketing-redis
+ host: ${REDIS_HOST:ticketing-redis}
port: ${REDIS_PORT:6379}
timeout: 5000ms
lettuce:
@@ -31,7 +31,7 @@ spring:
jpa:
hibernate:
- ddl-auto: ${JPA_DDL_AUTO:update} # 도커는 update (create면 매번 초기화 위험)
+ ddl-auto: ${JPA_DDL_AUTO:update}
properties:
hibernate:
default_schema: ${QUEUE_SCHEMA:queue}
@@ -39,12 +39,12 @@ spring:
create_schemas: true
kafka:
- bootstrap-servers: kafka:9092
+ bootstrap-servers: ${SPRING_KAFKA_BOOTSTRAP_SERVERS}
eureka:
client:
service-url:
- defaultZone: ${EUREKA_DEFAULT_ZONE:http://eureka-server:10001/eureka/} # 도커 내부 서비스명
+ defaultZone: ${EUREKA_DEFAULT_ZONE:http://eureka-server:10001/eureka/}
management:
metrics:
@@ -56,7 +56,7 @@ management:
include: health,info,prometheus
tracing:
sampling:
- probability: 1.0
+ probability: 0.1
zipkin:
tracing:
endpoint: ${ZIPKIN_URL:http://localhost:9411/api/v2/spans}
diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml
index 565e304..08f7166 100644
--- a/src/main/resources/logback.xml
+++ b/src/main/resources/logback.xml
@@ -19,25 +19,45 @@
-
+
+
+
+ {"service":"queue-service"}
+
+ timestamp
+ message
+ logger
+ thread
+ level
+
+
+
+
+
logs/queue-service.log
-
logs/queue-service.%d{yyyy-MM-dd}.log
14
-
%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %thread %logger{36} [traceId=%X{traceId}] - %msg%n
UTF-8
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
From 7e64def2d74e9443ee4ecd4210f0e451eeafb6f2 Mon Sep 17 00:00:00 2001
From: Lim JinKeon <0907john24@gmail.com>
Date: Thu, 7 May 2026 12:47:26 +0900
Subject: [PATCH 04/16] =?UTF-8?q?Refactor:=20Dockerfild,=20.gitignore=20?=
=?UTF-8?q?=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.gitignore | 6 ++++++
Dockerfile | 24 ++++++++++--------------
2 files changed, 16 insertions(+), 14 deletions(-)
diff --git a/.gitignore b/.gitignore
index 4ff88f9..f7a7c5c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,9 @@
HELP.md
+README.md
+.git
+.gitignore
.gradle
+build
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
@@ -22,6 +26,7 @@ bin/
*.iws
*.iml
*.ipr
+out
out/
!**/src/main/**/out/
!**/src/test/**/out/
@@ -35,4 +40,5 @@ out/
/.nb-gradle/
### VS Code ###
+.vscode
.vscode/
diff --git a/Dockerfile b/Dockerfile
index d55aedb..458d952 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,27 +1,23 @@
-FROM eclipse-temurin:17-jdk AS build
+FROM gradle:8.12-jdk17 AS build
+
WORKDIR /app
ARG GPR_USER
ARG GPR_TOKEN
-COPY gradlew .
-COPY gradle gradle
-COPY build.gradle .
-COPY settings.gradle .
-
-RUN chmod +x gradlew
+COPY build.gradle settings.gradle ./
+COPY gradle ./gradle
-RUN ./gradlew dependencies --no-daemon \
- -PGPR_USER=${GPR_USER} \
- -PGPR_TOKEN=${GPR_TOKEN} || true
+RUN GPR_USER=${GPR_USER} GPR_TOKEN=${GPR_TOKEN} gradle dependencies --no-daemon || true
-COPY src src
+COPY src ./src
-RUN ./gradlew bootJar -x test --no-daemon \
- -PGPR_USER=${GPR_USER} \
- -PGPR_TOKEN=${GPR_TOKEN}
+RUN GPR_USER=${GPR_USER} GPR_TOKEN=${GPR_TOKEN} gradle bootJar --no-daemon -x test
FROM eclipse-temurin:17-jre
+
WORKDIR /app
+
COPY --from=build /app/build/libs/*.jar app.jar
+
ENTRYPOINT ["java", "-jar", "app.jar"]
\ No newline at end of file
From 386cec8bbc01bc44d8f15ecc140c27fae1cc3c74 Mon Sep 17 00:00:00 2001
From: Lim JinKeon <0907john24@gmail.com>
Date: Fri, 8 May 2026 10:32:48 +0900
Subject: [PATCH 05/16] =?UTF-8?q?Refactor:=20application-docker.yml=20?=
=?UTF-8?q?=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/main/resources/application-docker.yml | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/main/resources/application-docker.yml b/src/main/resources/application-docker.yml
index 560a1f0..e5a38ab 100644
--- a/src/main/resources/application-docker.yml
+++ b/src/main/resources/application-docker.yml
@@ -21,7 +21,7 @@ spring:
shutdown-timeout: 100ms
datasource:
- url: jdbc:postgresql://${DB_HOST:ticketing-postgres}:${DB_PORT:15432}/${DB_NAME:ticketing}?currentSchema=${QUEUE_SCHEMA:queue}
+ url: jdbc:postgresql://${DB_HOST:ticketing-postgres}:${DB_PORT:15432}/${DB_NAME:ticketing}?currentSchema=${DB_SCHEMA:queue}
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
driver-class-name: org.postgresql.Driver
@@ -34,7 +34,7 @@ spring:
ddl-auto: ${JPA_DDL_AUTO:update}
properties:
hibernate:
- default_schema: ${QUEUE_SCHEMA:queue}
+ default_schema: ${DB_SCHEMA:queue}
hbm2ddl:
create_schemas: true
@@ -49,7 +49,7 @@ eureka:
management:
metrics:
tags:
- application: ${spring.application.name} # Grafana 필터링 기준
+ application: ${spring.application.name}
endpoints:
web:
exposure:
From 8ec98eb95fd74b90ca34fb9ceb936edbf3bf08b5 Mon Sep 17 00:00:00 2001
From: Lim JinKeon <0907john24@gmail.com>
Date: Sun, 10 May 2026 00:25:41 +0900
Subject: [PATCH 06/16] =?UTF-8?q?Refactor:=20=EB=8C=80=EA=B8=B0=EC=97=B4?=
=?UTF-8?q?=20=EC=A7=84=EC=9E=85,=20SSE=20=EC=88=98=EC=8B=A0=20=EC=84=B1?=
=?UTF-8?q?=EB=8A=A5=20=ED=96=A5=EC=83=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../application/service/QueueService.java | 95 +++++++++----------
.../queue/domain/model/AcquireResult.java | 11 ++-
.../repository/QueueRedisRepository.java | 4 +-
.../infrastructure/config/SseConfig.java | 27 +++++-
.../persistence/QueueRedisRepositoryImpl.java | 39 +++++---
.../persistence/SseEmitterRepository.java | 32 ++++---
.../redis/pubsub/QueueRedisSubscriber.java | 43 +++++----
.../queue/infrastructure/util/RuaScript.java | 33 ++++---
src/main/resources/application-docker.yml | 20 +++-
9 files changed, 182 insertions(+), 122 deletions(-)
diff --git a/src/main/java/org/ticketing/queue/application/service/QueueService.java b/src/main/java/org/ticketing/queue/application/service/QueueService.java
index 4386ac5..0db3134 100644
--- a/src/main/java/org/ticketing/queue/application/service/QueueService.java
+++ b/src/main/java/org/ticketing/queue/application/service/QueueService.java
@@ -1,6 +1,5 @@
package org.ticketing.queue.application.service;
-import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
@@ -51,8 +50,6 @@ public class QueueService {
private final QueueRedisSubscriber queueRedisSubscriber;
private final BannedUserRepository bannedUserRepository;
- private final ObjectMapper objectMapper;
-
// SSE 타임아웃: 15분 (대기열 최대 대기 시간 기준)
private static final long SSE_TIMEOUT_MS = 15 * 60 * 1000L;
@@ -133,7 +130,6 @@ public SseEmitter subscribe(UUID matchId, UUID userId) {
LocalDateTime enteredAt = queueRedisRepository.getEnteredAt(matchId, userId);
queueHistoryService.record(matchId, userId, enteredAt, QueueExitReason.TIMEOUT);
sseEmitterRepository.remove(matchId, userId);
- emitter.complete();
});
emitter.onError(e -> {
log.warn("[SSE] 연결 에러. matchId={}, userId={}", matchId, userId);
@@ -142,36 +138,39 @@ public SseEmitter subscribe(UUID matchId, UUID userId) {
sseEmitterRepository.save(matchId, userId, emitter);
- // 토큰 보유 유저 → 즉시 토큰 전송 후 대기열 제거
- String existingToken = queueRedisRepository.getPassToken(matchId, userId);
- if (existingToken != null) {
- log.info("[SSE] 토큰 보유 유저 재접속. 즉시 토큰 전송. matchId={}, userId={}", matchId, userId);
- try {
- sendEvent(emitter, UserStatusResponse.ofIssued(existingToken));
- } catch (IOException e) {
- log.warn("[SSE] 토큰 즉시 전송 실패. matchId={}, userId={}", matchId, userId);
- } finally {
- queueRedisRepository.exit(matchId, userId); // 대기열 잔류 제거
- emitter.complete();
- sseEmitterRepository.remove(matchId, userId);
+ try {
+ // 토큰 보유 유저 → 즉시 토큰 전송 후 대기열 제거
+ String existingToken = queueRedisRepository.getPassToken(matchId, userId);
+ if (existingToken != null) {
+ log.info("[SSE] 토큰 보유 유저 재접속. 즉시 토큰 전송. matchId={}, userId={}", matchId, userId);
+ try {
+ sendEvent(emitter, UserStatusResponse.ofIssued(existingToken));
+ } catch (IOException e) {
+ log.warn("[SSE] 토큰 즉시 전송 실패. matchId={}, userId={}", matchId, userId);
+ } finally {
+ queueRedisRepository.exit(matchId, userId);
+ emitter.complete(); // onCompletion → remove()
+ }
+ return emitter;
}
- return emitter;
- }
- // 구독 즉시 슬롯 비교 → 범위 내면 바로 토큰 발급
- Long rank = queueRedisRepository.getRank(matchId, userId);
- Long totalCount = queueRedisRepository.getTotalCount(matchId);
- Long availableSlots = queueRedisRepository.getAvailableSlots(matchId);
+ // 구독 즉시 슬롯 비교 → 범위 내면 바로 토큰 발급
+ Long rank = queueRedisRepository.getRank(matchId, userId);
+ Long totalCount = queueRedisRepository.getTotalCount(matchId);
- if (rank != null && availableSlots != null && rank <= availableSlots) {
- // 슬롯 범위 내 → 즉시 토큰 발급 시도
queueRedisSubscriber.pushStatus(matchId, userId, emitter, rank, totalCount);
- } else {
- // 슬롯 범위 밖 → 현재 순위만 전송하고 대기
+
+ } catch (Exception e) {
+ log.error("[SSE] 구독 처리 중 예외 발생. matchId={}, userId={}", matchId, userId, e);
try {
- sendEvent(emitter, UserStatusResponse.ofWaiting(rank, totalCount));
- } catch (IOException e) {
- log.warn("[SSE] 초기 상태 전송 실패. matchId={}, userId={}", matchId, userId);
+ emitter.send(
+ SseEmitter.event()
+ .name("error")
+ .data("서버 오류가 발생했습니다.")
+ );
+ } catch (IOException ignored) {
+ emitter.completeWithError(e);
+ sseEmitterRepository.remove(matchId, userId);
}
}
@@ -191,23 +190,23 @@ public void pushStatusToAll() {
List userIds = sseEmitterRepository.findUserIdsByMatchId(matchId);
if (userIds.isEmpty()) continue;
- // Pipeline으로 모든 유저 순위 한 번에 조회
Map ranks = queueRedisRepository.getRankBatch(matchId, userIds);
- for (UUID userId : userIds) {
+ // parallel stream으로 변경
+ userIds.parallelStream().forEach(userId -> {
SseEmitter emitter = sseEmitterRepository.find(matchId, userId);
- if (emitter == null) continue;
+ if (emitter == null) return;
- try {
- // 순위 업데이트만 - 토큰 발급 로직 없음
- Long rank = ranks.get(userId);
- sendEvent(emitter, UserStatusResponse.ofWaiting(rank, totalCount));
- } catch (IOException e) {
- log.warn("[SSE] 순위 업데이트 전송 실패. matchId={}, userId={}", matchId, userId);
- sseEmitterRepository.remove(matchId, userId);
- emitter.completeWithError(e);
+ // rank == null, 이미 exit() 됐는데 emitter만 남은 경우 → 정리
+ Long rank = ranks.get(userId);
+ if (rank == null) {
+ emitter.complete(); // onCompletion → remove()
+ return;
}
- }
+
+ // 무조건 pushStatus → Lua 스크립트가 슬롯 판단
+ queueRedisSubscriber.pushStatus(matchId, userId, emitter, rank, totalCount);
+ });
}
}
@@ -217,13 +216,13 @@ private void sendEvent(SseEmitter emitter, UserStatusResponse response) throws I
emitter.send(
SseEmitter.event()
.name("queue-status")
- .data(objectMapper.writeValueAsString(response))
+ .data(response)
.id(String.valueOf(System.currentTimeMillis()))
.reconnectTime(3000)
);
} catch (IllegalStateException e) {
- // 이미 완료된 emitter → 무시
- log.warn("[SSE] 이미 완료된 emitter. 전송 스킵");
+ // 이미 완료된 emitter → IOException으로 변환해 호출부에서 처리
+ throw new IOException("Emitter already completed", e);
}
}
@@ -292,9 +291,7 @@ private void notifyRefreshAndCloseEmitters(UUID matchId) {
emitter.send(
SseEmitter.event()
.name("queue-refresh")
- .data(objectMapper.writeValueAsString(
- UserStatusResponse.ofRefreshed()
- ))
+ .data(UserStatusResponse.ofRefreshed())
.id(String.valueOf(System.currentTimeMillis()))
);
} catch (IOException e) {
@@ -376,9 +373,7 @@ private void notifyBannedAndCloseEmitter(UUID matchId, UUID userId) {
emitter.send(
SseEmitter.event()
.name("queue-banned")
- .data(objectMapper.writeValueAsString(
- UserStatusResponse.ofBanned()
- ))
+ .data(UserStatusResponse.ofBanned())
.id(String.valueOf(System.currentTimeMillis()))
);
} catch (IOException e) {
diff --git a/src/main/java/org/ticketing/queue/domain/model/AcquireResult.java b/src/main/java/org/ticketing/queue/domain/model/AcquireResult.java
index 6cdb081..8dbd08b 100644
--- a/src/main/java/org/ticketing/queue/domain/model/AcquireResult.java
+++ b/src/main/java/org/ticketing/queue/domain/model/AcquireResult.java
@@ -1,9 +1,10 @@
package org.ticketing.queue.domain.model;
public enum AcquireResult {
- SUCCESS, // 1: 슬롯+토큰 선점 성공
- NO_SLOT, // -1: 슬롯 없음
- NOT_INITIALIZED, // -2: 슬롯 미초기화
- PENDING, // -3: 다른 스레드 발급 중
- ALREADY_ISSUED // -4: 이미 발급 완료
+ SUCCESS, // 1: 슬롯+토큰 선점 성공
+ NO_SLOT, // -1: 슬롯 없음
+ NOT_INITIALIZED, // -2: 슬롯 미초기화
+ PENDING, // -3: 다른 스레드 발급 중
+ ALREADY_ISSUED, // -4: 이미 발급 완료
+ USER_NOT_IN_QUEUE // -5: 유저가 이미 대기열에서 제거됨 (ban/refresh/rollback 등)
}
diff --git a/src/main/java/org/ticketing/queue/domain/repository/QueueRedisRepository.java b/src/main/java/org/ticketing/queue/domain/repository/QueueRedisRepository.java
index b7eb84b..fdb7158 100644
--- a/src/main/java/org/ticketing/queue/domain/repository/QueueRedisRepository.java
+++ b/src/main/java/org/ticketing/queue/domain/repository/QueueRedisRepository.java
@@ -1,6 +1,6 @@
package org.ticketing.queue.domain.repository;
-import org.ticketing.queue.domain.model.AcquireResult;
+import org.ticketing.queue.domain.model.SlotAcquire;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
@@ -36,7 +36,7 @@ public interface QueueRedisRepository {
void releaseSlot(UUID matchId);
- AcquireResult acquireSlotAndToken(UUID matchId, UUID userId);
+ SlotAcquire acquireSlotAndToken(UUID matchId, UUID userId);
// ── 통과 토큰 관리 ───────────────────────────────────────────────────
diff --git a/src/main/java/org/ticketing/queue/infrastructure/config/SseConfig.java b/src/main/java/org/ticketing/queue/infrastructure/config/SseConfig.java
index 2ae2cc7..13c904a 100644
--- a/src/main/java/org/ticketing/queue/infrastructure/config/SseConfig.java
+++ b/src/main/java/org/ticketing/queue/infrastructure/config/SseConfig.java
@@ -2,24 +2,41 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
-import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+import java.util.concurrent.ThreadPoolExecutor;
+
@Configuration
-@EnableAsync
@EnableScheduling // @Scheduled 활성화
public class SseConfig implements WebMvcConfigurer {
/**
- * SSE는 비동기 요청 처리 방식이므로 MVC async timeout 설정 필요
- * 기본값(30초)을 SSE 타임아웃보다 충분히 크게 설정
+ * SSE 비동기 요청 MVC timeout 설정 (30분)
*/
@Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
- configurer.setDefaultTimeout(30 * 60 * 1000L); // 30분
+ configurer.setDefaultTimeout(15 * 60 * 1000L);
+ configurer.setTaskExecutor(mvcAsyncExecutor());
+ }
+
+ /**
+ * MVC Async 전용 executor (SSE 요청 처리)
+ */
+ @Bean
+ public ThreadPoolTaskExecutor mvcAsyncExecutor() {
+ ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+ executor.setCorePoolSize(500);
+ executor.setMaxPoolSize(1000);
+ executor.setQueueCapacity(2000);
+ executor.setKeepAliveSeconds(60);
+ executor.setThreadNamePrefix("mvc-async-");
+ executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
+ executor.initialize();
+ return executor;
}
/**
diff --git a/src/main/java/org/ticketing/queue/infrastructure/persistence/QueueRedisRepositoryImpl.java b/src/main/java/org/ticketing/queue/infrastructure/persistence/QueueRedisRepositoryImpl.java
index addac85..bde7aa0 100644
--- a/src/main/java/org/ticketing/queue/infrastructure/persistence/QueueRedisRepositoryImpl.java
+++ b/src/main/java/org/ticketing/queue/infrastructure/persistence/QueueRedisRepositoryImpl.java
@@ -11,6 +11,7 @@
import org.ticketing.queue.domain.exception.*;
import org.ticketing.queue.domain.model.AcquireResult;
import org.ticketing.queue.domain.model.Queue;
+import org.ticketing.queue.domain.model.SlotAcquire;
import org.ticketing.queue.domain.repository.QueueRedisRepository;
import org.ticketing.queue.domain.repository.QueueRepository;
@@ -211,34 +212,46 @@ public void releaseSlot(UUID matchId) {
}
/**
- * 결과에 따라 분기만 처리
- * 원자적으로 슬롯+토큰 동시 획득
+ * 원자적으로 슬롯+토큰 선점 및 enteredAt 조회
+ * Lua 스크립트가 enteredAt 존재 여부를 슬롯 획득 전에 체크하므로
+ * acquireSlot SUCCESS 이후 별도 getEnteredAt() 호출 불필요
*/
@Override
- public AcquireResult acquireSlotAndToken(UUID matchId, UUID userId) {
+ @SuppressWarnings("unchecked")
+ public SlotAcquire acquireSlotAndToken(UUID matchId, UUID userId) {
String tokenKey = getPassTokenKey(matchId, userId);
String availableKey = SLOTS_AVAILABLE_KEY.formatted(matchId);
+ String enteredAtKey = getEnteredAtKey(matchId);
long ttlSeconds = TOKEN_TTL_MINUTES * 60;
- Long result = redisTemplate.execute(
+ List