22
33import java .time .LocalDateTime ;
44import java .time .format .DateTimeFormatter ;
5- import java .util .Optional ;
65
76import org .springframework .context .ApplicationEventPublisher ;
87import org .springframework .stereotype .Service ;
1312import com .nowait .applicationuser .waiting .dto .GetWaitingSizeResponse ;
1413import com .nowait .applicationuser .waiting .dto .RegisterWaitingRequest ;
1514import com .nowait .applicationuser .waiting .dto .RegisterWaitingResponse ;
15+ import com .nowait .applicationuser .waiting .dto .WaitingCancelIdempotencyValue ;
1616import com .nowait .applicationuser .waiting .dto .WaitingIdempotencyValue ;
1717import com .nowait .applicationuser .waiting .event .AddWaitingRegisterEvent ;
1818import com .nowait .applicationuser .waiting .redis .WaitingIdempotencyRepository ;
@@ -59,13 +59,11 @@ public class WaitingService {
5959 @ Transactional
6060 public RegisterWaitingResponse registerWaiting (CustomOAuth2User oAuth2User , String publicCode , RegisterWaitingRequest waitingRequest , HttpServletRequest httpServletRequest ) {
6161
62- String idempotentKey = httpServletRequest .getHeader ("Idempotency-Key" );
63-
64- // TODO 멱등성 검증 로직 점검 필요
65- Optional <WaitingIdempotencyValue > existingIdempotencyValue = waitingIdempotencyRepository .findByKey (idempotentKey );
66- if (existingIdempotencyValue .isPresent ()) {
67- log .info ("Existing idempotency key found: {}" , idempotentKey );
68- return existingIdempotencyValue .get ().getResponse ();
62+ // TODO 멱등키 동시성 처리 로직 고려 필요 (분산락 등)
63+ RegisterWaitingResponse registerWaitingResponse = validateIdempotency (httpServletRequest );
64+ if (registerWaitingResponse != null ) {
65+ log .info ("Idempotent request detected. Returning existing response." );
66+ return registerWaitingResponse ;
6967 }
7068
7169 // TODO 유저 및 주점 존재 검증은 공통으로 많이 쓰이니 AOP로 빼는게 좋을 듯
@@ -75,19 +73,15 @@ public RegisterWaitingResponse registerWaiting(CustomOAuth2User oAuth2User, Stri
7573 User user = userRepository .findById (oAuth2User .getUserId ())
7674 .orElseThrow (UserNotFoundException ::new );
7775
76+ // 일일 가능 웨이팅 최대 개수 초과 검증
77+ // TODO race condition 발생 가능성 점검 필요, DB 저장 로직 실패 시 롤백 처리 필요
78+ waitingRedisRepository .incrementAndCheckWaitingLimit (user .getId (), 3L );
79+
7880 // 웨이팅 고유 번호 생성 - YYYYMMDD-storeId-sequence number 일련 번호
7981 Long storeId = store .getStoreId ();
8082 LocalDateTime timestamp = LocalDateTime .now ();
8183 String waitingNumber = generateWaitingNumber (storeId , timestamp );
8284
83- // 멱등키 검증 - 이미 동일한 멱등키로 등록된 웨이팅이 있는지 확인
84- // waitingRedisRepository.idempotentKeyKeyExists(idempotentKey, ReservationStatus.WAITING.name());
85-
86-
87- // 일일 가능 웨이팅 최대 개수 초과 검증
88- // TODO race condition 발생 가능성 점검 필요
89- waitingRedisRepository .incrementAndCheckWaitingLimit (user .getId (), 3L );
90-
9185 // DB에 상태 값 저장
9286 Reservation reservation = Reservation .builder ()
9387 .reservationNumber (waitingNumber )
@@ -115,25 +109,27 @@ public RegisterWaitingResponse registerWaiting(CustomOAuth2User oAuth2User, Stri
115109 .partySize (waitingRequest .getPartySize ())
116110 .build ();
117111
118- // 멱등키가 있다면 멱등 응답 저장
119- waitingIdempotencyRepository . saveIdempotencyValue ( idempotentKey , response );
112+ // TODO 멱등키 응답 실패 시 어떻게 처리할 지 점검 필요
113+ saveIdempotencyResponse ( httpServletRequest . getHeader ( "Idempotency-Key" ) , response );
120114
121115 return response ;
122116 }
123117
124118 @ Transactional
125- public CancelWaitingResponse cancelWaiting (CustomOAuth2User oAuth2User , String publicCode , CancelWaitingRequest request ) {
119+ public CancelWaitingResponse cancelWaiting (CustomOAuth2User oAuth2User , String publicCode , CancelWaitingRequest request , HttpServletRequest httpServletRequest ) {
120+ // TODO 멱등키 동시성 처리 로직 고려 필요 (분산락 등)
121+ CancelWaitingResponse cancelWaitingResponse = validateCancelIdempotency (httpServletRequest );
122+ if (cancelWaitingResponse != null ) {
123+ log .info ("Idempotent request detected. Returning existing response." );
124+ return cancelWaitingResponse ;
125+ }
126126
127127 Store store = storeRepository .findByPublicCodeAndDeletedFalse (publicCode ).orElseThrow (StoreNotFoundException ::new );
128128 Long storeId = store .getStoreId ();
129129
130130 User user = userRepository .findById (oAuth2User .getUserId ())
131131 .orElseThrow (UserNotFoundException ::new );
132132
133- // TODO 멱등키 검증 로직 점검 필요
134- // String idempotentKey = generateIdempotentKey(storeId, user.getId());
135- // waitingRedisRepository.idempotentKeyKeyExists(idempotentKey, ReservationStatus.CANCELLED.name());
136-
137133 // DB 웨이팅 상태 취소 처리
138134 Reservation reservation = reservationRepository .findReservationByReservationNumber (request .getWaitingNumber ())
139135 .orElseThrow (ReservationNotFoundException ::new );
@@ -143,28 +139,62 @@ public CancelWaitingResponse cancelWaiting(CustomOAuth2User oAuth2User, String p
143139 // Redis 대기열 취소 이벤트 발행
144140 waitingRedisRepository .removeWaiting (storeId , user .getId ());
145141
146- return CancelWaitingResponse .builder ()
142+ CancelWaitingResponse response = CancelWaitingResponse .builder ()
147143 .waitingNumber (reservation .getReservationNumber ())
148144 .storeId (storeId )
149145 .reservationStatus (reservation .getStatus ())
150146 .canceledAt (reservation .getUpdatedAt ())
151147 .message ("대기 취소가 완료되었습니다." )
152148 .build ();
149+
150+ // 멱등키가 있다면 멱등 응답 저장
151+ waitingIdempotencyRepository .saveCancelIdempotencyValue (httpServletRequest .getHeader ("Idempotency-Key" ), response );
152+
153+ return response ;
154+ }
155+
156+ // 멱등키 검증 메서드
157+ private RegisterWaitingResponse validateIdempotency (HttpServletRequest httpServletRequest ) {
158+ String idempotentKey = httpServletRequest .getHeader ("Idempotency-Key" );
159+
160+ // 멱등키 검증 - 이미 동일한 멱등키로 등록된 웨이팅이 있는지 확인
161+ // TODO 멱등성 검증 로직 점검 필요
162+ return waitingIdempotencyRepository .findByKey (idempotentKey )
163+ .map (WaitingIdempotencyValue ::getResponse )
164+ .orElse (null );
165+ }
166+
167+ private CancelWaitingResponse validateCancelIdempotency (HttpServletRequest httpServletRequest ) {
168+ String idempotentKey = httpServletRequest .getHeader ("Idempotency-Key" );
169+
170+ // 멱등키 검증 - 이미 동일한 멱등키로 등록된 웨이팅이 있는지 확인
171+ // TODO 멱등성 검증 로직 점검 필요
172+ return waitingIdempotencyRepository .findByCancelKey (idempotentKey )
173+ .map (WaitingCancelIdempotencyValue ::getResponse )
174+ .orElse (null );
153175 }
154176
177+ // 멱등키 응답 저장 메서드
178+ private void saveIdempotencyResponse (String idempotentKey , RegisterWaitingResponse response ) {
179+ if (idempotentKey != null && !idempotentKey .isBlank ()) {
180+ waitingIdempotencyRepository .saveIdempotencyValue (idempotentKey , response );
181+ }
182+ }
183+
184+ // 현재 대기 인원 수 조회
155185 public GetWaitingSizeResponse getWaitingCount (CustomOAuth2User oAuth2User , String publicCode ) {
156186
157187 Store store = storeRepository .findByPublicCodeAndDeletedFalse (publicCode )
158188 .orElseThrow (StoreNotFoundException ::new );
159189
160- Long storeId = store .getStoreId ();
161-
162190 User user = userRepository .findById (oAuth2User .getUserId ())
163191 .orElseThrow (UserNotFoundException ::new );
164192
165193 Department department = departmentRepository .findById (store .getDepartmentId ())
166194 .orElseThrow (DepartmentNotFoundException ::new );
167195
196+ Long storeId = store .getStoreId ();
197+
168198 Long waitingCount = waitingRedisRepository .getWaitingCount (storeId );
169199
170200 return GetWaitingSizeResponse .builder ()
@@ -190,8 +220,4 @@ private String generateWaitingNumber(Long storeId, LocalDateTime timestamp) {
190220 // 4) 최종 ID 조합
191221 return today + "-" + storeId + "-" + seqStr ;
192222 }
193-
194- private String generateIdempotentKey (Long storeId , Long userId ) {
195- return "idempotentKey" + ":" + storeId + ":" + userId ;
196- }
197223}
0 commit comments