-
Notifications
You must be signed in to change notification settings - Fork 2
SCRUM-107 예약 로직 동시성 제어 #30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Walkthrough이 변경 사항은 예약(Reservation) 엔티티에 멤버, 레스토랑, 예약 가능 날짜 조합에 대한 유니크 제약 조건을 추가하고, 관련 데이터베이스 스키마를 업데이트합니다. 또한, 예약 저장 시 제약 조건 위반을 도메인 예외로 변환하는 로직과, 테스트 코드 및 서비스 로직 일부를 리팩토링 및 보완합니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant ReservationService
participant ReservationDomainService
participant Repository
participant RepositoryErrorDecoder
User->>ReservationService: 예약 저장 요청
ReservationService->>ReservationDomainService: save(reservation)
ReservationDomainService->>Repository: save(reservation)
Repository-->>ReservationDomainService: DataIntegrityViolationException?
alt 유니크 제약 위반
ReservationDomainService->>RepositoryErrorDecoder: isUniqueConstraintViolation(exception)
RepositoryErrorDecoder-->>ReservationDomainService: true
ReservationDomainService-->>ReservationService: ReservationException(ALREADY_RESERVED)
else 기타 예외
ReservationDomainService-->>ReservationService: 예외 재전파
else 정상 저장
Repository-->>ReservationDomainService: Reservation
ReservationDomainService-->>ReservationService: Reservation
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Note 🔌 MCP (Model Context Protocol) integration is now available in Early Access!Pro users can now connect to remote MCP servers under the Integrations page to get reviews and chat conversations that understand additional development context. ✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this 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
🧹 Nitpick comments (2)
domain/src/main/resources/schema.sql (2)
81-81: 유니크 키 컬럼 구성 단순화 또는 참조 무결성 보강 제안.
- 단순화: available_date가 이미 특정 restaurant에 종속이라면 (member_id, available_date_id)만으로 동일 의미를 만족합니다. 현재 정의는 인덱스 폭이 커져 쓰기 비용·스토리지가 증가할 수 있습니다.
- 보강: reservation.restaurant_id가 available_date.restaurant_id와 항상 일치함을 DB 차원에서 보장하려면 복합 FK를 고려하세요.
옵션 A) 단순화(선호, 스키마 영향 최소화):
- UNIQUE KEY unique_member_restaurant_available_date (member_id, restaurant_id, available_date_id), + UNIQUE KEY unique_member_restaurant_available_date (member_id, available_date_id),옵션 B) 데이터 무결성 보강(복합 FK):
- 선행: available_date에 (restaurant_id, id) 유니크 인덱스 추가
- 그 후 reservation에 (restaurant_id, available_date_id) → available_date(restaurant_id, id) 복합 FK 추가
장단:
- A: 간결·효율적. 단, reservation.restaurant_id ≠ available_date.restaurant_id 형태의 비일관성은 애플리케이션에서 막아야 함.
- B: 일관성 단단해짐. 단, 마이그레이션 및 쿼리 플랜 영향이 큼.
81-81: 중복 데이터 존재 시 적용 실패 가능 — 사전 점검 권고.운영 테이블에 이미 중복이 있으면 유니크 키 추가가 실패합니다. 적용 전 중복을 점검·정리해 주세요.
- 중복 탐지 예시(SQL):
SELECT member_id, restaurant_id, available_date_id, COUNT(*) AS cnt FROM reservation GROUP BY member_id, restaurant_id, available_date_id HAVING COUNT(*) > 1;
- 옵션 A로 단순화한다면:
SELECT member_id, available_date_id, COUNT(*) AS cnt FROM reservation GROUP BY member_id, available_date_id HAVING COUNT(*) > 1;필요하시다면 마이그레이션(정규화/머지 전략) 스크립트도 도와드리겠습니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
domain/src/main/java/com/wellmeet/domain/reservation/entity/Reservation.java(1 hunks)domain/src/main/resources/schema.sql(2 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: ezcolin2
PR: Team-soft-macaron/WellMeet-Backend#4
File: src/main/java/com/wellmeet/restaurant/domain/crawlingreview/domain/Atmosphere.java:15-16
Timestamp: 2025-06-25T08:56:15.148Z
Learning: CrawlingReview와 Restaurant 엔티티는 외부에서 수집한 식별자를 직접 사용하므로 GeneratedValue 없이 수동으로 ID를 설정하는 것이 적합하다.
Learnt from: ezcolin2
PR: Team-soft-macaron/WellMeet-Backend#4
File: src/main/java/com/wellmeet/restaurant/domain/crawlingreview/domain/CrawlingReviewAtmosphere.java:17-18
Timestamp: 2025-06-25T08:57:27.229Z
Learning: CrawlingReviewAtmosphere 엔티티는 크롤링 리뷰와 분위기 간의 관계를 나타내는 중간 테이블이므로 외부 식별자를 보존할 필요가 없어서 GeneratedValue를 사용한 자동 ID 생성 전략을 적용해야 한다.
📚 Learning: 2025-07-10T02:52:48.337Z
Learnt from: unifolio0
PR: Team-soft-macaron/WellMeet-Backend#14
File: src/main/java/com/wellmeet/recommend/dto/RecommendRestaurantResponse.java:3-3
Timestamp: 2025-07-10T02:52:48.337Z
Learning: The Restaurant class is located at `com.wellmeet.restaurant.domain.Restaurant` after the package refactoring in the WellMeet-Backend project, not in a nested model package structure.
Applied to files:
domain/src/main/java/com/wellmeet/domain/reservation/entity/Reservation.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build-and-push
🔇 Additional comments (3)
domain/src/main/resources/schema.sql (2)
81-81: 유니크 키 추가 적절 (중복 예약 방지 + 예외 분기 명시성 향상).(member_id, restaurant_id, available_date_id) 조합에 대한 DB 유니크 키로 동일 회원·식당·시간대의 중복 예약을 DB 레벨에서 확실히 차단합니다. JPA @table의 제약 이름과도 일치해 예외 분기(ConstraintViolation) 시에도 유리합니다. LGTM.
165-165: 비기능적 변경으로 보임.닫는 괄호 라인의 변경은 기능에 영향이 없어 보입니다. 별도 조치 불필요.
domain/src/main/java/com/wellmeet/domain/reservation/entity/Reservation.java (1)
16-18: @Table/UniqueConstraint import 추가 적절.스키마의 유니크 키와 매핑 일치. 컴파일·런타임 문제 소지 없어 보입니다.
| @Table( | ||
| name = "reservation", | ||
| uniqueConstraints = @UniqueConstraint( | ||
| name = "unique_member_restaurant_available_date", | ||
| columnNames = {"member_id", "restaurant_id", "available_date_id"} | ||
| ) | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
제약 이름 지정으로 저장 시 예외 분기 안정화 — 구현 가이드 제안.
이름이 부여된 유니크 제약(“unique_member_restaurant_available_date”)은 저장 시 중복 예약을 명확히 식별하는 데 유용합니다. 메시지 파싱 대신 제약 이름으로 분기하세요.
- Spring/Hibernate 예외 매핑 예시(서비스/어댑터 계층):
import org.springframework.dao.DataIntegrityViolationException;
import org.hibernate.exception.ConstraintViolationException;
public void createReservation(...) {
try {
reservationRepository.save(reservation);
} catch (DataIntegrityViolationException ex) {
ConstraintViolationException cve = unwrapConstraintViolation(ex);
if (cve != null && "unique_member_restaurant_available_date".equals(cve.getConstraintName())) {
// 동일 회원·식당·시간대 중복 예약
throw new ReservationException(ReservationErrorCode.DUPLICATE_RESERVATION);
}
// 그 외 DB 오류는 상위 핸들러로
throw ex;
}
}
private ConstraintViolationException unwrapConstraintViolation(Throwable t) {
while (t != null) {
if (t instanceof ConstraintViolationException cve) return cve;
t = t.getCause();
}
return null;
}- 추가 권고:
- 동시성 경합이 높은 구간에서는 DeadlockLoserDataAccessException/Lock wait timeout 대비 재시도 정책(예: Spring Retry, 지수 백오프) 적용을 권장합니다.
원하시면 공통 예외 변환기/리트라이 어드바이스를 만들어 드릴게요.
다음 스크립트로 코드베이스에 이미 제약 이름 기반 분기가 있는지 확인하세요.
🏁 Script executed:
#!/bin/bash
rg -n "unique_member_restaurant_available_date|ConstraintViolationException|DeadlockLoserDataAccessException|ER_DUP_ENTRY" -SLength of output: 877
서비스 계층에 제약 이름 기반 예외 처리를 추가해주세요.
현재 코드베이스에서 DB 제약 위반(“unique_member_restaurant_available_date”)을 잡아내는 로직은 없고, GlobalExceptionHandler는 Bean Validation용 jakarta.validation.ConstraintViolationException만 처리하고 있습니다. 따라서 저장 시 중복 예약을 명확히 식별하고 재시도 정책을 적용하려면 아래를 적용하는 것을 권장드립니다.
-
처리 대상
ReservationService#createReservation(...)(또는 예약 저장을 담당하는 서비스/어댑터 클래스)
-
구현 예시
public void createReservation(Reservation reservation) {
- reservationRepository.save(reservation);
+ try {
+ reservationRepository.save(reservation);
+ } catch (DataIntegrityViolationException ex) {
+ ConstraintViolationException cve = unwrapConstraintViolation(ex);
+ if (cve != null && "unique_member_restaurant_available_date".equals(cve.getConstraintName())) {
+ throw new ReservationException(ReservationErrorCode.DUPLICATE_RESERVATION);
+ }
+ throw ex;
+ }
}
private ConstraintViolationException unwrapConstraintViolation(Throwable t) {
while (t != null) {
if (t instanceof ConstraintViolationException cve) return cve;
t = t.getCause();
}
return null;
}- 동시성 경합 구간에 한정해,
DeadlockLoserDataAccessException이나LockTimeoutException발생 시 재시도 정책(예: Spring Retry + 지수 백오프)도 적용하면 안정성이 높아집니다.
필요하다면 공통 예외 변환기 또는 @retryable 어드바이스 구현을 도와드릴 수 있습니다.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In domain/src/main/java/com/wellmeet/domain/reservation/entity/Reservation.java
around lines 24 to 30, the unique constraint
"unique_member_restaurant_available_date" is defined but there is no service
layer logic to handle exceptions caused by violating this constraint. To fix
this, add exception handling in the ReservationService#createReservation method
to catch database exceptions related to this unique constraint violation and
throw a meaningful custom exception. Additionally, implement a retry mechanism
with exponential backoff for concurrency-related exceptions like
DeadlockLoserDataAccessException or LockTimeoutException to improve reliability
during concurrent reservation attempts.
There was a problem hiding this 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
🧹 Nitpick comments (3)
api-user/src/test/java/com/wellmeet/reservation/ReservationServiceTest.java (2)
84-85: 테스트 메서드명이 의미를 전달하지 않습니다
void name()은 테스트 목적을 알 수 없습니다. 테스트 의도를 드러내는 한글·영문 명칭으로 변경해 주세요 (예:동시_예약_수정시_용량_정상_차감된다).
126-131: 검증 범위가 불충분합니다용량만 확인하고 있어 두 예약이 실제로
availableDate3로 이동했는지, 파티 사이즈가 업데이트됐는지 확인하지 않습니다. 아래와 같이 추가 검증을 권장드립니다.assertThat(reservations) .allMatch(r -> r.getAvailableDate().getId().equals(availableDate3.getId()) && r.getPartySize() == changePartySize);domain/src/test/java/com/wellmeet/domain/restaurant/availabledate/entity/AvailableDateTest.java (1)
19-25: 엔티티 생성 시restaurant에 null 전달프로덕션 코드에서는
AvailableDate가Restaurant에@ManyToOne(optional = false)로 연결되어 있을 가능성이 높습니다. 테스트에서도 의미 있는 더미 레스토랑 객체를 전달하면 도메인 제약을 동일하게 검증할 수 있습니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
api-user/src/main/java/com/wellmeet/reservation/ReservationService.java(1 hunks)api-user/src/test/java/com/wellmeet/reservation/ReservationServiceTest.java(2 hunks)domain/src/main/java/com/wellmeet/domain/restaurant/availabledate/entity/AvailableDate.java(0 hunks)domain/src/test/java/com/wellmeet/domain/restaurant/availabledate/entity/AvailableDateTest.java(1 hunks)
💤 Files with no reviewable changes (1)
- domain/src/main/java/com/wellmeet/domain/restaurant/availabledate/entity/AvailableDate.java
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-07-10T02:52:48.337Z
Learnt from: unifolio0
PR: Team-soft-macaron/WellMeet-Backend#14
File: src/main/java/com/wellmeet/recommend/dto/RecommendRestaurantResponse.java:3-3
Timestamp: 2025-07-10T02:52:48.337Z
Learning: The Restaurant class is located at `com.wellmeet.restaurant.domain.Restaurant` after the package refactoring in the WellMeet-Backend project, not in a nested model package structure.
Applied to files:
api-user/src/main/java/com/wellmeet/reservation/ReservationService.javaapi-user/src/test/java/com/wellmeet/reservation/ReservationServiceTest.javadomain/src/test/java/com/wellmeet/domain/restaurant/availabledate/entity/AvailableDateTest.java
🧬 Code Graph Analysis (1)
api-user/src/main/java/com/wellmeet/reservation/ReservationService.java (2)
domain/src/main/java/com/wellmeet/domain/restaurant/availabledate/entity/AvailableDate.java (3)
increaseCapacity(64-67)Getter(22-68)reduceCapacity(54-62)domain/src/main/java/com/wellmeet/domain/reservation/ReservationDomainService.java (1)
Service(11-40)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build-and-push
| restaurantDomainService.increaseAvailableDateCapacity(availableDate, reservation.getPartySize()); | ||
| reservation.cancel(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
용량 복원 실패 여부 확인 로직이 없습니다
increaseAvailableDateCapacity가 내부적으로 업데이트된 행 수를 반환한다면 그 값을 검사하여 동시 취소 충돌(이미 취소됐거나 만석 해제 실패) 여부를 판단해야 합니다. 현재는 void 호출 후 바로 reservation.cancel()만 수행하므로 실제 용량이 복원되지 못해도 일관성이 깨질 수 있습니다.
예외 처리를 추가하거나 반환값 기반 검증으로 보강해 주세요.
증가 로직 실패 여부 검증을 위해 restaurantDomainService.increaseAvailableDateCapacity 메소드 정의를 확인합니다.
🏁 Script executed:
#!/bin/bash
rg "increaseAvailableDateCapacity" -n -A3 -B3Length of output: 2858
더 깊은 검증을 위해 increaseCapacity의 시그니처를 확인합니다.
🏁 Script executed:
#!/bin/bash
rg "increaseCapacity" -n -A3 -B3 domain/src/main/java/com/wellmeet/domain/restaurantLength of output: 2445
용량 복원 실패 검증 로직 추가 필요
increaseAvailableDateCapacity는 내부적으로 void 메소드(AvailableDateRepository.increaseCapacity)만 호출하여 반환값이 없어, 실제 업데이트 실패(동시 취소 충돌 등)를 알 수 없습니다. 반면 decreaseCapacity는 업데이트된 행 수를 반환하여 실패 여부를 검증하고 있습니다.
다음 위치를 참고하여 복원 실패 검증 로직을 추가해 주세요:
- ReservationService.java (api-user/src/main/java/com/wellmeet/reservation/ReservationService.java)
- cancel 메소드 78행:
restaurantDomainService.increaseAvailableDateCapacity(...)호출 직후 복원 성공 여부 점검
- cancel 메소드 78행:
- RestaurantDomainService.java (domain/src/main/java/com/wellmeet/domain/restaurant/RestaurantDomainService.java)
increaseAvailableDateCapacity를 반환값 있는 형태로 변경하거나 예외를 던지도록 수정
- AvailableDateRepository.java (domain/src/main/java/com/wellmeet/domain/restaurant/availabledate/repository/AvailableDateRepository.java)
@Modifying쿼리를int increaseCapacity(...)로 변경하고, 호출부에서 반환값 > 0 여부 검증
- void increaseCapacity(@Param("id") Long id, @Param("partySize") int partySize);
+ @Modifying
+ @Query(...)
+ int increaseCapacity(@Param("id") Long id, @Param("partySize") int partySize);그 후 ReservationService.cancel() 흐름에서 반환된 행 수를 확인하여 실패 시 예외 처리하거나 롤백 로직을 적용해 주세요.
🤖 Prompt for AI Agents
In api-user/src/main/java/com/wellmeet/reservation/ReservationService.java
around lines 78-79, the call to
restaurantDomainService.increaseAvailableDateCapacity does not verify if the
capacity restoration succeeded because the method returns void. To fix this,
modify AvailableDateRepository.increaseCapacity to return an int indicating the
number of updated rows, update
RestaurantDomainService.increaseAvailableDateCapacity to return this int or
throw an exception on failure, and then in ReservationService.cancel(), check
the returned value after calling increaseAvailableDateCapacity; if the update
failed (return value <= 0), throw an exception or trigger rollback to handle the
failure properly.
There was a problem hiding this 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)
api-user/src/test/java/com/wellmeet/reservation/ReservationServiceTest.java (3)
84-111: 예약 엔티티 필드까지 검증하면 테스트 신뢰도가 높아집니다
변경된 인원수가 정상 반영됐는지Reservation엔티티의partySize,availableDateId등 핵심 필드를 직접 조회해 추가로 확인해 주세요. 현재는 수량만 검증하고 있어, 컬럼 값이 잘못 저장돼도 테스트를 통과할 수 있습니다.
114-145: 동시성 테스트의 불안정성 가능성 점검 필요
10개의 스레드가 동일 업데이트를 수행할 때 중복 예약이 0-1건만 생성돼야 한다는 의도를reservations전체 개수(2)로 우회적으로 확인하고 있습니다.
①availableDate2에 생성된 예약 개수(1)를 직접 카운트하면 의도가 더 명확합니다.
② 트랜잭션 격리 수준이나runAtSameTime내부 구현에 따라 간헐적 실패(flaky)가 발생할 수 있으니,await().untilAsserted()와 같은 대기 로직을 고려해 보세요.
148-195: 복수 회원 동시 업데이트 시 예약 수 불변 여부 추가 확인 권장
availableDate3용량 감소만 검증하고 있는데, 두 회원이 같은 시간으로 변경했을 때 각 회원당 정확히 하나의 “활성” 예약만 존재하는지도 확인하면 테스트 범위가 명확해집니다. 예)long activeForMember1 = reservationRepository.countByMemberIdAndIsCanceledFalse(member1.getId()); assertThat(activeForMember1).isEqualTo(1);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
api-user/src/test/java/com/wellmeet/reservation/ReservationServiceTest.java(2 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-07-10T02:52:48.337Z
Learnt from: unifolio0
PR: Team-soft-macaron/WellMeet-Backend#14
File: src/main/java/com/wellmeet/recommend/dto/RecommendRestaurantResponse.java:3-3
Timestamp: 2025-07-10T02:52:48.337Z
Learning: The Restaurant class is located at `com.wellmeet.restaurant.domain.Restaurant` after the package refactoring in the WellMeet-Backend project, not in a nested model package structure.
Applied to files:
api-user/src/test/java/com/wellmeet/reservation/ReservationServiceTest.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build-and-push
|
구현하다가 혹시 몰라 update도 동시성 테스트를 작성해서 확인해보니 똑같이 동시성 문제가 발생했습니다. 이 부분은 유니크 인덱스로 처리가 안 되기 때문에 결국 분산락을 적용할 것 같습니다. |
There was a problem hiding this 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
🔭 Outside diff range comments (2)
domain/src/main/java/com/wellmeet/domain/restaurant/availabledate/repository/AvailableDateRepository.java (2)
29-31: increaseCapacity 메서드에도 일관성 있는 벌크 업데이트 보강 및 반환값 처리 적용 필요아래 작업을 통해
increaseCapacity에서도 1차 캐시 동기화 옵션과 영향 행 수 반환을 적용해주세요.
수정 대상
domain/src/main/java/com/wellmeet/domain/restaurant/availabledate/repository/AvailableDateRepository.javadomain/src/main/java/com/wellmeet/domain/restaurant/RestaurantDomainService.javadomain/src/main/java/com/wellmeet/domain/restaurant/availabledate/AvailableDateDomainService.java제안 diff (리포지토리 인터페이스 수정)
- @Modifying - @Query("update AvailableDate a set a.maxCapacity = a.maxCapacity + :partySize, a.isAvailable = true where a.id = :id") - void increaseCapacity(@Param("id") Long id, @Param("partySize") int partySize); + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query("update AvailableDate a set a.maxCapacity = a.maxCapacity + :partySize, a.isAvailable = true where a.id = :id") + int increaseCapacity(@Param("id") Long id, @Param("partySize") int partySize);
호출부 반영 예시
int updated = availableDateRepository.increaseCapacity(id, partySize); if (updated == 0) { // 예: 리소스 미존재 로깅 or 예외 처리 }호출부 위치
RestaurantDomainService.java(63–67행)AvailableDateDomainService.java(33–35행)반환값이 0인 경우(영향 행 없음)에 대한 로깅/예외 분기를 반드시 추가해주세요.
22-27: JPQL 벌크 업데이트: 1차 캐시 동기화 및 isAvailable 일관성 보강 권고
- 벌크 업데이트는 영속성 컨텍스트를 우회하므로,
@Modifying(clearAutomatically = true, flushAutomatically = true)로 캐시 플러시·초기화 보강isAvailable은 “남은 용량 > 0 → true” 로 항상 재설정하여 과거 잘못된 false 상태가 남지 않도록 처리- 적용 후 동시성/일관성 검증(동시 요청 시나리오)으로 결과 확인 권장
제안 diff:
- @Modifying + @Modifying(clearAutomatically = true, flushAutomatically = true) @Query("update AvailableDate a " - + "set a.maxCapacity = a.maxCapacity - :partySize, " - + "a.isAvailable = case when (a.maxCapacity - :partySize) = 0 then false else a.isAvailable end " + + "set a.maxCapacity = a.maxCapacity - :partySize, " + + "a.isAvailable = case when (a.maxCapacity - :partySize) = 0 then false else true end " + "where a.id = :id and a.maxCapacity >= :partySize") int decreaseCapacity(@Param("id") Long id, @Param("partySize") int partySize);
🧹 Nitpick comments (2)
domain/src/main/java/com/wellmeet/domain/restaurant/availabledate/repository/AvailableDateRepository.java (2)
4-4: 락 타임아웃 힌트 추가 권장(교착·무한 대기 방지)MySQL/InnoDB 환경에서는 락 대기가 길어질 수 있습니다.
@QueryHints로 타임아웃(밀리초)을 명시해 주면 장애 상황에서 빠른 페일이 가능합니다. 드라이버/DB에 따라 일부 무시될 수 있으니 운영 환경에서 적용 가능 여부를 검증해 주세요.아래 diff는 필요한 import 및 힌트 추가 예시입니다.
import com.wellmeet.domain.restaurant.availabledate.entity.AvailableDate; import jakarta.persistence.LockModeType; +import jakarta.persistence.QueryHint; import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.QueryHints; import org.springframework.data.jpa.repository.Lock; ... - @Lock(LockModeType.PESSIMISTIC_WRITE) + @Lock(LockModeType.PESSIMISTIC_WRITE) + @QueryHints(@QueryHint(name = "jakarta.persistence.lock.timeout", value = "5000")) Optional<AvailableDate> findByIdAndRestaurantId(Long id, String restaurantId);적용 후 실제로 타임아웃이 반영되는지(예: 장애 주입 테스트, DB 버전별 지원 여부) 검증이 필요합니다.
Also applies to: 8-8, 19-20
24-25: 도메인 용어 정합성 점검: ‘maxCapacity’가 실질적으로 ‘남은 수용 인원’ 역할JPQL에서
a.maxCapacity = a.maxCapacity - :partySize로 사용되는 것을 보면,maxCapacity는 “최대 수용 인원”보다 “현재 남은 수용 인원” 의미에 가깝습니다. 장기적으로는 필드명을remainingCapacity(또는availableCapacity)로 정비하거나, 최초 최대값을 별도 필드로 분리하면 가독성과 유지보수성이 향상됩니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
api-user/src/test/java/com/wellmeet/reservation/ReservationServiceTest.java(2 hunks)domain/src/main/java/com/wellmeet/domain/restaurant/availabledate/repository/AvailableDateRepository.java(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- api-user/src/test/java/com/wellmeet/reservation/ReservationServiceTest.java
🧰 Additional context used
🧬 Code Graph Analysis (1)
domain/src/main/java/com/wellmeet/domain/restaurant/availabledate/repository/AvailableDateRepository.java (2)
domain/src/main/java/com/wellmeet/domain/restaurant/availabledate/entity/AvailableDate.java (1)
Getter(22-68)domain/src/main/java/com/wellmeet/domain/restaurant/availabledate/AvailableDateDomainService.java (3)
Service(11-25)getByIdAndRestaurantId(21-24)getAvailableDatesByRestaurantId(17-19)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build-and-push
| @Lock(LockModeType.PESSIMISTIC_WRITE) | ||
| Optional<AvailableDate> findByIdAndRestaurantId(Long id, String restaurantId); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
PESSIMISTIC_WRITE는 트랜잭션 경계 안에서만 유의미 — 서비스 계층에 @transactional 보강 필요
현재 @Lock(LockModeType.PESSIMISTIC_WRITE)가 적용된 findByIdAndRestaurantId는 호출 시점에만 락을 잡습니다. 제공된 AvailableDateDomainService.getByIdAndRestaurantId(...) 스니펫에는 @Transactional이 없으므로, 이 메서드 호출 직후 락이 해제되어 이후의 감소/증가 쿼리(decreaseCapacity 등)를 보호하지 못합니다. 같은 트랜잭션 안에서 “조회(락 포함) → 변경”이 수행되도록 서비스 메서드(혹은 상위 서비스)를 트랜잭션으로 감싸 주세요.
추가 권장:
- 락 대기 무한정 방지를 위해 lock timeout 힌트 적용(아래 별도 코멘트).
- 단일 테이블의 수량 감소만으로 일관성이 보장된다면, 굳이 락킹 조회 없이 조건부 업데이트(
where a.maxCapacity >= :partySize)만으로 처리해 락 보유 시간을 줄이는 것도 선택지입니다.
서비스 계층 예시(다른 파일 수정):
// domain/.../AvailableDateDomainService.java
@Transactional
public AvailableDate getByIdAndRestaurantId(Long id, String restaurantId) {
return availableDateRepository.findByIdAndRestaurantId(id, restaurantId)
.orElseThrow(() -> new RestaurantException(RestaurantErrorCode.AVAILABLE_DATE_NOT_FOUND));
}아래 스크립트로 해당 서비스 메서드(또는 상위 호출자)에 @transactional이 선언되어 있는지 확인해 주세요.
🏁 Script executed:
#!/bin/bash
# AvailableDateDomainService의 트랜잭션 선언 여부 확인
fd -a 'AvailableDateDomainService.java' | while read -r f; do
echo "File: $f"
rg -n "@Transactional|class|getByIdAndRestaurantId\\(" -A 2 -B 2 "$f"
done
# 락킹 메서드 사용 경로 상위 서비스에서 @Transactional 있는지 주변 문맥 조회
rg -n "findByIdAndRestaurantId\\(" -A 5 -B 5Length of output: 4438
서비스 계층에 @transactional 어노테이션 추가 필요
현재 AvailableDateDomainService#getByIdAndRestaurantId 호출 시 PESSIMISTIC_WRITE 락은 즉시 해제되어 이후 decreaseCapacity 등의 업데이트가 보호되지 않습니다. 동일 트랜잭션 내에서 “조회(락 포함) → 변경”이 이루어지도록 아래와 같이 수정해주세요.
• 파일:
domain/src/main/java/com/wellmeet/domain/restaurant/availabledate/AvailableDateDomainService.java
- 클래스 레벨에 @transactional 추가
@Service @RequiredArgsConstructor @Transactional // << 이 어노테이션 추가 public class AvailableDateDomainService { // … }
- 또는 필요한 메서드에 개별 적용
@Transactional public AvailableDate getByIdAndRestaurantId(Long id, String restaurantId) { … } @Transactional public void decreaseCapacity(AvailableDate availableDate, int partySize) { … }
추가 권장 사항:
- 무한 대기를 방지하기 위해 LockModeType.PESSIMISTIC_WRITE 사용 시 timeout 힌트 적용 고려
- 단일 테이블 수량 감소만으로 충분한 일관성이 보장된다면, 락 조회 없이 조건부 업데이트(
WHERE a.maxCapacity >= :partySize) 사용하여 락 보유 시간을 줄이는 방안 검토
🤖 Prompt for AI Agents
In
domain/src/main/java/com/wellmeet/domain/restaurant/availabledate/repository/AvailableDateRepository.java
at lines 19-20, the PESSIMISTIC_WRITE lock is released immediately because there
is no surrounding transaction, which risks unprotected updates after locking. To
fix this, add the @Transactional annotation either at the class level or on the
specific methods like getByIdAndRestaurantId and decreaseCapacity in
AvailableDateDomainService.java to ensure the lock and subsequent updates occur
within the same transaction. Additionally, consider applying a timeout hint to
the lock to prevent indefinite waiting or refactor to use conditional updates
without locking if consistency requirements allow.
There was a problem hiding this 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
🔭 Outside diff range comments (1)
domain/src/main/java/com/wellmeet/domain/reservation/ReservationDomainService.java (1)
43-48: TOCTOU 레이스 방지: 유니크 제약 + 예외 변환으로 단일화하세요existsBy… 호출로 사전 존재 체크를 해도, insert/save 사이에 다른 트랜잭션이 개입하면 중복 저장을 막지 못합니다. 최종 방어선은 DB의 unique 제약이며, save() 시 발생하는 예외를 ReservationException(ALREADY_RESERVED)으로 변환하는 방식으로 일원화할 것을 권장합니다. UX 향상을 위한 best-effort 안내 용도로만 exists 체크를 남기고, 비즈니스 로직 분기엔 절대 의존하지 마세요.
확인된 비즈니스 로직 위치:
- api-user/src/main/java/com/wellmeet/reservation/ReservationService.java:30 (
alreadyReserved호출)제안 리팩터링 방안:
- Reservation 엔티티에 (member_id, restaurant_id, available_date_id) unique 제약 추가
- Reservation 생성(save) 시 DataIntegrityViolationException 등을 캐치하여 ReservationErrorCode.ALREADY_RESERVED로 변환
- ReservationService에서
alreadyReserved(...)호출 제거 (또는 UX용으로만 남기고 로직 분기에는 사용하지 않음)
🧹 Nitpick comments (2)
domain/src/main/java/com/wellmeet/domain/common/RepositoryErrorDecoder.java (1)
8-8: 상수명 의미 불일치 — constraint name을 반영하도록 리네이밍 제안값은 제약명이며 에러 메시지가 아닙니다. 가독성을 위해 이름을 아래처럼 바꾸는 것을 권장합니다.
- private static final String UNIQUE_RESERVATION_ERROR_MESSAGE = "unique_member_restaurant_available_date"; + private static final String UNIQUE_RESERVATION_CONSTRAINT_NAME = "unique_member_restaurant_available_date";그리고 참조부도 함께 변경:
- return UNIQUE_RESERVATION_ERROR_MESSAGE.equals(cve.getConstraintName()); + return UNIQUE_RESERVATION_CONSTRAINT_NAME.equals(cve.getConstraintName());Also applies to: 17-17
domain/src/main/java/com/wellmeet/domain/reservation/ReservationDomainService.java (1)
35-41: 조회 메서드에 readOnly 트랜잭션 적용을 고려하세요읽기 메서드에는
@Transactional(readOnly = true)를 적용하면 불필요한 dirty checking을 방지하고 성능에 유리합니다.예시:
@Transactional(readOnly = true) public List<Reservation> findAllByRestaurantId(String restaurantId) { ... }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
domain/src/main/java/com/wellmeet/domain/common/RepositoryErrorDecoder.java(1 hunks)domain/src/main/java/com/wellmeet/domain/reservation/ReservationDomainService.java(2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build-and-push
🔇 Additional comments (2)
domain/src/main/java/com/wellmeet/domain/common/RepositoryErrorDecoder.java (2)
10-12: 유틸 클래스로의 캡슐화는 적절합니다private 생성자와 정적 메서드로 한정한 설계는 목적에 부합합니다.
16-17: 문제 없음: Java 21로 설정되어 있어instanceof패턴 변수 지원
- build.gradle의 toolchain 설정에
JavaLanguageVersion.of(21)이 명시되어 있어 JDK 21을 대상으로 빌드됩니다.- JDK 16 이상에서 도입된
instanceof패턴 변수 문법 사용에 문제가 없습니다.
| public static boolean isUniqueConstraintViolation(DataIntegrityViolationException exception) { | ||
| Throwable cause = exception.getCause(); | ||
| while (cause != null) { | ||
| if (cause instanceof ConstraintViolationException cve) { | ||
| return UNIQUE_RESERVATION_ERROR_MESSAGE.equals(cve.getConstraintName()); | ||
| } | ||
| cause = cause.getCause(); | ||
| } | ||
| return false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
유니크 위반 감지가 Hibernate 전용에 치우침 — SQLState/에러코드 기반 fallback 추가 권장
드라이버/DB에 따라 cause 체인이 ConstraintViolationException이 아닐 수도 있고, constraintName이 null/다르게 세팅될 수 있습니다. SQLState(예: 23000, 23505)와 MySQL 에러코드(1062) 기반의 보강 로직을 추가해 내구성을 높여주세요.
아래처럼 보강을 제안합니다(핵심 변경만 표시):
- public static boolean isUniqueConstraintViolation(DataIntegrityViolationException exception) {
- Throwable cause = exception.getCause();
- while (cause != null) {
- if (cause instanceof ConstraintViolationException cve) {
- return UNIQUE_RESERVATION_ERROR_MESSAGE.equals(cve.getConstraintName());
- }
- cause = cause.getCause();
- }
- return false;
- }
+ public static boolean isUniqueConstraintViolation(DataIntegrityViolationException exception) {
+ Throwable cause = exception;
+ while (cause != null) {
+ if (cause instanceof ConstraintViolationException cve) {
+ if (UNIQUE_RESERVATION_CONSTRAINT_NAME.equals(cve.getConstraintName())) {
+ return true;
+ }
+ if (isUniqueViolation(cve.getSQLException())) {
+ return true;
+ }
+ } else if (cause instanceof SQLException sqlEx) {
+ if (isUniqueViolation(sqlEx)) {
+ return true;
+ }
+ }
+ cause = cause.getCause();
+ }
+ return false;
+ }추가로 필요한 보조 코드(선택 위치에 추가):
// import 추가
import java.sql.SQLException;
// helper 추가
private static boolean isUniqueViolation(SQLException ex) {
if (ex == null) return false;
String sqlState = ex.getSQLState();
int errorCode = ex.getErrorCode();
return "23000".equals(sqlState) // ANSI/Oracle/MySQL: integrity constraint violation
|| "23505".equals(sqlState) // PostgreSQL: unique_violation
|| errorCode == 1062; // MySQL: Duplicate entry
}🤖 Prompt for AI Agents
In domain/src/main/java/com/wellmeet/domain/common/RepositoryErrorDecoder.java
around lines 13 to 21, the unique constraint violation detection relies solely
on Hibernate's ConstraintViolationException and constraintName, which may be
null or absent depending on the DB/driver. Enhance robustness by adding fallback
logic that checks SQLState codes ("23000", "23505") and MySQL error code (1062)
from any SQLException in the cause chain. Implement a helper method
isUniqueViolation(SQLException ex) to encapsulate this logic and modify the
existing method to return true if either the constraintName matches or the
SQLState/errorCode indicates a unique violation.
| public Reservation save(Reservation reservation) { | ||
| return reservationRepository.save(reservation); | ||
| try { | ||
| return reservationRepository.save(reservation); | ||
| } catch (DataIntegrityViolationException exception) { | ||
| if (RepositoryErrorDecoder.isUniqueConstraintViolation(exception)) { | ||
| throw new ReservationException(ReservationErrorCode.ALREADY_RESERVED); | ||
| } | ||
| throw exception; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
제약 위반이 flush/commit 시점에 발생하면 현재 catch에 걸리지 않습니다 — @transactional + flush 보장 필요
JPA/Hibernate는 실제 DB 제약 위반을 flush/commit 시 던집니다. 현재 save()가 flush를 보장하지 않으면 DataIntegrityViolationException이 메서드 밖(트랜잭션 커밋 시점)에서 발생해 도메인 예외로 변환되지 않을 수 있습니다.
권장 수정(Repository가 JpaRepository인 경우):
@@
- public Reservation save(Reservation reservation) {
- try {
- return reservationRepository.save(reservation);
+ @Transactional
+ public Reservation save(Reservation reservation) {
+ try {
+ return reservationRepository.saveAndFlush(reservation);
} catch (DataIntegrityViolationException exception) {
if (RepositoryErrorDecoder.isUniqueConstraintViolation(exception)) {
throw new ReservationException(ReservationErrorCode.ALREADY_RESERVED);
}
throw exception;
}
}- 추가 import 필요:
import org.springframework.transaction.annotation.Transactional;
만약 saveAndFlush를 사용할 수 없다면, 같은 트랜잭션 내에서 save(..) 후 flush()를 호출해 예외를 이 메서드 범위로 당겨오세요:
@Transactional
public Reservation save(Reservation reservation) {
try {
reservationRepository.save(reservation);
reservationRepository.flush(); // 혹은 EntityManager.flush()
return reservation;
} catch (DataIntegrityViolationException e) {
if (RepositoryErrorDecoder.isUniqueConstraintViolation(e)) {
throw new ReservationException(ReservationErrorCode.ALREADY_RESERVED);
}
throw e;
}
}ReservationRepository가 JpaRepository를 상속하는지 확인(가능하면 saveAndFlush 사용):
🏁 Script executed:
#!/bin/bash
rg -n "interface\s+ReservationRepository" -A5
rg -n "extends\s+JpaRepository<" -A1Length of output: 3002
트랜잭션 범위 내에서 제약 위반 예외를 안전하게 처리하도록 save 메서드 수정 필요
- 파일: domain/src/main/java/com/wellmeet/domain/reservation/ReservationDomainService.java
대상 메서드:save(Reservation reservation)(19~27행) ReservationRepository가JpaRepository를 상속하므로saveAndFlush()사용 가능
제안하는 변경사항:
@@
- public Reservation save(Reservation reservation) {
+ @Transactional
+ public Reservation save(Reservation reservation) {
try {
- return reservationRepository.save(reservation);
+ return reservationRepository.saveAndFlush(reservation);
} catch (DataIntegrityViolationException exception) {
if (RepositoryErrorDecoder.isUniqueConstraintViolation(exception)) {
throw new ReservationException(ReservationErrorCode.ALREADY_RESERVED);
}
throw exception;
}
}추가 import:
import org.springframework.transaction.annotation.Transactional;🤖 Prompt for AI Agents
In
domain/src/main/java/com/wellmeet/domain/reservation/ReservationDomainService.java
around lines 19 to 27, modify the save method to handle constraint violation
exceptions safely within a transaction scope by adding the @Transactional
annotation to the method and replacing reservationRepository.save(reservation)
with reservationRepository.saveAndFlush(reservation). Also, add the import for
org.springframework.transaction.annotation.Transactional at the top of the file.
|
확인해보니 비관락을 적용하면 다른 테스트가 터져서... 이것도 아티클로 쓸예정입니다. 아무튼 요점은 이런 이유로 다음 pr에 분산락을 적용해서 올리겠습니다. |

🚩 Jira Ticket
SCRUM-107
🗣️ 리뷰 요구사항 (선택)
인덱스키를 활용하여 동시성을 제어했습니다. 다만 아래의 고민점으로 Redis 분산락이나 Named Lock을 활용하도록 리팩토링할까 고민중입니다.
예약이 가능한 지 확인하는 방법을 Repository에서 반환한 int 값을 이용해 만약 변경된 Row가 없어서 0을 반환했다는 것은 where 조건에서
a.maxCapacity >= :partySize를 만족하지 못한 것이라 판단하고 예외 처리를 하고 있습니다. 다만 무조건 이 경우밖에 없다고 단정짓지 못 하기에 제대로 검증하기 위해선 추가적인 절차가 필요하다고 판단했습니다.DB의 save에서 예외가 발생했을 때 이 예외가 Unique로 인한 조건인지 다른 문제로 인한 것인지 파악하는 로직을 추가해야 하는데 이 부분을 예외 메시지에 Unique 키가 포함되어 있을 때 따로 처리하거나, MySQL의 Unique 오류 예외 코드를 통해 따로 처리하거나 해야되는데 이 방법이 좋은 방법인지 의문점이 듭니다.
Summary by CodeRabbit
버그 수정
테스트
새로운 기능
리팩터