From 7b97738ccdfa3f0f897da5a7a2ddd00a0076e2e4 Mon Sep 17 00:00:00 2001 From: rlaxxwls13 Date: Mon, 18 May 2026 04:43:06 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=EB=8C=80=EA=B8=B0=EC=97=B4=20?= =?UTF-8?q?=EC=A7=84=EC=9E=85=20=EC=8B=9C=20=ED=99=9C=EC=84=B1=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EB=9E=A8=20=EA=B2=80=EC=A6=9D=20+=20?= =?UTF-8?q?=EC=8A=A4=EC=BC=80=EC=A4=84=EB=9F=AC=20=ED=99=9C=EC=84=B1=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=EA=B7=B8=EB=9E=A8=20=ED=8C=90=EB=8B=A8=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/ProgramMetaRepository.java | 5 + .../application/QueueTokenService.java | 28 ++++- .../domain/QueueTokenRepository.java | 5 - .../exception/ProgramNotActiveException.java | 20 ++++ .../exception/ProgramNotFoundException.java | 19 ++++ .../domain/exception/QueueErrorCode.java | 2 + .../redis/RedisQueueTokenRepository.java | 100 +++++++++--------- .../scheduler/AdmissionScheduler.java | 14 ++- .../redis/RedisQueueTokenRepositoryTest.java | 63 ----------- 9 files changed, 132 insertions(+), 124 deletions(-) create mode 100644 src/main/java/com/firstticket/queueservice/queuetoken/domain/exception/ProgramNotActiveException.java create mode 100644 src/main/java/com/firstticket/queueservice/queuetoken/domain/exception/ProgramNotFoundException.java diff --git a/src/main/java/com/firstticket/queueservice/programmeta/domain/ProgramMetaRepository.java b/src/main/java/com/firstticket/queueservice/programmeta/domain/ProgramMetaRepository.java index 7e22090..89d683e 100644 --- a/src/main/java/com/firstticket/queueservice/programmeta/domain/ProgramMetaRepository.java +++ b/src/main/java/com/firstticket/queueservice/programmeta/domain/ProgramMetaRepository.java @@ -5,6 +5,7 @@ import java.time.LocalDateTime; import java.util.List; import java.util.Optional; +import java.util.UUID; /** * ProgramMeta 영속화 인터페이스. @@ -19,6 +20,10 @@ public interface ProgramMetaRepository { Optional findById(ProgramId programId); + default Optional findById(UUID programId) { + return findById(ProgramId.of(programId)); + } + List findAll(); void deleteById(ProgramId programId); diff --git a/src/main/java/com/firstticket/queueservice/queuetoken/application/QueueTokenService.java b/src/main/java/com/firstticket/queueservice/queuetoken/application/QueueTokenService.java index a759550..03e938f 100644 --- a/src/main/java/com/firstticket/queueservice/queuetoken/application/QueueTokenService.java +++ b/src/main/java/com/firstticket/queueservice/queuetoken/application/QueueTokenService.java @@ -1,19 +1,21 @@ package com.firstticket.queueservice.queuetoken.application; +import com.firstticket.queueservice.programmeta.domain.ProgramMeta; +import com.firstticket.queueservice.programmeta.domain.ProgramMetaRepository; import com.firstticket.queueservice.queuetoken.application.dto.CancelQueueTokenCommand; import com.firstticket.queueservice.queuetoken.application.dto.GetQueueTokenQuery; import com.firstticket.queueservice.queuetoken.application.dto.IssueQueueTokenCommand; import com.firstticket.queueservice.queuetoken.application.dto.QueueTokenResult; import com.firstticket.queueservice.queuetoken.domain.QueueToken; import com.firstticket.queueservice.queuetoken.domain.QueueTokenRepository; -import com.firstticket.queueservice.queuetoken.domain.exception.DuplicateTokenException; -import com.firstticket.queueservice.queuetoken.domain.exception.InvalidTokenStateException; -import com.firstticket.queueservice.queuetoken.domain.exception.TokenNotFoundException; +import com.firstticket.queueservice.queuetoken.domain.exception.*; import com.firstticket.queueservice.queuetoken.domain.vo.ProgramId; import com.firstticket.queueservice.queuetoken.domain.vo.UserId; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import java.time.LocalDateTime; + /** * 대기열 진입 / 조회 / 취소를 처리하는 서비스. */ @@ -22,6 +24,7 @@ public class QueueTokenService { private final QueueTokenRepository queueTokenRepository; + private final ProgramMetaRepository programMetaRepository; /** * 대기열에 진입한다. @@ -36,7 +39,10 @@ public QueueTokenResult issueToken(IssueQueueTokenCommand command) { UserId userId = command.userId(); ProgramId programId = command.programId(); - // 같은 user+program 토큰이 있으면 폐기 후 새로 발급 + // 1. 활성 프로그램 검증 + validateProgramActive(programId); + + // 2. 같은 user+program 토큰이 있으면 폐기 후 새로 발급 queueTokenRepository.findByUserIdAndProgramId(userId, programId) .ifPresent(queueTokenRepository::delete); @@ -87,4 +93,18 @@ public void cancelToken(CancelQueueTokenCommand command) { queueTokenRepository.delete(token); } + + /** + * ProgramMeta 로 활성 프로그램 검증. + * + *

존재 여부 + 시간 + status 를 확인한다.

+ */ + private void validateProgramActive(ProgramId programId) { + ProgramMeta programMeta = programMetaRepository.findById(programId.id()) + .orElseThrow(ProgramNotFoundException::new); + + if (!programMeta.isActiveAt(LocalDateTime.now())) { + throw new ProgramNotActiveException(); + } + } } diff --git a/src/main/java/com/firstticket/queueservice/queuetoken/domain/QueueTokenRepository.java b/src/main/java/com/firstticket/queueservice/queuetoken/domain/QueueTokenRepository.java index 74cc35b..3b94d49 100644 --- a/src/main/java/com/firstticket/queueservice/queuetoken/domain/QueueTokenRepository.java +++ b/src/main/java/com/firstticket/queueservice/queuetoken/domain/QueueTokenRepository.java @@ -55,11 +55,6 @@ public interface QueueTokenRepository { */ void admit(QueueToken token); - /** - * 현재 큐가 존재하는 모든 프로그램 ID 를 조회한다. - */ - List findActiveProgramIds(); - /** * 특정 프로그램의 모든 대기 / 입장 토큰을 삭제한다. * Program 이 취소되었을 때 호출. diff --git a/src/main/java/com/firstticket/queueservice/queuetoken/domain/exception/ProgramNotActiveException.java b/src/main/java/com/firstticket/queueservice/queuetoken/domain/exception/ProgramNotActiveException.java new file mode 100644 index 0000000..2e5545c --- /dev/null +++ b/src/main/java/com/firstticket/queueservice/queuetoken/domain/exception/ProgramNotActiveException.java @@ -0,0 +1,20 @@ +package com.firstticket.queueservice.queuetoken.domain.exception; + +import com.firstticket.common.exception.BusinessException; + +/** + * 현재 시점이 프로그램의 입장 가능 시간이 아닐 때. + * + *

본질: + *

    + *
  • openAt 전
  • + *
  • closeAt 후
  • + *
  • CANCELLED 상태
  • + *
+ */ +public class ProgramNotActiveException extends BusinessException { + + public ProgramNotActiveException() { + super(QueueErrorCode.PROGRAM_NOT_ACTIVE); + } +} diff --git a/src/main/java/com/firstticket/queueservice/queuetoken/domain/exception/ProgramNotFoundException.java b/src/main/java/com/firstticket/queueservice/queuetoken/domain/exception/ProgramNotFoundException.java new file mode 100644 index 0000000..fae998d --- /dev/null +++ b/src/main/java/com/firstticket/queueservice/queuetoken/domain/exception/ProgramNotFoundException.java @@ -0,0 +1,19 @@ +package com.firstticket.queueservice.queuetoken.domain.exception; + +import com.firstticket.common.exception.BusinessException; + +/** + * 입장하려는 프로그램이 ProgramMeta 캐시에 존재하지 않을 때. + * + *

가능한 본질: + *

    + *
  • program.created 이벤트 도착 전
  • + *
  • 존재하지 않는 programId 로 입장 시도
  • + *
+ */ +public class ProgramNotFoundException extends BusinessException { + + public ProgramNotFoundException() { + super(QueueErrorCode.PROGRAM_NOT_FOUND); + } +} diff --git a/src/main/java/com/firstticket/queueservice/queuetoken/domain/exception/QueueErrorCode.java b/src/main/java/com/firstticket/queueservice/queuetoken/domain/exception/QueueErrorCode.java index bf70cb5..cd0c33d 100644 --- a/src/main/java/com/firstticket/queueservice/queuetoken/domain/exception/QueueErrorCode.java +++ b/src/main/java/com/firstticket/queueservice/queuetoken/domain/exception/QueueErrorCode.java @@ -8,6 +8,8 @@ @Getter @RequiredArgsConstructor public enum QueueErrorCode implements ErrorCode { + PROGRAM_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 프로그램입니다"), + PROGRAM_NOT_ACTIVE(HttpStatus.UNPROCESSABLE_ENTITY, "현재 입장 가능 시간이 아닙니다"), INVALID_TOKEN_STATE(HttpStatus.BAD_REQUEST, "대기 토큰 상태 전이 규칙을 위반했습니다"), DUPLICATE_TOKEN(HttpStatus.CONFLICT, "이미 대기 중인 토큰이 있습니다"), TOKEN_NOT_FOUND(HttpStatus.NOT_FOUND, "토큰을 찾을 수 없습니다"); diff --git a/src/main/java/com/firstticket/queueservice/queuetoken/infrastructure/redis/RedisQueueTokenRepository.java b/src/main/java/com/firstticket/queueservice/queuetoken/infrastructure/redis/RedisQueueTokenRepository.java index 971e593..7ff0b3b 100644 --- a/src/main/java/com/firstticket/queueservice/queuetoken/infrastructure/redis/RedisQueueTokenRepository.java +++ b/src/main/java/com/firstticket/queueservice/queuetoken/infrastructure/redis/RedisQueueTokenRepository.java @@ -339,55 +339,57 @@ public List execute(RedisOperations operations) { }); } - /** - * Redis 기반 findActiveProgramIds 구현. - * - *

{@code queue:program:*} 패턴으로 SCAN 하여 활성 프로그램 ID 목록을 반환한다. - * - *

SCAN 사용 이유: KEYS 명령은 production 에서 블로킹 발생 위험. SCAN 은 점진적. - * - *

Future Work

- * 본 메서드는 MVP 의 가정 (큐 존재 = 활성 프로그램) 을 따른다. - *

program-service 와 Kafka 이벤트 통합 후엔: - *

    - *
  • {@code program.opened} 이벤트 → Redis Set 의 활성 프로그램 추가
  • - *
  • {@code program.closed} 이벤트 → Set 에서 제거
  • - *
  • 본 메서드는 Redis Set 직접 조회 (SCAN 불필요)
  • - *
- */ - public List findActiveProgramIds() { - String pattern = QUEUE_KEY_PREFIX + PROGRAM_KEY_PREFIX + "*"; - - // 1. SCAN 으로 모든 큐 키 수집 - Set keys = redisTemplate.execute((RedisCallback>) connection -> { - Set result = new HashSet<>(); - ScanOptions options = ScanOptions.scanOptions() - .match(pattern) // queue:program:* 매칭 - .count(100) // 한 번에 100 개씩 점진적 조회 - .build(); - - // try-with-resources 로 cursor 자동 정리 - try (Cursor cursor = connection.scan(options)) { - while (cursor.hasNext()) { - // Redis 는 byte[] 반환 → UTF-8 문자열로 변환 - result.add(new String(cursor.next(), StandardCharsets.UTF_8)); - } - } - return result; - }); - - if (keys == null || keys.isEmpty()) { - return List.of(); - } - - // 2. 키에서 UUID 추출하여 ProgramId 로 변환 - // 예: "queue:program:abc-123" → "abc-123" → ProgramId.of("abc-123") - String prefix = QUEUE_KEY_PREFIX + PROGRAM_KEY_PREFIX; - return keys.stream() - .map(key -> key.substring(prefix.length())) - .map(ProgramId::fromString) - .toList(); - } +// program-service 와 kafka 이벤트 통합 후 교체 완료 +// +// /** +// * Redis 기반 findActiveProgramIds 구현. +// * +// *

{@code queue:program:*} 패턴으로 SCAN 하여 활성 프로그램 ID 목록을 반환한다. +// * +// *

SCAN 사용 이유: KEYS 명령은 production 에서 블로킹 발생 위험. SCAN 은 점진적. +// * +// *

Future Work

+// * 본 메서드는 MVP 의 가정 (큐 존재 = 활성 프로그램) 을 따른다. +// *

program-service 와 Kafka 이벤트 통합 후엔: +// *

    +// *
  • {@code program.opened} 이벤트 → Redis Set 의 활성 프로그램 추가
  • +// *
  • {@code program.closed} 이벤트 → Set 에서 제거
  • +// *
  • 본 메서드는 Redis Set 직접 조회 (SCAN 불필요)
  • +// *
+// */ +// public List findActiveProgramIds() { +// String pattern = QUEUE_KEY_PREFIX + PROGRAM_KEY_PREFIX + "*"; +// +// // 1. SCAN 으로 모든 큐 키 수집 +// Set keys = redisTemplate.execute((RedisCallback>) connection -> { +// Set result = new HashSet<>(); +// ScanOptions options = ScanOptions.scanOptions() +// .match(pattern) // queue:program:* 매칭 +// .count(100) // 한 번에 100 개씩 점진적 조회 +// .build(); +// +// // try-with-resources 로 cursor 자동 정리 +// try (Cursor cursor = connection.scan(options)) { +// while (cursor.hasNext()) { +// // Redis 는 byte[] 반환 → UTF-8 문자열로 변환 +// result.add(new String(cursor.next(), StandardCharsets.UTF_8)); +// } +// } +// return result; +// }); +// +// if (keys == null || keys.isEmpty()) { +// return List.of(); +// } +// +// // 2. 키에서 UUID 추출하여 ProgramId 로 변환 +// // 예: "queue:program:abc-123" → "abc-123" → ProgramId.of("abc-123") +// String prefix = QUEUE_KEY_PREFIX + PROGRAM_KEY_PREFIX; +// return keys.stream() +// .map(key -> key.substring(prefix.length())) +// .map(ProgramId::fromString) +// .toList(); +// } /** * Redis 기반 deleteAllByProgramId 구현. diff --git a/src/main/java/com/firstticket/queueservice/queuetoken/infrastructure/scheduler/AdmissionScheduler.java b/src/main/java/com/firstticket/queueservice/queuetoken/infrastructure/scheduler/AdmissionScheduler.java index 2632e5e..78079af 100644 --- a/src/main/java/com/firstticket/queueservice/queuetoken/infrastructure/scheduler/AdmissionScheduler.java +++ b/src/main/java/com/firstticket/queueservice/queuetoken/infrastructure/scheduler/AdmissionScheduler.java @@ -1,5 +1,6 @@ package com.firstticket.queueservice.queuetoken.infrastructure.scheduler; +import com.firstticket.queueservice.programmeta.domain.ProgramMetaRepository; import com.firstticket.queueservice.queuetoken.config.QueueProperties; import com.firstticket.queueservice.queuetoken.domain.QueueToken; import com.firstticket.queueservice.queuetoken.domain.QueueTokenRepository; @@ -10,6 +11,7 @@ import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; +import java.time.LocalDateTime; import java.util.List; /** @@ -20,7 +22,8 @@ * *

흐름: *

    - *
  1. Redis SCAN 으로 활성 프로그램 (큐가 존재하는 프로그램) 발견
  2. + *
  3. programmeta Aggregate 에서 활성 프로그램 조회 + * (CANCELLED 아니고 openAt ~ closeAt 사이)
  4. *
  5. 각 프로그램의 큐 앞에서 batchSize 명 조회 (Sorted Set ZRANGE)
  6. *
  7. 각 토큰에 대해 JWT 입장 토큰 발급 + 도메인 상태 전이 + Redis 영속성
  8. *
@@ -28,7 +31,6 @@ *

MVP 단계 한계: *

    *
  • 단일 인스턴스 가정 — 여러 인스턴스에서 동시 실행 시 race 가능 (v0.2.0 별도 이슈)
  • - *
  • 활성 프로그램 발견을 Redis SCAN 에 의존 — program-service 통합 후 변경
  • *
*/ @Slf4j @@ -37,6 +39,7 @@ public class AdmissionScheduler { private final QueueTokenRepository queueTokenRepository; + private final ProgramMetaRepository programMetaRepository; private final EntryTokenIssuer entryTokenIssuer; private final QueueProperties queueProperties; @@ -47,7 +50,12 @@ public class AdmissionScheduler { */ @Scheduled(fixedRate = 5000) public void admit() { - List activePrograms = queueTokenRepository.findActiveProgramIds(); + // programmeta Aggregate 에서 활성 프로그램 조회 → queuetoken 의 ProgramId 로 변환 + List activePrograms = programMetaRepository + .findActiveProgramIds(LocalDateTime.now()) + .stream() + .map(metaId -> ProgramId.of(metaId.id())) + .toList(); if (activePrograms.isEmpty()) { return; diff --git a/src/test/java/com/firstticket/queueservice/queuetoken/infrastructure/redis/RedisQueueTokenRepositoryTest.java b/src/test/java/com/firstticket/queueservice/queuetoken/infrastructure/redis/RedisQueueTokenRepositoryTest.java index 5741c7f..b32c6f8 100644 --- a/src/test/java/com/firstticket/queueservice/queuetoken/infrastructure/redis/RedisQueueTokenRepositoryTest.java +++ b/src/test/java/com/firstticket/queueservice/queuetoken/infrastructure/redis/RedisQueueTokenRepositoryTest.java @@ -6,7 +6,6 @@ import com.firstticket.queueservice.queuetoken.domain.vo.ProgramId; import com.firstticket.queueservice.queuetoken.domain.vo.QueueTokenId; import com.firstticket.queueservice.queuetoken.domain.vo.UserId; -import com.firstticket.queueservice.queuetoken.infrastructure.redis.RedisQueueTokenRepository; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -401,68 +400,6 @@ class Admit { } } - @Nested - @DisplayName("findActiveProgramIds") - class FindActiveProgramIds { - - @Test - @DisplayName("큐가 없으면 빈 리스트 반환") - void 큐_없음_빈_리스트() { - List result = repository.findActiveProgramIds(); - - assertThat(result).isEmpty(); - } - - @Test - @DisplayName("한 프로그램에 토큰이 있으면 그 프로그램 ID 1 개 반환") - void 한_프로그램_1_개() { - // given - UserId userId = UserId.of(UUID.randomUUID()); - ProgramId programId = ProgramId.of(UUID.randomUUID()); - repository.enqueue(QueueToken.issue(userId, programId)); - - // when - List result = repository.findActiveProgramIds(); - - // then - assertThat(result).containsExactly(programId); - } - - @Test - @DisplayName("여러 프로그램에 토큰이 있으면 모든 프로그램 ID 반환") - void 여러_프로그램_모두() { - // given - ProgramId program1 = ProgramId.of(UUID.randomUUID()); - ProgramId program2 = ProgramId.of(UUID.randomUUID()); - ProgramId program3 = ProgramId.of(UUID.randomUUID()); - repository.enqueue(QueueToken.issue(UserId.of(UUID.randomUUID()), program1)); - repository.enqueue(QueueToken.issue(UserId.of(UUID.randomUUID()), program2)); - repository.enqueue(QueueToken.issue(UserId.of(UUID.randomUUID()), program3)); - - // when - List result = repository.findActiveProgramIds(); - - // then - assertThat(result).containsExactlyInAnyOrder(program1, program2, program3); - } - - @Test - @DisplayName("같은 프로그램에 여러 토큰이 있어도 프로그램 ID 1 개만 반환") - void 같은_프로그램_중복_X() { - // given - ProgramId programId = ProgramId.of(UUID.randomUUID()); - repository.enqueue(QueueToken.issue(UserId.of(UUID.randomUUID()), programId)); - repository.enqueue(QueueToken.issue(UserId.of(UUID.randomUUID()), programId)); - repository.enqueue(QueueToken.issue(UserId.of(UUID.randomUUID()), programId)); - - // when - List result = repository.findActiveProgramIds(); - - // then - assertThat(result).containsExactly(programId); - } - } - private QueueToken newToken() { return QueueToken.issue( UserId.of(UUID.randomUUID()), From 59a32bbd9bba5ff47555c4f1333fc0db9d7cba48 Mon Sep 17 00:00:00 2001 From: rlaxxwls13 Date: Mon, 18 May 2026 05:00:05 +0900 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20=ED=99=9C=EC=84=B1=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=20=ED=8C=90=EB=8B=A8/=EC=98=88=EC=99=B8=EB=A5=BC=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=EB=A1=9C=20=EC=9C=84=EC=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 코드래빗 리뷰 반영 --- .../programmeta/domain/ProgramMeta.java | 15 +++++++++++++++ .../exception/ProgramNotActiveException.java | 3 ++- .../queuetoken/application/QueueTokenService.java | 9 +++++---- 3 files changed, 22 insertions(+), 5 deletions(-) rename src/main/java/com/firstticket/queueservice/{queuetoken => programmeta}/domain/exception/ProgramNotActiveException.java (74%) diff --git a/src/main/java/com/firstticket/queueservice/programmeta/domain/ProgramMeta.java b/src/main/java/com/firstticket/queueservice/programmeta/domain/ProgramMeta.java index f43a925..1e68309 100644 --- a/src/main/java/com/firstticket/queueservice/programmeta/domain/ProgramMeta.java +++ b/src/main/java/com/firstticket/queueservice/programmeta/domain/ProgramMeta.java @@ -1,5 +1,6 @@ package com.firstticket.queueservice.programmeta.domain; +import com.firstticket.queueservice.programmeta.domain.exception.ProgramNotActiveException; import com.firstticket.queueservice.programmeta.domain.vo.ProgramId; import lombok.AccessLevel; import lombok.AllArgsConstructor; @@ -79,6 +80,20 @@ public boolean isActiveAt(LocalDateTime now) { return !openAt.isAfter(now) && !closeAt.isBefore(now); } + /** + * 현재 시점에 입장 가능한지 보장. 그렇지 않으면 도메인 예외를 던진다. + * + *

대기열 진입 시점처럼 "활성 본질을 강제" 해야 하는 곳에서 사용한다. + * 호출 측은 분기 없이 메소드만 호출하면 된다 (Tell, Don't Ask).

+ * + * @throws ProgramNotActiveException CANCELLED 이거나 openAt 전 / closeAt 후 + */ + public void ensureActiveAt(LocalDateTime now) { + if (!isActiveAt(now)) { + throw new ProgramNotActiveException(); + } + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/src/main/java/com/firstticket/queueservice/queuetoken/domain/exception/ProgramNotActiveException.java b/src/main/java/com/firstticket/queueservice/programmeta/domain/exception/ProgramNotActiveException.java similarity index 74% rename from src/main/java/com/firstticket/queueservice/queuetoken/domain/exception/ProgramNotActiveException.java rename to src/main/java/com/firstticket/queueservice/programmeta/domain/exception/ProgramNotActiveException.java index 2e5545c..b9c545f 100644 --- a/src/main/java/com/firstticket/queueservice/queuetoken/domain/exception/ProgramNotActiveException.java +++ b/src/main/java/com/firstticket/queueservice/programmeta/domain/exception/ProgramNotActiveException.java @@ -1,6 +1,7 @@ -package com.firstticket.queueservice.queuetoken.domain.exception; +package com.firstticket.queueservice.programmeta.domain.exception; import com.firstticket.common.exception.BusinessException; +import com.firstticket.queueservice.queuetoken.domain.exception.QueueErrorCode; /** * 현재 시점이 프로그램의 입장 가능 시간이 아닐 때. diff --git a/src/main/java/com/firstticket/queueservice/queuetoken/application/QueueTokenService.java b/src/main/java/com/firstticket/queueservice/queuetoken/application/QueueTokenService.java index 03e938f..d77d274 100644 --- a/src/main/java/com/firstticket/queueservice/queuetoken/application/QueueTokenService.java +++ b/src/main/java/com/firstticket/queueservice/queuetoken/application/QueueTokenService.java @@ -8,7 +8,10 @@ import com.firstticket.queueservice.queuetoken.application.dto.QueueTokenResult; import com.firstticket.queueservice.queuetoken.domain.QueueToken; import com.firstticket.queueservice.queuetoken.domain.QueueTokenRepository; -import com.firstticket.queueservice.queuetoken.domain.exception.*; +import com.firstticket.queueservice.queuetoken.domain.exception.DuplicateTokenException; +import com.firstticket.queueservice.queuetoken.domain.exception.InvalidTokenStateException; +import com.firstticket.queueservice.queuetoken.domain.exception.ProgramNotFoundException; +import com.firstticket.queueservice.queuetoken.domain.exception.TokenNotFoundException; import com.firstticket.queueservice.queuetoken.domain.vo.ProgramId; import com.firstticket.queueservice.queuetoken.domain.vo.UserId; import lombok.RequiredArgsConstructor; @@ -103,8 +106,6 @@ private void validateProgramActive(ProgramId programId) { ProgramMeta programMeta = programMetaRepository.findById(programId.id()) .orElseThrow(ProgramNotFoundException::new); - if (!programMeta.isActiveAt(LocalDateTime.now())) { - throw new ProgramNotActiveException(); - } + programMeta.ensureActiveAt(LocalDateTime.now()); } } From b7ec5cc3aace6c3783981f63c28a91bd9ef956b8 Mon Sep 17 00:00:00 2001 From: rlaxxwls13 Date: Mon, 18 May 2026 10:30:45 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20ensureActiveAt=EC=97=90=20now=20null?= =?UTF-8?q?=20=EB=B0=A9=EC=96=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../firstticket/queueservice/programmeta/domain/ProgramMeta.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/firstticket/queueservice/programmeta/domain/ProgramMeta.java b/src/main/java/com/firstticket/queueservice/programmeta/domain/ProgramMeta.java index 1e68309..1115a75 100644 --- a/src/main/java/com/firstticket/queueservice/programmeta/domain/ProgramMeta.java +++ b/src/main/java/com/firstticket/queueservice/programmeta/domain/ProgramMeta.java @@ -89,6 +89,7 @@ public boolean isActiveAt(LocalDateTime now) { * @throws ProgramNotActiveException CANCELLED 이거나 openAt 전 / closeAt 후 */ public void ensureActiveAt(LocalDateTime now) { + Objects.requireNonNull(now, "now는 null일 수 없습니다."); if (!isActiveAt(now)) { throw new ProgramNotActiveException(); }