From b57ec411bf054fca8deb6b28606300f36c130e33 Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Sun, 4 Jan 2026 16:56:34 +0900 Subject: [PATCH 01/17] =?UTF-8?q?feat:=20=EC=9B=A8=EC=9D=B4=ED=8C=85=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=EB=A5=BC=20=EB=8B=B4=EA=B8=B0=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20dto=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reservation/dto/WaitingSnapshot.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/dto/WaitingSnapshot.java diff --git a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/dto/WaitingSnapshot.java b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/dto/WaitingSnapshot.java new file mode 100644 index 00000000..651b3959 --- /dev/null +++ b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/dto/WaitingSnapshot.java @@ -0,0 +1,12 @@ +package com.nowait.applicationuser.reservation.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class WaitingSnapshot { + private final Long rank; + private final Integer partySize; + private final String reservationId; +} From f47ddfd3398c636f3e7ace9df7a0683f8181e405 Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Sun, 4 Jan 2026 16:57:01 +0900 Subject: [PATCH 02/17] =?UTF-8?q?refactor:=20=EC=9B=A8=EC=9D=B4=ED=8C=85?= =?UTF-8?q?=20=EB=93=B1=EB=A1=9D=20Lua=20=EC=8A=A4=ED=81=AC=EB=A6=BD?= =?UTF-8?q?=ED=8A=B8=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../WaitingUserRedisRepository.java | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java index d11e5a30..9d36def5 100644 --- a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java +++ b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java @@ -18,8 +18,10 @@ import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.ScanOptions; import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.stereotype.Repository; +import com.nowait.applicationuser.reservation.dto.WaitingSnapshot; import com.nowait.domaincoreredis.common.util.RedisKeyUtils; import lombok.RequiredArgsConstructor; @@ -28,6 +30,48 @@ @RequiredArgsConstructor public class WaitingUserRedisRepository { private final StringRedisTemplate redisTemplate; + private static final String ADD_WAITING_LUA = """ + -- KEYS + -- 1: queueKey + -- 2: partyKey + -- 3: statusKey + -- 4: numberMapKey + -- 5: dailySeqKey + + -- ARGV + -- 1: userId + -- 2: timestamp (ms) + -- 3: partySize + -- 4: today (YYYYMMDD) + -- 5: storeId + -- 6: ttlMillis + + -- 1) 큐 등록 (중복 방지) + local added = redis.call('ZADD', KEYS[1], 'NX', ARGV[2], ARGV[1]) + if added == 0 then + local rid = redis.call('HGET', KEYS[4], ARGV[1]) + return {0, rid} + end + + -- 2) 일일 시퀀스 + local seq = redis.call('INCR', KEYS[5]) + local seqStr = string.format('%04d', seq) + local reservationId = ARGV[5] .. '-' .. ARGV[4] .. '-' .. seqStr + + -- 3) 메타 저장 + 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') + + -- 4) TTL은 최초 1회만 + if redis.call('PTTL', KEYS[1]) < 0 then + for i = 1, #KEYS do + redis.call('PEXPIRE', KEYS[i], ARGV[6]) + end + end + + return {1, reservationId} + """; // 중복 등록 방지: 이미 있으면 추가X // 특정 주점에 대한 예약 등록 @@ -68,6 +112,49 @@ public String addToWaitingQueue(Long storeId, String userId, Integer partySize, return reservationId; } + // 루아 스크립트 사용 + public String addToWaitingQueueLua( + Long storeId, + String userId, + Integer partySize, + long ts, + Duration ttl + ) { + String queueKey = RedisKeyUtils.buildWaitingKeyPrefix() + storeId; + String partyKey = RedisKeyUtils.buildWaitingPartySizeKeyPrefix() + storeId; + String statusKey = RedisKeyUtils.buildWaitingStatusKeyPrefix() + storeId; + String numberMapKey = RedisKeyUtils.buildReservationNumberKey(storeId); + + String today = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); + String dailySeqKey = RedisKeyUtils.buildReservationSeqKey(storeId) + ":" + today; + + List keys = List.of( + queueKey, + partyKey, + statusKey, + numberMapKey, + dailySeqKey + ); + + Object result = redisTemplate.execute( + new DefaultRedisScript<>(ADD_WAITING_LUA, List.class), + keys, + userId, + String.valueOf(ts), + String.valueOf(partySize), + today, + String.valueOf(storeId), + String.valueOf(ttl.toMillis()) + ); + + if (result == null) return null; + + @SuppressWarnings("unchecked") + List response = (List) result; + return response.size() >= 2 ? String.valueOf(response.get(1)) : null; + + } + // 예약한 사람이 등록한 동반인원(partySize) 조회 public Integer getPartySize(Long storeId, String userId) { String partyKey = RedisKeyUtils.buildWaitingPartySizeKeyPrefix() + storeId; @@ -214,6 +301,53 @@ public String GenerateReservationNumber(String seqKey, Long storeId) { return reservationId; } + + public WaitingSnapshot getWaitingSnapshot(Long storeId, String userId) { + String queueKey = RedisKeyUtils.buildWaitingKeyPrefix() + storeId; + String partyKey = RedisKeyUtils.buildWaitingPartySizeKeyPrefix() + storeId; + String numberMapKey = RedisKeyUtils.buildReservationNumberKey(storeId); + + List results = redisTemplate.executePipelined((RedisCallback) conn -> { + byte[] qk = redisTemplate.getStringSerializer().serialize(queueKey); + byte[] pk = redisTemplate.getStringSerializer().serialize(partyKey); + byte[] nk = redisTemplate.getStringSerializer().serialize(numberMapKey); + byte[] uid = redisTemplate.getStringSerializer().serialize(userId); + + conn.zRank(qk, uid); + conn.hGet(pk, uid); + conn.hGet(nk, uid); + return null; + }); + + if (results == null || results.size() < 3) { + return new WaitingSnapshot(null, null, null); + } + + // 1) rank + Long rank = (results.get(0) instanceof Long r) ? r : null; + + // 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) { + partySize = Integer.valueOf( + redisTemplate.getStringSerializer().deserialize(b) + ); + } + + // 3) reservationId + String reservationId = null; + Object ridObj = results.get(2); + if (ridObj instanceof String s) { + reservationId = s; + } else if (ridObj instanceof byte[] b) { + reservationId = redisTemplate.getStringSerializer().deserialize(b); + } + + return new WaitingSnapshot(rank, partySize, reservationId); + } } From 4d0b2cf26573fabb61846b85192cf11fe0dec521 Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Sun, 4 Jan 2026 16:57:24 +0900 Subject: [PATCH 03/17] =?UTF-8?q?refactor:=20=EC=9B=A8=EC=9D=B4=ED=8C=85?= =?UTF-8?q?=20=EC=A4=91=EB=B3=B5=20=EC=9A=94=EC=B2=AD=20=EC=8B=9C=20409=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EB=B0=98=ED=99=98=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/GlobalExceptionHandler.java | 2 +- .../service/ReservationService.java | 31 +++++++++---------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/exception/GlobalExceptionHandler.java b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/exception/GlobalExceptionHandler.java index cf09ca46..78c82358 100644 --- a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/exception/GlobalExceptionHandler.java +++ b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/exception/GlobalExceptionHandler.java @@ -215,7 +215,7 @@ public ErrorResponse reservationNotFoundException(ReservationNotFoundException e return new ErrorResponse(e.getMessage(), NOTFOUND_RESERVATION.getCode()); } - @ResponseStatus(BAD_REQUEST) + @ResponseStatus(CONFLICT) @ExceptionHandler(DuplicateReservationException.class) public ErrorResponse duplicateReservationException(DuplicateReservationException e, WebRequest request) { alarm(e, request); diff --git a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java index d4ec8ab0..38cc9288 100644 --- a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java +++ b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java @@ -22,6 +22,7 @@ import com.nowait.applicationuser.reservation.dto.ReservationCreateRequestDto; import com.nowait.applicationuser.reservation.dto.ReservationCreateResponseDto; import com.nowait.applicationuser.reservation.dto.WaitingResponseDto; +import com.nowait.applicationuser.reservation.dto.WaitingSnapshot; import com.nowait.applicationuser.reservation.repository.WaitingUserRedisRepository; import com.nowait.common.enums.ReservationStatus; import com.nowait.common.enums.Role; @@ -118,16 +119,10 @@ public WaitingResponseDto registerWaiting(Long storeId, CustomOAuth2User princip String userId = user.getId().toString(); Duration ttlTo3am = waitingUserRedisRepository.calculateTTLUntilNext03AM(); - // 1) 이미 해당 store에 대기 중이면 임대 없이 현재 상태 반환 (중복 요청 허용) - if (Boolean.TRUE.equals(waitingUserRedisRepository.isUserWaiting(storeId, userId))) { - Long rank = waitingUserRedisRepository.getRank(storeId, userId); - Integer ps = waitingUserRedisRepository.getPartySize(storeId, userId); - String reservationId = waitingUserRedisRepository.getReservationId(storeId, userId); - return WaitingResponseDto.builder() - .reservationNumber(reservationId) - .rank(rank == null ? -1 : rank.intValue() + 1) - .partySize(ps == null ? 0 : ps) - .build(); + // 1) 이미 해당 store에 대기 중이면 임대 없이 현재 상태 반환 (중복 요청 허용 X) + WaitingSnapshot waitingSnapshot = waitingUserRedisRepository.getWaitingSnapshot(storeId, userId); + if (waitingSnapshot.getRank() != null) { + throw new DuplicateReservationException(); } // 1) 임대 획득 @@ -146,23 +141,25 @@ public WaitingResponseDto registerWaiting(Long storeId, CustomOAuth2User princip } } - String reservationId = null; + String reservationId = ""; try { // 2) 스토어 큐 등록(기존 메서드 그대로) long ts = System.currentTimeMillis(); - reservationId = waitingUserRedisRepository.addToWaitingQueue(storeId, userId, dto.getPartySize(), ts); - if (reservationId == null) + // reservationId = waitingUserRedisRepository.addToWaitingQueue(storeId, userId, dto.getPartySize(), ts); + reservationId = waitingUserRedisRepository.addToWaitingQueueLua(storeId, userId, dto.getPartySize(), ts, ttlTo3am); + if (reservationId.isEmpty()) throw new ReservationNumberIssueFailException(); // 3) 확정(holding→active) waitingPermitLuaRepository.finalizeActive(userId, token, String.valueOf(storeId), reservationId, ttlTo3am); + WaitingSnapshot after = waitingUserRedisRepository.getWaitingSnapshot(storeId, userId); + // 4) 응답 - Long rank = waitingUserRedisRepository.getRank(storeId, userId); return WaitingResponseDto.builder() - .reservationNumber(reservationId) - .rank(rank == null ? -1 : rank.intValue() + 1) - .partySize(dto.getPartySize() == null ? 0 : dto.getPartySize()) + .reservationNumber(after.getReservationId()) + .rank(after.getRank() == null ? -1 : after.getRank().intValue() + 1) + .partySize(after.getPartySize() == null ? 0 : after.getPartySize()) .build(); } catch (RuntimeException e) { From 7a9f9d14dacbfc05335f9b5772caaafaf52f4dfa Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Sun, 4 Jan 2026 17:30:12 +0900 Subject: [PATCH 04/17] =?UTF-8?q?refactor:=20=EC=9E=84=EB=8C=80=20?= =?UTF-8?q?=EB=B0=98=EB=82=A9=20=EA=B0=9C=EC=88=98=20=EC=A0=80=EC=9E=A5?= =?UTF-8?q?=ED=95=A0=20redis=20=ED=82=A4=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/nowait/domaincoreredis/common/util/RedisKeyUtils.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/common/util/RedisKeyUtils.java b/nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/common/util/RedisKeyUtils.java index 1f81d2f8..b27505c9 100644 --- a/nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/common/util/RedisKeyUtils.java +++ b/nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/common/util/RedisKeyUtils.java @@ -80,6 +80,8 @@ public static String buildReservationUserKey(Long storeId) { return String.format("reservation:user:%d", storeId); } + public static String buildUserLeaseCountKey(String userId) { return "userID:{" + userId + "}:lease:cnt"; } + /** * 대기 호출 시각(hash)에 사용할 키 접두사 */ From 53a457faeca403f40721e253644d3c0052913900 Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Sun, 4 Jan 2026 17:30:22 +0900 Subject: [PATCH 05/17] =?UTF-8?q?refactor:=20scard=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../WaitingPermitLuaRepository.java | 100 ++++++++++++++++-- 1 file changed, 91 insertions(+), 9 deletions(-) diff --git a/nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/reservation/repository/WaitingPermitLuaRepository.java b/nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/reservation/repository/WaitingPermitLuaRepository.java index ffab7c60..71fb8fc9 100644 --- a/nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/reservation/repository/WaitingPermitLuaRepository.java +++ b/nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/reservation/repository/WaitingPermitLuaRepository.java @@ -29,29 +29,95 @@ public class WaitingPermitLuaRepository { "redis.call('ZADD', KEYS[1], tonumber(ARGV[1]) + tonumber(ARGV[2]), ARGV[4]);" + "return 1;"; + private static final String ACQUIRE_SCRIPT_V2 = + """ + -- KEYS[1] = holding zset (u:{uid}:holding) + -- KEYS[2] = active set (u:{uid}:active) + -- KEYS[3] = lease count (u:{uid}:lease:cnt) + + -- ARGV[1] = nowMs + -- ARGV[2] = leaseMs + -- ARGV[3] = limit + -- ARGV[4] = token + -- ARGV[5] = ttlMs + + -- 1) 만료된 holding 정리 + cnt 보정 + local removed = redis.call('ZREMRANGEBYSCORE', KEYS[1], '-inf', ARGV[1]) + if removed and removed > 0 then + local after = redis.call('DECRBY', KEYS[3], removed) + if after < 0 then redis.call('SET', KEYS[3], 0) end + end + + -- 2) 정확한 limit 체크 (핵심) + local active = redis.call('SCARD', KEYS[2]) + local holding = redis.call('ZCARD', KEYS[1]) + + if (active + holding) >= tonumber(ARGV[3]) then + return 0 + end + + -- 3) lease 카운트 증가 + redis.call('INCR', KEYS[3]) + + -- 4) holding 추가 + redis.call( + 'ZADD', + KEYS[1], + tonumber(ARGV[1]) + tonumber(ARGV[2]), + ARGV[4] + ) + + -- 5) TTL 동기화 + redis.call('PEXPIRE', KEYS[1], ARGV[5]) + redis.call('PEXPIRE', KEYS[2], ARGV[5]) + redis.call('PEXPIRE', KEYS[3], ARGV[5]) + + return 1 + """; + private static final String FINALIZE_SCRIPT = "redis.call('ZREM', KEYS[1], ARGV[1]);" + "redis.call('SADD', KEYS[2], ARGV[2]);" + "return 1;"; + private static final String RELEASE_SCRIPT_V2 = + """ + local removed = redis.call('ZREM', KEYS[1], ARGV[1]) + if removed == 1 then + local cnt = redis.call('DECR', KEYS[2]) + if cnt < 0 then redis.call('SET', KEYS[2], 0) end + end + return removed + """; + + private static final String REMOVE_ACTIVE_SCRIPT = + """ + local removed = redis.call('SREM', KEYS[1], ARGV[1]) + if removed == 1 then + local cnt = redis.call('DECR', KEYS[2]) + if cnt < 0 then redis.call('SET', KEYS[2], 0) end + end + return removed + """; + + public boolean acquireLease(String userId, String token, long nowMs, long leaseMs, int limit, Duration ttlTo3am) { final String hk = RedisKeyUtils.buildUserHoldingKey(userId); // u:{uid}:holding final String ak = RedisKeyUtils.buildUserActiveKey(userId); // u:{uid}:active + final String ck = RedisKeyUtils.buildUserLeaseCountKey(userId); // u:{uid}:lease:cnt Long ok = redis.execute((RedisCallback) conn -> { Object res = conn.eval( - ACQUIRE_SCRIPT.getBytes(StandardCharsets.UTF_8), + ACQUIRE_SCRIPT_V2.getBytes(StandardCharsets.UTF_8), ReturnType.INTEGER, - 2, - raw(hk), raw(ak), + 3, + raw(hk), raw(ak), raw(ck), raw(Long.toString(nowMs)), raw(Long.toString(leaseMs)), raw(Integer.toString(limit)), - raw(token) + raw(token), + raw(Long.toString(ttlTo3am.toMillis())) ); - // TTL 정렬(스크립트 밖에서) - conn.pExpire(raw(hk), ttlTo3am.toMillis()); - conn.pExpire(raw(ak), ttlTo3am.toMillis()); return (Long) res; }); return ok != null && ok == 1L; @@ -77,8 +143,16 @@ public void finalizeActive(String userId, String token, String storeId, String r public void releaseLease(String userId, String token) { final String hk = RedisKeyUtils.buildUserHoldingKey(userId); + final String ck = RedisKeyUtils.buildUserLeaseCountKey(userId); + redis.execute((RedisCallback) conn -> { - conn.zRem(raw(hk), raw(token)); + conn.eval( + RELEASE_SCRIPT_V2.getBytes(StandardCharsets.UTF_8), + ReturnType.INTEGER, + 2, + raw(hk), raw(ck), + raw(token) + ); return null; }); } @@ -96,9 +170,17 @@ public Set getActiveMembers(String userId) { public void removeActiveMember(String userId, String storeId, String reservationId) { final String ak = RedisKeyUtils.buildUserActiveKey(userId); + final String ck = RedisKeyUtils.buildUserLeaseCountKey(userId); final String member = storeId + ":" + reservationId; + redis.execute((RedisCallback) conn -> { - conn.sRem(raw(ak), raw(member)); + conn.eval( + REMOVE_ACTIVE_SCRIPT.getBytes(StandardCharsets.UTF_8), + ReturnType.INTEGER, + 2, + raw(ak), raw(ck), + raw(member) + ); return null; }); } From 2242469cf189521c7b1cc325ee110e55fe88b0ac Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Sun, 4 Jan 2026 17:38:53 +0900 Subject: [PATCH 06/17] =?UTF-8?q?refactor:=20isEmpty()=20=EC=B2=B4?= =?UTF-8?q?=ED=81=AC=EA=B0=80=20=EC=A4=91=EB=B3=B5=20=EC=BC=80=EC=9D=B4?= =?UTF-8?q?=EC=8A=A4=EB=A5=BC=20=EA=B0=90=EC=A7=80=ED=95=A0=20=EC=88=98=20?= =?UTF-8?q?=EC=9E=88=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../applicationuser/reservation/service/ReservationService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java index 38cc9288..59c1ea2c 100644 --- a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java +++ b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java @@ -141,7 +141,7 @@ public WaitingResponseDto registerWaiting(Long storeId, CustomOAuth2User princip } } - String reservationId = ""; + String reservationId; try { // 2) 스토어 큐 등록(기존 메서드 그대로) long ts = System.currentTimeMillis(); From a95159ff107632b460985b3c103705d90f52380b Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Sun, 4 Jan 2026 17:40:25 +0900 Subject: [PATCH 07/17] =?UTF-8?q?refactor:=20deserialize=20=EA=B2=B0?= =?UTF-8?q?=EA=B3=BC=EC=97=90=20=EB=8C=80=ED=95=9C=20null=20=EC=B2=B4?= =?UTF-8?q?=ED=81=AC=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reservation/repository/WaitingUserRedisRepository.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java index 9d36def5..a6f3c452 100644 --- a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java +++ b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java @@ -332,9 +332,8 @@ public WaitingSnapshot getWaitingSnapshot(Long storeId, String userId) { if (psObj instanceof String s) { partySize = Integer.valueOf(s); } else if (psObj instanceof byte[] b) { - partySize = Integer.valueOf( - redisTemplate.getStringSerializer().deserialize(b) - ); + String deserialized = redisTemplate.getStringSerializer().deserialize(b); + partySize = deserialized != null ? Integer.valueOf(deserialized) : null; } // 3) reservationId From 2d08d5bef7505403268006443f33b2c42a7c1464 Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Sun, 4 Jan 2026 17:44:08 +0900 Subject: [PATCH 08/17] =?UTF-8?q?refactor:=20Lua=20=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=EB=A6=BD=ED=8A=B8=20=EB=B0=98=ED=99=98=EA=B0=92=EC=9D=98=20?= =?UTF-8?q?=EC=84=B1=EA=B3=B5/=EC=A4=91=EB=B3=B5=20=EA=B5=AC=EB=B6=84?= =?UTF-8?q?=EC=9D=B4=20=EB=88=84=EB=9D=BD=20=EC=BC=80=EC=9D=B4=EC=8A=A4=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 --- .../repository/WaitingUserRedisRepository.java | 14 ++++++++++++-- .../reservation/service/ReservationService.java | 8 ++++---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java index a6f3c452..15fdcf54 100644 --- a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java +++ b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java @@ -113,7 +113,7 @@ public String addToWaitingQueue(Long storeId, String userId, Integer partySize, } // 루아 스크립트 사용 - public String addToWaitingQueueLua( + public WaitingSnapshot addToWaitingQueueLua( Long storeId, String userId, Integer partySize, @@ -151,8 +151,18 @@ public String addToWaitingQueueLua( @SuppressWarnings("unchecked") List response = (List) result; - return response.size() >= 2 ? String.valueOf(response.get(1)) : 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)); + + // added == 0이면 중복, 1이면 신규 등록 + // 중복인 경우 rank를 null로 반환하여 구분 가능하게 함 + if (added == 0) { + return new WaitingSnapshot(null, partySize, reservationId); // 기존 데이터 + } + + return new WaitingSnapshot(0L, partySize, reservationId); // 신규 등록 } // 예약한 사람이 등록한 동반인원(partySize) 조회 diff --git a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java index 59c1ea2c..5c67fb19 100644 --- a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java +++ b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java @@ -141,17 +141,17 @@ public WaitingResponseDto registerWaiting(Long storeId, CustomOAuth2User princip } } - String reservationId; + WaitingSnapshot snapshot; try { // 2) 스토어 큐 등록(기존 메서드 그대로) long ts = System.currentTimeMillis(); // reservationId = waitingUserRedisRepository.addToWaitingQueue(storeId, userId, dto.getPartySize(), ts); - reservationId = waitingUserRedisRepository.addToWaitingQueueLua(storeId, userId, dto.getPartySize(), ts, ttlTo3am); - if (reservationId.isEmpty()) + snapshot = waitingUserRedisRepository.addToWaitingQueueLua(storeId, userId, dto.getPartySize(), ts, ttlTo3am); + if (snapshot.getReservationId().isEmpty()) throw new ReservationNumberIssueFailException(); // 3) 확정(holding→active) - waitingPermitLuaRepository.finalizeActive(userId, token, String.valueOf(storeId), reservationId, ttlTo3am); + waitingPermitLuaRepository.finalizeActive(userId, token, String.valueOf(storeId), snapshot.getReservationId(), ttlTo3am); WaitingSnapshot after = waitingUserRedisRepository.getWaitingSnapshot(storeId, userId); From a9628c56486279f01c3a3789464d4dd589898a31 Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Sun, 4 Jan 2026 17:50:42 +0900 Subject: [PATCH 09/17] =?UTF-8?q?refactor:=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=EC=8B=9C=20partySize=20=EB=B0=98=ED=99=98=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reservation/repository/WaitingUserRedisRepository.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java index 15fdcf54..11265a63 100644 --- a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java +++ b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java @@ -159,10 +159,13 @@ public WaitingSnapshot addToWaitingQueueLua( // added == 0이면 중복, 1이면 신규 등록 // 중복인 경우 rank를 null로 반환하여 구분 가능하게 함 if (added == 0) { - return new WaitingSnapshot(null, partySize, reservationId); // 기존 데이터 + Integer existingPartySize = getPartySize(storeId, userId); + Long existingRank = getRank(storeId, userId); + return new WaitingSnapshot(existingRank, existingPartySize, reservationId); } - return new WaitingSnapshot(0L, partySize, reservationId); // 신규 등록 + Long actualRank = getRank(storeId, userId); + return new WaitingSnapshot(actualRank, partySize, reservationId); } // 예약한 사람이 등록한 동반인원(partySize) 조회 From 2a774ef3f453bf3b6ec9a5fe4c56203a58fc8671 Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Sun, 4 Jan 2026 17:52:15 +0900 Subject: [PATCH 10/17] =?UTF-8?q?refactor:=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=EC=B2=B4=ED=81=AC=20=EC=8B=9C=EC=A0=90=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reservation/service/ReservationService.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java index 5c67fb19..86ec2448 100644 --- a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java +++ b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java @@ -119,12 +119,6 @@ public WaitingResponseDto registerWaiting(Long storeId, CustomOAuth2User princip String userId = user.getId().toString(); Duration ttlTo3am = waitingUserRedisRepository.calculateTTLUntilNext03AM(); - // 1) 이미 해당 store에 대기 중이면 임대 없이 현재 상태 반환 (중복 요청 허용 X) - WaitingSnapshot waitingSnapshot = waitingUserRedisRepository.getWaitingSnapshot(storeId, userId); - if (waitingSnapshot.getRank() != null) { - throw new DuplicateReservationException(); - } - // 1) 임대 획득 String token = java.util.UUID.randomUUID().toString(); int attempts = 0; @@ -141,6 +135,13 @@ public WaitingResponseDto registerWaiting(Long storeId, CustomOAuth2User princip } } + // 2) 임대 획득 후 중복 체크 + WaitingSnapshot existingSnapshot = waitingUserRedisRepository.getWaitingSnapshot(storeId, userId); + if (existingSnapshot.getRank() != null) { + waitingPermitLuaRepository.releaseLease(userId, token); + throw new DuplicateReservationException(); + } + WaitingSnapshot snapshot; try { // 2) 스토어 큐 등록(기존 메서드 그대로) From fafef12a5a8b1420fa8a4a1f663849520543425f Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Sun, 4 Jan 2026 18:05:25 +0900 Subject: [PATCH 11/17] =?UTF-8?q?refactor:=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reservation/service/ReservationService.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java index 86ec2448..40776ae5 100644 --- a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java +++ b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java @@ -142,17 +142,15 @@ public WaitingResponseDto registerWaiting(Long storeId, CustomOAuth2User princip throw new DuplicateReservationException(); } - 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()) + existingSnapshot = waitingUserRedisRepository.addToWaitingQueueLua(storeId, userId, dto.getPartySize(), ts, ttlTo3am); + if (existingSnapshot.getReservationId().isEmpty()) throw new ReservationNumberIssueFailException(); - // 3) 확정(holding→active) - waitingPermitLuaRepository.finalizeActive(userId, token, String.valueOf(storeId), snapshot.getReservationId(), ttlTo3am); + // 3) 확정(holding → active) + waitingPermitLuaRepository.finalizeActive(userId, token, String.valueOf(storeId), existingSnapshot.getReservationId(), ttlTo3am); WaitingSnapshot after = waitingUserRedisRepository.getWaitingSnapshot(storeId, userId); From ea022d2f42fdd915ca41254bf47784e05fac77a2 Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Sun, 4 Jan 2026 18:42:33 +0900 Subject: [PATCH 12/17] =?UTF-8?q?refactor:=20=EC=98=88=EC=95=BD=20isNew=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EC=B2=98=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reservation/dto/WaitingSnapshot.java | 1 + .../WaitingUserRedisRepository.java | 105 +++++++++--------- .../service/ReservationService.java | 29 ++++- 3 files changed, 78 insertions(+), 57 deletions(-) diff --git a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/dto/WaitingSnapshot.java b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/dto/WaitingSnapshot.java index 651b3959..7f75bdbb 100644 --- a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/dto/WaitingSnapshot.java +++ b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/dto/WaitingSnapshot.java @@ -9,4 +9,5 @@ public class WaitingSnapshot { private final Long rank; private final Integer partySize; private final String reservationId; + private final boolean isNew; } diff --git a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java index 11265a63..e0a63137 100644 --- a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java +++ b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java @@ -31,46 +31,50 @@ public class WaitingUserRedisRepository { private final StringRedisTemplate redisTemplate; private static final String ADD_WAITING_LUA = """ - -- KEYS - -- 1: queueKey - -- 2: partyKey - -- 3: statusKey - -- 4: numberMapKey - -- 5: dailySeqKey - - -- ARGV - -- 1: userId - -- 2: timestamp (ms) - -- 3: partySize - -- 4: today (YYYYMMDD) - -- 5: storeId - -- 6: ttlMillis - - -- 1) 큐 등록 (중복 방지) - local added = redis.call('ZADD', KEYS[1], 'NX', ARGV[2], ARGV[1]) - if added == 0 then - local rid = redis.call('HGET', KEYS[4], ARGV[1]) - return {0, rid} - end - - -- 2) 일일 시퀀스 - local seq = redis.call('INCR', KEYS[5]) - local seqStr = string.format('%04d', seq) - local reservationId = ARGV[5] .. '-' .. ARGV[4] .. '-' .. seqStr - - -- 3) 메타 저장 - 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') - - -- 4) TTL은 최초 1회만 - if redis.call('PTTL', KEYS[1]) < 0 then - for i = 1, #KEYS do - redis.call('PEXPIRE', KEYS[i], ARGV[6]) - end - end - - return {1, reservationId} + -- KEYS + -- 1: queueKey + -- 2: partyKey + -- 3: statusKey + -- 4: numberMapKey + -- 5: dailySeqKey + + -- ARGV + -- 1: userId + -- 2: timestamp (ms) + -- 3: partySize + -- 4: today (YYYYMMDD) + -- 5: storeId + -- 6: ttlMillis + + -- 1) ZADD NX + local added = redis.call('ZADD', KEYS[1], 'NX', ARGV[2], ARGV[1]) + + local isNew = 0 + local reservationId + + 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]) + end + + local rank = redis.call('ZRANK', KEYS[1], ARGV[1]) + + -- TTL: 키가 처음 생성된 경우만 + if redis.call('PTTL', KEYS[1]) == -1 then + for i = 1, #KEYS do + redis.call('PEXPIRE', KEYS[i], ARGV[6]) + end + end + + return {isNew, rank, ARGV[3], reservationId} """; // 중복 등록 방지: 이미 있으면 추가X @@ -151,21 +155,20 @@ public WaitingSnapshot addToWaitingQueueLua( @SuppressWarnings("unchecked") List response = (List) result; - if (response.size() < 2) return null; + if (response == null || response.size() < 4) 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)); + 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()); + String reservationId = response.get(3) == null ? null : response.get(3).toString(); - // added == 0이면 중복, 1이면 신규 등록 - // 중복인 경우 rank를 null로 반환하여 구분 가능하게 함 - if (added == 0) { - Integer existingPartySize = getPartySize(storeId, userId); - Long existingRank = getRank(storeId, userId); - return new WaitingSnapshot(existingRank, existingPartySize, reservationId); + if (!isNew) { + // 중복 등록: Redis 재조회 없음 + return new WaitingSnapshot(rank, ps, reservationId, false); } - Long actualRank = getRank(storeId, userId); - return new WaitingSnapshot(actualRank, partySize, reservationId); + // 신규 등록 + return new WaitingSnapshot(rank, ps, reservationId, true); } // 예약한 사람이 등록한 동반인원(partySize) 조회 diff --git a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java index 40776ae5..a2dab99a 100644 --- a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java +++ b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java @@ -10,6 +10,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.UUID; import java.util.function.Function; import java.util.stream.Collectors; @@ -120,7 +121,7 @@ public WaitingResponseDto registerWaiting(Long storeId, CustomOAuth2User princip Duration ttlTo3am = waitingUserRedisRepository.calculateTTLUntilNext03AM(); // 1) 임대 획득 - String token = java.util.UUID.randomUUID().toString(); + String token = UUID.randomUUID().toString(); int attempts = 0; while (true) { boolean ok = waitingPermitLuaRepository.acquireLease(userId, token, System.currentTimeMillis(), LEASE_MS, @@ -142,16 +143,30 @@ public WaitingResponseDto registerWaiting(Long storeId, CustomOAuth2User princip throw new DuplicateReservationException(); } + WaitingSnapshot snapshot = null; try { // 2) 스토어 큐 등록(기존 메서드 그대로) long ts = System.currentTimeMillis(); - existingSnapshot = waitingUserRedisRepository.addToWaitingQueueLua(storeId, userId, dto.getPartySize(), ts, ttlTo3am); - if (existingSnapshot.getReservationId().isEmpty()) + + snapshot = waitingUserRedisRepository.addToWaitingQueueLua( + storeId, userId, dto.getPartySize(), ts, ttlTo3am + ); + + if (snapshot == null || snapshot.getReservationId() == null) throw new ReservationNumberIssueFailException(); - // 3) 확정(holding → active) - waitingPermitLuaRepository.finalizeActive(userId, token, String.valueOf(storeId), existingSnapshot.getReservationId(), ttlTo3am); + if (snapshot.isNew()) { + waitingPermitLuaRepository.finalizeActive( + userId, + token, + String.valueOf(storeId), + snapshot.getReservationId(), + ttlTo3am + ); + } + + // 3) 확정(holding → active) WaitingSnapshot after = waitingUserRedisRepository.getWaitingSnapshot(storeId, userId); // 4) 응답 @@ -163,7 +178,9 @@ public WaitingResponseDto registerWaiting(Long storeId, CustomOAuth2User princip } catch (RuntimeException e) { // 실패 시 임대 반납 - waitingPermitLuaRepository.releaseLease(userId, token); + if (snapshot != null && snapshot.isNew()) { + waitingPermitLuaRepository.releaseLease(userId, token); + } throw e; } } From f01a7bf468643610cbef9a0007ed3d3f857b42a1 Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Sun, 4 Jan 2026 18:45:48 +0900 Subject: [PATCH 13/17] =?UTF-8?q?refactor:=20=EC=98=88=EC=95=BD=20isNew=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EC=B2=98=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reservation/repository/WaitingUserRedisRepository.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java index e0a63137..bd340511 100644 --- a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java +++ b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java @@ -336,7 +336,7 @@ public WaitingSnapshot getWaitingSnapshot(Long storeId, String userId) { }); if (results == null || results.size() < 3) { - return new WaitingSnapshot(null, null, null); + return new WaitingSnapshot(null, null, null, false); } // 1) rank @@ -361,7 +361,7 @@ public WaitingSnapshot getWaitingSnapshot(Long storeId, String userId) { reservationId = redisTemplate.getStringSerializer().deserialize(b); } - return new WaitingSnapshot(rank, partySize, reservationId); + return new WaitingSnapshot(rank, partySize, reservationId, false); } } From 3f1fffb487af3ef8eace0967001f46d00d0972bd Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Sun, 4 Jan 2026 18:59:31 +0900 Subject: [PATCH 14/17] =?UTF-8?q?refactor:=20=EC=9E=84=EB=8C=80=20?= =?UTF-8?q?=EB=88=84=EC=88=98=20=EC=BC=80=EC=9D=B4=EC=8A=A4=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../applicationuser/reservation/service/ReservationService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java index a2dab99a..e19be0b9 100644 --- a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java +++ b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java @@ -178,7 +178,7 @@ public WaitingResponseDto registerWaiting(Long storeId, CustomOAuth2User princip } catch (RuntimeException e) { // 실패 시 임대 반납 - if (snapshot != null && snapshot.isNew()) { + if (snapshot != null || snapshot.isNew()) { waitingPermitLuaRepository.releaseLease(userId, token); } throw e; From 28138d357602eb21bcf9bd35bda2e1fac17c0869 Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Mon, 5 Jan 2026 17:02:38 +0900 Subject: [PATCH 15/17] =?UTF-8?q?refactor:=20isNew=20=3D=20false=20?= =?UTF-8?q?=EC=BC=80=EC=9D=B4=EC=8A=A4=20=EB=88=84=EB=9D=BD=20=EB=B3=B4?= =?UTF-8?q?=EC=99=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reservation/service/ReservationService.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java index e19be0b9..14ee1429 100644 --- a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java +++ b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java @@ -164,6 +164,10 @@ public WaitingResponseDto registerWaiting(Long storeId, CustomOAuth2User princip snapshot.getReservationId(), ttlTo3am ); + } else { + // Lua 스크립트 중복 감지 케이스 + waitingPermitLuaRepository.releaseLease(userId, token); + throw new DuplicateReservationException(); } // 3) 확정(holding → active) From 9939f72425630349d868b05c38cdee71c03f8d9d Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Mon, 5 Jan 2026 17:08:17 +0900 Subject: [PATCH 16/17] =?UTF-8?q?refactor:=20existingSnapshot=20null=20?= =?UTF-8?q?=EC=B2=B4=ED=81=AC=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../applicationuser/reservation/service/ReservationService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java index 14ee1429..0f9e8cf1 100644 --- a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java +++ b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java @@ -138,7 +138,7 @@ public WaitingResponseDto registerWaiting(Long storeId, CustomOAuth2User princip // 2) 임대 획득 후 중복 체크 WaitingSnapshot existingSnapshot = waitingUserRedisRepository.getWaitingSnapshot(storeId, userId); - if (existingSnapshot.getRank() != null) { + if (existingSnapshot != null && existingSnapshot.getRank() != null) { waitingPermitLuaRepository.releaseLease(userId, token); throw new DuplicateReservationException(); } From 114de85058830de2ae4caa5aeeeab0584dae2d81 Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Mon, 5 Jan 2026 17:10:31 +0900 Subject: [PATCH 17/17] =?UTF-8?q?refactor:=20null=20=EC=B2=B4=ED=81=AC=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reservation/service/ReservationService.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java index 0f9e8cf1..ca69a598 100644 --- a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java +++ b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java @@ -172,6 +172,8 @@ public WaitingResponseDto registerWaiting(Long storeId, CustomOAuth2User princip // 3) 확정(holding → active) WaitingSnapshot after = waitingUserRedisRepository.getWaitingSnapshot(storeId, userId); + if (after == null) + throw new ReservationNumberIssueFailException(); // 4) 응답 return WaitingResponseDto.builder() @@ -182,7 +184,7 @@ public WaitingResponseDto registerWaiting(Long storeId, CustomOAuth2User princip } catch (RuntimeException e) { // 실패 시 임대 반납 - if (snapshot != null || snapshot.isNew()) { + if (snapshot == null || (snapshot.isNew())) { waitingPermitLuaRepository.releaseLease(userId, token); } throw e;