Skip to content

Conversation

@Jjiggu
Copy link
Contributor

@Jjiggu Jjiggu commented Jan 4, 2026

작업 요약

  • zcard 명령어 제거
  • 반복적인 redis 호출 하나로 묶음
  • Lua 스크립트 적용

Issue Link

#342

문제점 및 어려움

해결 방안

Reference

Summary by CodeRabbit

  • Bug Fixes

    • 중복 예약 발생 시 반환되는 HTTP 상태 코드가 BAD_REQUEST에서 CONFLICT로 변경되어 클라이언트 처리 정확도 향상
  • New Features

    • 대기 상태 스냅샷 도입 — 순번, 인원, 예약 ID 및 신규 여부를 명확히 조회 가능
    • Redis 기반 원자적 대기열 삽입 및 중복 감지 도입으로 중복 처리·TTL 동기화 개선
    • 대여(lease) 카운트 추적 및 보유(holding)/활성(active) 관리 로직 강화로 동시성·제한 관리 정밀도 향상

✏️ Tip: You can customize this high-level summary in your review settings.

@Jjiggu Jjiggu self-assigned this Jan 4, 2026
@Jjiggu Jjiggu added the refactor 리팩토링 label Jan 4, 2026
@coderabbitai
Copy link

coderabbitai bot commented Jan 4, 2026

Caution

Review failed

The pull request is closed.

Walkthrough

DuplicateReservationException의 응답 상태를 400→409로 변경했습니다. Redis 대기열 삽입/조회가 Lua 스크립트 기반으로 원자화되어 WaitingSnapshot DTO와 관련 add/get 메서드가 추가되었고, 대기 허용증(lease) 획득/해제 로직이 Lua V2로 확장되어 TTL 및 리스 카운트 동기화를 수행하도록 변경되었습니다.

Changes

Cohort / File(s) 변경 요약
예외 처리 상태 코드
nowait-app-user-api/.../exception/GlobalExceptionHandler.java
DuplicateReservationException 핸들러의 @ResponseStatusBAD_REQUESTCONFLICT(409)로 변경
대기 상태 DTO
nowait-app-user-api/.../reservation/dto/WaitingSnapshot.java
불변 DTO 클래스 추가: rank: Long, partySize: Integer, reservationId: String, isNew: boolean (Lombok 사용)
대기열 Redis 저장소 (앱 레이어)
nowait-app-user-api/.../reservation/repository/WaitingUserRedisRepository.java
ADD_WAITING_LUA Lua 스크립트 추가; addToWaitingQueueLua(...) 추가(원자적 삽입·중복 감지·데일리 시퀀스·메타·TTL); getWaitingSnapshot(...) 추가(파이프라인으로 rank/partySize/reservationId 조회)
예약 서비스 로직
nowait-app-user-api/.../reservation/service/ReservationService.java
기존 빠른 중복 반환 경로 제거; Lua 삽입 결과(WaitingSnapshot) 기준으로 중복 판정 및 흐름 제어; finalize/release 호출 조건 및 예외 처리 변경; 응답 생성에 후속 스냅샷 사용
Redis 키 유틸
nowait-domain/domain-redis/.../common/util/RedisKeyUtils.java
buildUserLeaseCountKey(String userId) 공용 메서드 추가(리스 카운트 키 생성)
대기 허용증 Lua 저장소 (도메인 레이어)
nowait-domain/domain-redis/.../reservation/repository/WaitingPermitLuaRepository.java
ACQUIRE_SCRIPT_V2, RELEASE_SCRIPT_V2, REMOVE_ACTIVE_SCRIPT 추가; 리스 카운트 키(CK) 도입; acquireLease, releaseLease, removeActiveMember를 Lua 기반 원자적 조작으로 변경; finalizeActive(...)ttlTo3am 파라미터 추가 및 TTL 동기화 로직 포함

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Client as Client
    participant Service as ReservationService
    participant PermitRepo as WaitingPermitLuaRepository
    participant UserRepo as WaitingUserRedisRepository
    participant Redis as Redis

    Note over Client,Service: 예약 요청 (storeId, userId, partySize)
    Client->>Service: createReservation(...)
    Service->>PermitRepo: acquireLease(userId, token, storeId, ttl)
    PermitRepo->>Redis: EVAL ACQUIRE_SCRIPT_V2 (holding, active, leaseCount, token, ttl)
    Redis-->>PermitRepo: 1 / 0
    alt lease 성공
        Service->>UserRepo: addToWaitingQueueLua(storeId, userId, partySize, ts, ttl)
        UserRepo->>Redis: EVAL ADD_WAITING_LUA (waitingZ, meta, dailySeq, ttl)
        Redis-->>UserRepo: WaitingSnapshot(rank, partySize, reservationId, isNew)
        UserRepo-->>Service: WaitingSnapshot
        alt isNew == true
            Service->>PermitRepo: finalizeActive(userId, token, storeId, reservationId, ttlTo3am)
            PermitRepo->>Redis: EVAL finalize / pExpire
            Service-->>Client: 응답(reservationId, rank, partySize)
        else 중복
            Service->>PermitRepo: releaseLease(userId, token, storeId)
            PermitRepo->>Redis: EVAL RELEASE_SCRIPT_V2 (holding, active, leaseCount)
            Service-->>Client: DuplicateReservationException (HTTP 409)
        end
    else lease 실패
        Service-->>Client: 자원 제한 실패
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • HyemIin

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.46% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목이 변경 사항의 주요 내용과 일치합니다. 제목은 웨이팅 등록 기능의 성능 개선을 명확히 나타내며, 요약에서 확인할 수 있는 Lua 스크립트 적용, Redis 호출 최적화 등의 변경 사항과 부합합니다.

📜 Recent review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 28138d3 and 114de85.

📒 Files selected for processing (1)
  • nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions bot requested a review from HyemIin January 4, 2026 08:31
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (3)
nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java (1)

156-163: 응답 생성을 위한 추가 조회는 일관성을 보장하지만 최적화 가능합니다.

Lua 스크립트 결과에서 rank를 함께 반환하도록 수정하면 getWaitingSnapshot 재호출을 제거할 수 있습니다. 현재 구현도 정확하지만, 성능 개선 목적의 PR 취지를 고려하면 추가 최적화를 검토해볼 수 있습니다.

nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/common/util/RedisKeyUtils.java (1)

83-84: 키 네이밍 컨벤션 불일치를 개선하세요.

기존 메서드들(buildUserHoldingKey, buildUserActiveKey)은 "waiting:user:{userId}:..." 패턴을 사용하는데, 새로 추가된 메서드는 "userID:{userId}:lease:cnt" 패턴을 사용하고 있습니다. 일관성을 위해 기존 컨벤션에 맞춰 "waiting:user:{userId}:lease:cnt" 형태로 변경하는 것을 권장합니다.

🔎 제안하는 수정안
-	public static String buildUserLeaseCountKey(String userId) { return "userID:{" + userId + "}:lease:cnt"; }
+	public static String buildUserLeaseCountKey(String userId) { 
+		return "waiting:user:{" + userId + "}:lease:cnt"; 
+	}
nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/reservation/repository/WaitingPermitLuaRepository.java (1)

24-30: 사용하지 않는 이전 스크립트를 제거하는 것을 고려하세요.

ACQUIRE_SCRIPT는 더 이상 사용되지 않으며 ACQUIRE_SCRIPT_V2로 대체되었습니다. 코드베이스를 깔끔하게 유지하고 혼란을 방지하기 위해 제거하는 것을 권장합니다.

🔎 제안하는 수정안
-	private static final String ACQUIRE_SCRIPT =
-		"redis.call('ZREMRANGEBYSCORE', KEYS[1], '-inf', ARGV[1]);" +
-		"local holding = redis.call('ZCARD', KEYS[1]);" +
-		"local active  = redis.call('SCARD', KEYS[2]);" +
-		"if (holding + active) >= tonumber(ARGV[3]) then return 0 end;" +
-		"redis.call('ZADD', KEYS[1], tonumber(ARGV[1]) + tonumber(ARGV[2]), ARGV[4]);" +
-		"return 1;";
-
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8f87261 and 53a457f.

📒 Files selected for processing (6)
  • nowait-app-user-api/src/main/java/com/nowait/applicationuser/exception/GlobalExceptionHandler.java
  • nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/dto/WaitingSnapshot.java
  • nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java
  • nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java
  • nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/common/util/RedisKeyUtils.java
  • nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/reservation/repository/WaitingPermitLuaRepository.java
🧰 Additional context used
🧬 Code graph analysis (3)
nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/dto/WaitingSnapshot.java (1)
nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/user/dto/ManagerLoginResponseDto.java (1)
  • AllArgsConstructor (10-34)
nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java (1)
nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/common/util/RedisKeyUtils.java (1)
  • RedisKeyUtils (9-100)
nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/reservation/repository/WaitingPermitLuaRepository.java (1)
nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/common/util/RedisKeyUtils.java (1)
  • RedisKeyUtils (9-100)
🔇 Additional comments (11)
nowait-app-user-api/src/main/java/com/nowait/applicationuser/exception/GlobalExceptionHandler.java (1)

218-224: LGTM! HTTP 상태 코드 변경이 적절합니다.

DuplicateReservationException에 대해 BAD_REQUEST(400)에서 CONFLICT(409)로 변경한 것은 REST 의미론에 더 부합합니다. 409는 리소스 상태 충돌(중복 생성 시도)을 나타내고, 400은 잘못된 요청 형식을 나타내므로 이 변경이 정확합니다.

nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/dto/WaitingSnapshot.java (1)

1-12: LGTM! 불변 DTO가 잘 설계되었습니다.

final 필드와 Lombok 어노테이션을 사용한 불변 DTO 패턴이 적절합니다. Redis 파이프라인 결과를 담기에 적합한 구조입니다.

nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java (1)

33-74: Lua 스크립트 로직이 적절합니다.

ZADD NX를 사용한 원자적 중복 방지와 TTL 초기화 로직이 잘 구현되어 있습니다. 반환값 {added, reservationId} 구조도 명확합니다.

nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java (1)

122-126: 조기 중복 체크 로직이 적절합니다.

lease 획득 전에 중복 체크를 수행하여 불필요한 리소스 사용을 방지합니다. 단, WaitingUserRedisRepository.addToWaitingQueueLua의 반환값 처리 개선이 필요합니다(해당 파일의 리뷰 참조).

nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/reservation/repository/WaitingPermitLuaRepository.java (7)

32-76: Lua 스크립트 기반 원자적 작업 처리가 잘 구현되었습니다.

새로운 ACQUIRE_SCRIPT_V2는 여러 Redis 작업을 하나의 원자적 트랜잭션으로 통합하여 성능을 개선했습니다. 만료된 항목 정리, 정확한 limit 체크, lease 카운트 증가, TTL 동기화를 모두 하나의 스크립트에서 처리하고 있어 네트워크 왕복 횟수가 크게 감소합니다.

참고: PR 목표에 "zcard 명령어 제거"가 명시되어 있으나, Line 52-53에서 SCARDZCARD를 여전히 사용하고 있습니다. 다만 이는 정확한 limit 체크를 위해 필수적이며, 기존에는 여러 번의 개별 호출이었던 것을 단일 Lua 스크립트로 통합한 것이므로 성능 개선 목표에 부합합니다.


83-91: 원자적 해제 로직과 방어적 카운트 처리가 우수합니다.

RELEASE_SCRIPT_V2는 holding에서 제거와 lease 카운트 감소를 원자적으로 처리하며, 카운트가 음수가 되는 경우를 방어적으로 처리하고 있습니다 (Line 88). 이는 동시성 환경에서 데이터 무결성을 보장하는 좋은 패턴입니다.


93-101: active 멤버 제거 로직이 일관성 있게 구현되었습니다.

REMOVE_ACTIVE_SCRIPTRELEASE_SCRIPT_V2와 동일한 패턴을 따라 원자적으로 제거와 카운트 감소를 처리합니다. 일관된 구현 패턴은 유지보수성을 높입니다.


104-124: lease 획득 로직이 새로운 스크립트에 맞게 올바르게 업데이트되었습니다.

메서드가 3개의 키(holding, active, lease count)를 전달하고 TTL을 스크립트 내부에서 설정하도록 변경되어, 모든 작업이 원자적으로 처리됩니다. 이는 기존에 분리되어 있던 pExpire 호출을 제거하고 일관성을 보장합니다.


144-158: lease 해제 로직이 원자적 스크립트로 개선되었습니다.

기존의 직접 ZREM 호출을 RELEASE_SCRIPT_V2를 사용한 원자적 작업으로 교체하여, holding 제거와 lease count 감소가 함께 처리됩니다. 이는 데이터 일관성을 보장하는 좋은 개선입니다.


171-186: active 멤버 제거가 원자적으로 처리되도록 개선되었습니다.

REMOVE_ACTIVE_SCRIPT를 사용하여 active set에서의 제거와 lease count 감소를 원자적으로 처리합니다. 이는 releaseLease와 일관된 패턴을 따르며 동시성 안전성을 보장합니다.


126-142: finalizeActive에서 모든 Redis 키에 일관된 TTL을 적용하세요.

acquireLeaseACQUIRE_SCRIPT_V2는 holding, active, lease count 세 키 모두에 동일한 TTL을 설정합니다(PEXPIRE 3회 호출). 반면 finalizeActive는 active 키에만 TTL을 적용하고 있습니다(line 139).

holding 및 lease count 키가 active 키보다 먼저 만료되면 데이터 불일치가 발생할 수 있으므로, finalizeActive에서도 ttlTo3am을 모든 키에 적용하거나, TTL 동기화가 불필요한 경우 그 이유를 명확히 해야 합니다.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java (1)

144-169: Lua 스크립트의 중복 플래그를 확인하지 않습니다.

Line 150에서 snapshot.getReservationId().isEmpty()만 체크하고 있습니다. 하지만 Lua 스크립트가 중복을 반환하는 경우(added == 0)에도 reservationId는 존재하므로 isEmpty()는 false입니다.

WaitingUserRedisRepository.addToWaitingQueueLua의 현재 구현에서는 중복 시 ranknull로 반환하므로, snapshot.getRank() == null을 체크하여 중복을 감지해야 합니다.

🔎 제안: rank를 체크하여 중복 감지
 		WaitingSnapshot snapshot;
 		try {
 			// 2) 스토어 큐 등록(기존 메서드 그대로)
 			long ts = System.currentTimeMillis();
-			// reservationId = waitingUserRedisRepository.addToWaitingQueue(storeId, userId, dto.getPartySize(), ts);
 			snapshot = waitingUserRedisRepository.addToWaitingQueueLua(storeId, userId, dto.getPartySize(), ts, ttlTo3am);
-			if (snapshot.getReservationId().isEmpty())
+			
+			// Lua 스크립트가 중복을 반환한 경우 (rank == null)
+			if (snapshot.getRank() == null) {
+				throw new DuplicateReservationException();
+			}
+			
+			if (snapshot.getReservationId() == null || snapshot.getReservationId().isEmpty())
 				throw new ReservationNumberIssueFailException();
 
 			// 3) 확정(holding→active)
 			waitingPermitLuaRepository.finalizeActive(userId, token, String.valueOf(storeId), snapshot.getReservationId(), ttlTo3am);
 
 			WaitingSnapshot after = waitingUserRedisRepository.getWaitingSnapshot(storeId, userId);
 
 			// 4) 응답
 			return WaitingResponseDto.builder()
 				.reservationNumber(after.getReservationId())
 				.rank(after.getRank() == null ? -1 : after.getRank().intValue() + 1)
 				.partySize(after.getPartySize() == null ? 0 : after.getPartySize())
 				.build();
 
 		} catch (RuntimeException e) {
 			// 실패 시 임대 반납
 			waitingPermitLuaRepository.releaseLease(userId, token);
 			throw e;
 		}
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 53a457f and 2d08d5b.

📒 Files selected for processing (2)
  • nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java
  • nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java
🧰 Additional context used
🧬 Code graph analysis (1)
nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java (1)
nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/common/util/RedisKeyUtils.java (1)
  • RedisKeyUtils (9-100)
🔇 Additional comments (2)
nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java (2)

33-74: Lua 스크립트 구현이 올바릅니다.

원자적 중복 방지를 위해 ZADD NX를 사용하고, 성공/중복을 명확히 구분하여 반환하며, TTL을 조건부로 설정하는 로직이 정확합니다.


315-359: 파이프라인을 활용한 효율적인 구현입니다.

한 번의 round-trip으로 rank, partySize, reservationId를 조회하고, deserialize 결과에 대한 null 체크를 포함하여 이전 리뷰의 NPE 우려를 해결했습니다.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java (1)

138-143: 중복 체크와 큐 등록 사이에 race condition이 여전히 존재합니다.

Line 139의 getWaitingSnapshot 호출과 Line 150의 addToWaitingQueueLua 호출 사이에 다른 스레드가 동일한 사용자를 등록할 수 있습니다. 현재 로직은:

  1. 임대 획득 (Line 122-136)
  2. 중복 체크 (Line 139-143)
  3. 큐 등록 (Line 150)

이 3단계가 원자적이지 않으므로, 두 요청이 동시에 중복 체크를 통과한 후 모두 큐에 등록될 수 있습니다.

addToWaitingQueueLua의 Lua 스크립트는 이미 ZADD ... NX로 중복을 원자적으로 처리하므로, Line 138-143의 사전 체크를 제거하고 Lua 스크립트의 반환값(added == 0)으로 중복을 판단해야 합니다.

🔎 제안: Lua 스크립트 반환값으로 중복 판단
 	// 1) 임대 획득
 	String token = java.util.UUID.randomUUID().toString();
 	int attempts = 0;
 	while (true) {
 		boolean ok = waitingPermitLuaRepository.acquireLease(userId, token, System.currentTimeMillis(), LEASE_MS,
 			USER_LIMIT, ttlTo3am);
 		if (ok)
 			break;
 		if (++attempts >= 3)
 			throw new UserWaitingLimitExceededException();
 		try {
 			Thread.sleep((long)(5 * Math.pow(3, attempts - 1)));
 		} catch (InterruptedException ignored) {
 		}
 	}
 
-	// 2) 임대 획득 후 중복 체크
-	WaitingSnapshot existingSnapshot = waitingUserRedisRepository.getWaitingSnapshot(storeId, userId);
-	if (existingSnapshot.getRank() != null) {
-		waitingPermitLuaRepository.releaseLease(userId, token);
-		throw new DuplicateReservationException();
-	}
-
 	WaitingSnapshot snapshot;
 	try {
-		// 2) 스토어 큐 등록(기존 메서드 그대로)
+		// 2) 스토어 큐 등록 (Lua 스크립트가 중복 체크를 원자적으로 수행)
 		long ts = System.currentTimeMillis();
-		// reservationId = waitingUserRedisRepository.addToWaitingQueue(storeId, userId, dto.getPartySize(), ts);
 		snapshot = waitingUserRedisRepository.addToWaitingQueueLua(storeId, userId, dto.getPartySize(), ts, ttlTo3am);
+		
+		// 중복인 경우 (Lua에서 added == 0 반환)
+		if (snapshot.getRank() != null) {
+			waitingPermitLuaRepository.releaseLease(userId, token);
+			throw new DuplicateReservationException();
+		}
+		
 		if (snapshot.getReservationId().isEmpty())
 			throw new ReservationNumberIssueFailException();
🧹 Nitpick comments (2)
nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java (2)

115-169: 신규 등록 시 rank 조회가 Lua 스크립트 실행과 원자적이지 않습니다.

Line 167에서 getRank(storeId, userId)를 별도로 호출하므로, Lua 스크립트 실행과 rank 조회 사이에 다른 사용자가 등록되면 반환된 rank가 정확하지 않을 수 있습니다.

더 정확한 구현을 위해 Lua 스크립트가 신규 등록 시 rank도 함께 반환하도록 개선할 수 있습니다 (예: redis.call('ZRANK', KEYS[1], ARGV[1])을 스크립트 내에서 수행).

🔎 제안: Lua 스크립트에서 rank도 함께 반환

Lua 스크립트 수정:

-- 신규 등록 후 rank 조회 추가 (line 73 이전에)
local rank = redis.call('ZRANK', KEYS[1], ARGV[1])
return {1, reservationId, rank}

Java 코드 수정:

 @SuppressWarnings("unchecked")
 List<Object> response = (List<Object>) result;
-if (response.size() < 2) return null;
+if (response.size() < 2) return null;
 
 Long added = response.get(0) instanceof Long l ? l : Long.parseLong(String.valueOf(response.get(0)));
 String reservationId = String.valueOf(response.get(1));
 
 if (added == 0) {
 	Integer existingPartySize = getPartySize(storeId, userId);
 	Long existingRank = getRank(storeId, userId);
 	return new WaitingSnapshot(existingRank, existingPartySize, reservationId);
 }
 
-Long actualRank = getRank(storeId, userId);
+// Lua 스크립트가 3개 요소를 반환하는 경우
+Long actualRank = (response.size() >= 3 && response.get(2) instanceof Long r) 
+    ? r 
+    : getRank(storeId, userId);
 return new WaitingSnapshot(actualRank, partySize, reservationId);

343-350: Integer.valueOf 호출 시 NumberFormatException 처리 고려

Line 346과 349에서 Integer.valueOf(s)Integer.valueOf(deserialized)를 호출하는데, Redis 데이터가 손상되었거나 예상치 못한 형식인 경우 NumberFormatException이 발생할 수 있습니다.

현재 구현은 Redis 데이터 무결성을 신뢰하는 경우 충분하지만, 더 방어적인 코드를 원한다면 try-catch로 감싸서 null을 반환하도록 할 수 있습니다.

🔎 선택적 개선안
 Integer partySize = null;
 Object psObj = results.get(1);
 if (psObj instanceof String s) {
-	partySize = Integer.valueOf(s);
+	try {
+		partySize = Integer.valueOf(s);
+	} catch (NumberFormatException e) {
+		// Redis 데이터 손상 시 null 유지
+	}
 } else if (psObj instanceof byte[] b) {
 	String deserialized = redisTemplate.getStringSerializer().deserialize(b);
-	partySize = deserialized != null ? Integer.valueOf(deserialized) : null;
+	if (deserialized != null) {
+		try {
+			partySize = Integer.valueOf(deserialized);
+		} catch (NumberFormatException e) {
+			// Redis 데이터 손상 시 null 유지
+		}
+	}
 }
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2d08d5b and 2a774ef.

📒 Files selected for processing (2)
  • nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java
  • nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java
🔇 Additional comments (2)
nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java (2)

33-74: Lua 스크립트의 TTL 로직이 적절합니다.

ZADD의 NX 플래그를 사용하여 원자적으로 중복을 방지하고, PTTL 체크로 TTL이 설정되지 않은 경우에만 TTL을 설정하는 로직이 올바릅니다. 일일 시퀀스 생성 및 메타데이터 저장도 적절하게 구현되어 있습니다.


318-362: 파이프라인을 활용한 효율적인 구현입니다.

getWaitingSnapshot 메서드가 Redis 파이프라인을 사용하여 rank, partySize, reservationId를 한 번의 round-trip으로 조회하는 것은 성능 측면에서 우수한 접근입니다. 타입 체크와 null 안전성도 적절하게 처리되어 있습니다.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java (2)

139-150: 변수명이 흐름을 명확히 반영하지 못합니다.

existingSnapshot 변수가 두 가지 다른 목적으로 재사용되고 있습니다:

  • Line 139: 기존 대기 데이터 존재 여부 확인용
  • Line 148: 신규 삽입 결과 저장용

Line 143에서 기존 데이터가 없음을 확인한 후 Line 148에서는 신규 삽입 결과를 저장하므로, 두 번째 사용 시점의 변수명은 newSnapshot 또는 insertResult 등으로 변경하면 코드 의도가 더 명확해집니다.

🔎 변수명 개선 제안
 		// 2) 임대 획득 후 중복 체크
 		WaitingSnapshot existingSnapshot = waitingUserRedisRepository.getWaitingSnapshot(storeId, userId);
 		if (existingSnapshot.getRank() != null) {
 			waitingPermitLuaRepository.releaseLease(userId, token);
 			throw new DuplicateReservationException();
 		}
 
 		try {
 			// 2) 스토어 큐 등록(기존 메서드 그대로)
 			long ts = System.currentTimeMillis();
-			existingSnapshot = waitingUserRedisRepository.addToWaitingQueueLua(storeId, userId, dto.getPartySize(), ts, ttlTo3am);
-			if (existingSnapshot.getReservationId().isEmpty())
+			WaitingSnapshot insertResult = waitingUserRedisRepository.addToWaitingQueueLua(storeId, userId, dto.getPartySize(), ts, ttlTo3am);
+			if (insertResult.getReservationId().isEmpty())
 				throw new ReservationNumberIssueFailException();
 
 			// 3) 확정(holding → active)
-			waitingPermitLuaRepository.finalizeActive(userId, token, String.valueOf(storeId), existingSnapshot.getReservationId(), ttlTo3am);
+			waitingPermitLuaRepository.finalizeActive(userId, token, String.valueOf(storeId), insertResult.getReservationId(), ttlTo3am);

155-162: 방어적 null 체크 추가를 고려해보세요.

응답 생성 시 rankpartySize에 대해서는 null 체크를 수행하고 있으나(Lines 160-161), Line 159의 after.getReservationId()는 null 체크 없이 사용됩니다.

Line 149에서 이미 reservationId가 비어있지 않음을 확인했고, finalizeActive 이후에도 제거되지 않아야 하지만, 예상치 못한 Redis 상태 문제에 대비한 방어적 코딩을 위해 null 체크를 추가하는 것을 고려해보세요.

🔎 방어적 null 체크 추가 예시
 			WaitingSnapshot after = waitingUserRedisRepository.getWaitingSnapshot(storeId, userId);
+			
+			String reservationId = after.getReservationId();
+			if (reservationId == null || reservationId.isEmpty()) {
+				throw new ReservationNumberIssueFailException();
+			}
 
 			// 4) 응답
 			return WaitingResponseDto.builder()
-				.reservationNumber(after.getReservationId())
+				.reservationNumber(reservationId)
 				.rank(after.getRank() == null ? -1 : after.getRank().intValue() + 1)
 				.partySize(after.getPartySize() == null ? 0 : after.getPartySize())
 				.build();
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2a774ef and fafef12.

📒 Files selected for processing (1)
  • nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java
🔇 Additional comments (2)
nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java (2)

138-143: 중복 체크 시점 개선이 잘 적용되었습니다.

임대 획득 후 중복 체크를 수행하도록 변경되어, 이전 리뷰에서 지적된 TOCTOU(time-of-check-to-time-of-use) 경쟁 상태 문제가 해결되었습니다. 임대를 먼저 획득하여 동일 사용자의 동시 요청을 차단한 후 중복 여부를 확인하므로, 중복 체크와 삽입 사이의 간격이 보호됩니다.


152-153: finalizeActive 호출 시점이 올바르게 수정되었습니다.

이전 리뷰에서 지적된 "중복 등록 시에도 finalizeActive가 호출될 수 있는" 문제가 해결되었습니다. 현재 흐름에서는:

  • Line 142: 중복 감지 시 예외 발생으로 종료
  • Line 149: 삽입 실패 시 예외 발생으로 종료
  • Line 153: 신규 삽입 성공 시에만 도달

따라서 finalizeActive는 새로운 대기 등록에 대해서만 호출되어, 데이터 불일치 위험이 제거되었습니다.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java (1)

162-162: NumberFormatException 처리가 누락되었습니다.

Line 162에서 response.get(2).toString()의 결과가 유효한 정수 문자열이 아닐 경우 Integer.valueOfNumberFormatException을 던질 수 있습니다. Lua 스크립트가 항상 정수를 반환하더라도, 방어적 프로그래밍을 위해 예외 처리를 추가하는 것이 좋습니다.

🔎 제안: try-catch로 NumberFormatException 처리
 		boolean isNew = ((Long) response.get(0)) == 1L;
 		Long rank = response.get(1) == null ? null : (Long) response.get(1);
-		Integer ps = response.get(2) == null ? null : Integer.valueOf(response.get(2).toString());
+		Integer ps = null;
+		if (response.get(2) != null) {
+			try {
+				ps = Integer.valueOf(response.get(2).toString());
+			} catch (NumberFormatException e) {
+				// Lua 스크립트 오류 또는 데이터 손상
+				ps = null;
+			}
+		}
 		String reservationId = response.get(3) == null ? null : response.get(3).toString();
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fafef12 and ea022d2.

📒 Files selected for processing (3)
  • nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/dto/WaitingSnapshot.java
  • nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java
  • nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/dto/WaitingSnapshot.java
🧰 Additional context used
🧬 Code graph analysis (1)
nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java (1)
nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/common/util/RedisKeyUtils.java (1)
  • RedisKeyUtils (9-100)
🔇 Additional comments (5)
nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java (2)

139-144: 임대 획득 후 중복 체크 로직이 잘 구현되었습니다.

이전 리뷰에서 지적된 race condition 문제를 해결하기 위해 임대 획득 후 중복 체크를 수행하도록 개선되었습니다. 중복이 감지되면 임대를 즉시 반납하고 예외를 던지는 로직이 명확합니다.


170-177: 응답 구성 로직이 올바르게 개선되었습니다.

getWaitingSnapshot을 호출하여 최신 상태를 가져온 후 응답을 구성하는 방식으로 변경되어, 데이터 일관성이 보장됩니다. null-safe 처리도 적절합니다.

nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java (3)

33-78: Lua 스크립트가 잘 설계되었습니다.

원자적 작업을 통해 중복 등록을 방지하고, ZADD NX를 사용하여 이미 존재하는 경우 추가하지 않도록 구현되었습니다. 반환값 구조 {isNew, rank, partySize, reservationId}도 명확하며, TTL 처리도 적절합니다.


160-172: Lua 응답 파싱 로직이 개선되었습니다.

이전 리뷰에서 지적된 성공/중복 구분 문제가 isNew 플래그를 통해 해결되었으며, 중복과 신규 등록 케이스를 명확히 구분하여 WaitingSnapshot을 반환합니다.


321-365: getWaitingSnapshot 구현이 효율적입니다.

파이프라인을 사용하여 rank, partySize, reservationId를 한 번의 round-trip으로 조회하며, null-safe 처리도 잘 되어 있습니다. Line 351-352의 NumberFormatException 위험은 이전 리뷰에서 지적되어 이미 처리된 것으로 표시되어 있습니다.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (3)
nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java (3)

64-77: 중복 등록 시 partySize 반환값 확인 필요

Line 77에서 ARGV[3](입력 partySize)를 항상 반환하고 있습니다. 중복 등록(added == 0)인 경우, 기존에 저장된 partySize와 새 요청의 partySize가 다를 수 있습니다.

만약 중복 시 기존 저장값을 반환해야 한다면 수정이 필요합니다:

🔎 중복 시 저장된 partySize 반환
+local storedPartySize = ARGV[3]
+
 if added == 1 then
     isNew = 1
     local seq = redis.call('INCR', KEYS[5])
     local seqStr = string.format('%04d', seq)
     reservationId = ARGV[5] .. '-' .. ARGV[4] .. '-' .. seqStr
     
     redis.call('HSET', KEYS[4], ARGV[1], reservationId)
     redis.call('HSET', KEYS[2], ARGV[1], ARGV[3])
     redis.call('HSET', KEYS[3], ARGV[1], 'WAITING')
 else
     reservationId = redis.call('HGET', KEYS[4], ARGV[1])
+    storedPartySize = redis.call('HGET', KEYS[2], ARGV[1])
 end

 local rank = redis.call('ZRANK', KEYS[1], ARGV[1])
 
 -- TTL 처리 동일...

-return {isNew, rank, ARGV[3], reservationId}
+return {isNew, rank, storedPartySize, reservationId}

160-163: NumberFormatException 가능성

Line 162에서 Integer.valueOf(response.get(2).toString())이 잘못된 형식의 문자열에 대해 예외를 발생시킬 수 있습니다. Lua 스크립트가 항상 유효한 숫자를 반환하므로 실질적 위험은 낮지만, 방어적 코딩을 고려해볼 수 있습니다.

🔎 방어적 파싱 적용
-Integer ps = response.get(2) == null ? null : Integer.valueOf(response.get(2).toString());
+Integer ps = null;
+if (response.get(2) != null) {
+    try {
+        ps = Integer.valueOf(response.get(2).toString());
+    } catch (NumberFormatException e) {
+        // 로깅 또는 기본값 처리
+    }
+}

348-353: Integer 파싱 시 예외 처리 고려

addToWaitingQueueLua와 동일하게 Integer.valueOf() 호출에 대한 방어적 처리를 고려해볼 수 있습니다. Redis에 저장된 값이 항상 유효한 숫자라면 실질적 위험은 낮습니다.

🔎 방어적 파싱 예시
 // 2) partySize
 Integer partySize = null;
 Object psObj = results.get(1);
-if (psObj instanceof String s) {
-    partySize = Integer.valueOf(s);
-} else if (psObj instanceof byte[] b) {
-    String deserialized = redisTemplate.getStringSerializer().deserialize(b);
-    partySize = deserialized != null ? Integer.valueOf(deserialized) : null;
+try {
+    if (psObj instanceof String s) {
+        partySize = Integer.valueOf(s);
+    } else if (psObj instanceof byte[] b) {
+        String deserialized = redisTemplate.getStringSerializer().deserialize(b);
+        partySize = deserialized != null ? Integer.valueOf(deserialized) : null;
+    }
+} catch (NumberFormatException e) {
+    // 로깅 후 null 유지
 }
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ea022d2 and f01a7bf.

📒 Files selected for processing (1)
  • nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java
🧰 Additional context used
🧬 Code graph analysis (1)
nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java (1)
nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/common/util/RedisKeyUtils.java (1)
  • RedisKeyUtils (9-100)
🔇 Additional comments (3)
nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java (3)

21-24: LGTM!

Lua 스크립트 실행과 새로운 DTO 반환을 위한 적절한 import입니다.


321-365: 파이프라인을 활용한 효율적인 조회 구현

Redis 파이프라인을 사용하여 rank, partySize, reservationId를 단일 round-trip으로 조회하는 좋은 구현입니다. 이전 리뷰에서 지적된 NPE 문제(line 352)도 적절히 처리되었습니다.

isNew=false를 항상 반환하는 것은 조회 메서드의 특성상 적절합니다.


82-117: 기존 메서드와 신규 Lua 메서드 병존 확인

addToWaitingQueue(기존)와 addToWaitingQueueLua(신규)가 함께 존재합니다. 점진적 마이그레이션을 위한 것이라면 괜찮지만, 마이그레이션 완료 후에는 기존 메서드 제거를 고려하세요.

참고: 기존 메서드에서 userMapKey(line 88)는 TTL이 설정되지 않는 반면, 다른 키들은 TTL이 설정됩니다 (lines 106-110). 일관성 검토가 필요할 수 있습니다.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Fix all issues with AI Agents 🤖
In
@nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java:
- Around line 174-180: After calling
waitingUserRedisRepository.getWaitingSnapshot(storeId, userId) you must
null-check the returned variable after before accessing its fields; update
ReservationService to verify if after == null and handle it consistently with
the service's error handling (either throw the appropriate not-found exception,
e.g., ReservationNotFoundException/EntityNotFoundException, or return a
WaitingResponseDto with safe default values), ensuring you reference the same
variable name 'after' and use WaitingResponseDto.builder() in the chosen code
path.
- Around line 139-144: existingSnapshot may be null before calling
existingSnapshot.getRank(), causing NPE; update the duplicate-check block after
waitingUserRedisRepository.getWaitingSnapshot(storeId, userId) to first test for
null and only call getRank() when existingSnapshot != null — e.g. if
existingSnapshot == null then skip the duplicate logic, otherwise if
existingSnapshot.getRank() != null call
waitingPermitLuaRepository.releaseLease(userId, token) and throw new
DuplicateReservationException(); ensure no call to getRank() when
existingSnapshot is null.
- Around line 183-189: Fix the null-check logic in the catch block so you only
call releaseLease when snapshot is null or isNew; replace the faulty condition
`if (snapshot != null || snapshot.isNew())` with a check that evaluates true
when snapshot is null or when snapshot.isNew() is true (i.e., `snapshot == null
|| snapshot.isNew()`), ensuring the call to
waitingPermitLuaRepository.releaseLease(userId, token) cannot trigger a
NullPointerException.
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f01a7bf and 28138d3.

📒 Files selected for processing (1)
  • nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java
🔇 Additional comments (3)
nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java (3)

13-13: LGTM: 필요한 import 추가됨

UUID와 WaitingSnapshot의 import가 올바르게 추가되어 새로운 Lua 기반 대기열 로직을 지원합니다.

Also applies to: 26-26


123-137: LGTM: 임대 획득 로직이 적절하게 구현됨

재시도 로직과 exponential backoff를 사용한 임대 획득 메커니즘이 잘 구현되었습니다. 최대 3회 재시도 및 동시성 제어가 적절합니다.


159-171: LGTM: isNew=false 케이스 처리 완료

이전 리뷰 코멘트에서 지적된 isNew=false 케이스가 올바르게 처리되었습니다. Race condition으로 Lua 스크립트가 중복을 감지한 경우 임대를 해제하고 예외를 발생시킵니다.

@Jjiggu Jjiggu merged commit 0a5e5cb into develop Jan 5, 2026
1 check was pending
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

refactor 리팩토링

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants