refactor/db-lock#10
Conversation
PR 리뷰: refactor/db-lock낙관적/비관적 락을 구조화하려는 방향은 좋습니다. 다만 몇 가지 버그와 설계 개선 포인트를 공유드립니다. 🔴 Critical: Self-invocation으로 인한 @transactional 미적용파일: // execute()에서 같은 클래스의 메서드를 직접 호출
return decreaseStock(input); // ← Spring AOP 프록시를 우회함Spring의 트랜잭션은 AOP 프록시 기반으로 동작합니다. 같은 클래스 내에서 해결 방법: // 별도 클래스로 분리
@Service
public class DecreaseStockTransactionalHelper {
@Transactional
public Inventory decreaseStockWithOptimisticLock(Long productId) { ... }
}
// DecreaseStockWithOptimisticLockUseCase에서 주입받아 사용
private final DecreaseStockTransactionalHelper helper;🟠 Major: UseCase가 UseCase를 직접 의존 (아키텍처 위반)파일: private final DecreaseStockWithPessimisticLockUseCase decreaseStockWithPessimisticLockUseCase;CLAUDE.md 아키텍처 원칙에 따르면 UseCase는 Domain + Port 계층에만 의존해야 합니다. UseCase가 다른 UseCase를 직접 주입받으면 레이어 간 결합이 생기고, 락 전략을 바꿀 때마다 현재 구조에서 🟠 Major:
|
코드 리뷰DB 락 전략(낙관적/비관적)을 단계별로 도입하고 🔴 Critical: 트랜잭션 분리로 인한 데이터 정합성 문제
// 재고 감소 (별도 트랜잭션 - 커밋됨)
DecreaseStockWithOptimisticLockUseCase.Output output = decreaseStockWithOptimisticLockUseCase.execute(...);
// 주문 생성 (별도 트랜잭션)
createOrderUseCase.execute(createOrderUseCaseInput);재고 감소와 주문 생성이 각각 별개의 트랜잭션으로 동작합니다. 재고 감소가 커밋된 후 주문 생성이 실패하면, 재고는 줄었는데 주문 기록은 없는 상황이 발생합니다. 이 두 작업은 하나의 트랜잭션 안에서 처리되어야 합니다. 🔴 Critical: 미사용 빈 주입 + 주석 처리된 코드
private final DecreaseStockUseCase decreaseStockUseCase; // 실제로 사용 안 함
private final DecreaseStockWithOptimisticLockUseCase decreaseStockWithOptimisticLockUseCase;
private final DecreaseStockWithPessimisticLockUseCase decreaseStockWithPessimisticLockUseCase; // 실제로 사용 안 함주석 처리된 코드( 🟡 Warning:
|
PR 리뷰: refactor/db-lock전반적으로 DB 락 전략을 구체적으로 구현하려는 방향은 좋습니다. 다만 몇 가지 중요한 문제들이 있어 피드백 드립니다. 🔴 치명적 버그1. 낙관적 락 재시도 로직이 실제로 동작하지 않음가장 심각한 문제입니다.
낙관적 락의
해결 방향: // InventoryProcessor
@Transactional(propagation = Propagation.REQUIRES_NEW) // 매 재시도마다 새 트랜잭션
public Inventory decreaseStockWithOptimisticLock(Long productId) { ... }🟠 설계 문제2. CLAUDE.md 아키텍처 원칙 위반 - Processor 조기 생성CLAUDE.md: "3개 이상의 UseCase에서 검증된 중복이 있을 때만 도입. 절대 미리 만들지 말 것"
이 정도 조합은 Processor 없이 UseCase 내부나 Controller에서 직접 처리해도 충분합니다. 3.
|
| 우선순위 | 항목 |
|---|---|
| 🔴 즉시 수정 | 낙관적 락 재시도 로직 트랜잭션 문제 (실제로 동작 안 함) |
| 🟠 수정 권장 | GlobalExceptionHandler RuntimeException → 500, Processor 조기 추상화 |
| 🟡 개선 고려 | 데드 코드 정리, 불필요한 @Lock 제거, 에러 메시지 통일 |
This reverts commit ca8b0fb.
PR 리뷰: refactor/db-lock전반적으로 비관적 락(Pessimistic Lock)과 낙관적 락(Optimistic Lock) 전략을 도입하려는 방향은 좋습니다. 다만 몇 가지 중요한 버그와 개선 사항이 있어 공유드립니다. 🔴 Critical Bug1. CreateOrderWithOptimisticLockUseCase가 실제로는 비관적 락을 사용하고 있음클래스 이름은 Optimistic Lock이라고 되어 있지만, 내부에서는 findByProductIdWithPessimisticLock을 호출하고 있습니다. // CreateOrderWithOptimisticLockUseCase.java:25
Inventory inventory = inventoryRepository.findByProductIdWithPessimisticLock(input.productId())이름이 약속하는 것과 구현이 완전히 다릅니다. 두 전략이 모두 비관적 락을 쓰고 있어, 낙관적 락 구현이 실질적으로 없는 상태입니다. 수정 방향: findByProductIdWithOptimisticLock으로 변경하고, 낙관적 락 충돌 시 발생하는 ObjectOptimisticLockingFailureException을 처리하는 재시도 로직(retry)이 필요합니다. 🟠 Major Issues2. findByProductIdWithOptimisticLock이 실제로 사용되지 않음Repository에 메서드를 추가했지만, 어떤 UseCase에서도 호출하지 않습니다. 위 버그(#1)와 직결된 문제입니다. 3. GlobalExceptionHandler에서 503 상태코드 오용@ExceptionHandler(RuntimeException.class)
public ResponseEntity<String> handleRuntimeException(RuntimeException e) {
return ResponseEntity.status(503).body(e.getMessage());
}503 (Service Unavailable)은 서버가 일시적으로 요청을 처리할 수 없는 인프라/가용성 문제에 사용해야 합니다. 일반적인 RuntimeException에 503을 반환하면 클라이언트가 잘못된 신호를 받게 됩니다. 낙관적 락 충돌(OptimisticLockException)의 경우 409 Conflict 또는 429 Too Many Requests가 더 적합합니다. 4. Controller가 CreateOrderWithPessimisticLockUseCase를 전혀 노출하지 않음// OrderController.java
private final CreateOrderWithOptimisticLockUseCase createOrderWithOptimisticLockUseCase;CreateOrderWithPessimisticLockUseCase는 만들어졌지만 컨트롤러에 연결되지 않아 사용할 방법이 없습니다. 두 전략을 비교 테스트하려는 프로젝트 목적에 맞지 않습니다. 🟡 Minor Issues5. 예외 메시지 언어 불일치같은 파일 내에서 한국어와 영어가 혼용되고 있습니다. .orElseThrow(() -> new IllegalArgumentException("재고를 찾을 수 없습니다"));
.orElseThrow(() -> new IllegalArgumentException("User not found"));프로젝트 전체적으로 언어를 통일하는 것이 좋습니다. 6. 예외 메시지가 클라이언트에 그대로 노출됨 (보안)return ResponseEntity.badRequest().body(e.getMessage());내부 예외 메시지를 그대로 응답 body로 반환하면, 내부 구현 세부사항이 외부에 노출될 수 있습니다. 특히 RuntimeException 핸들러는 예상치 못한 내부 오류 메시지까지 클라이언트에 노출할 수 있습니다. 공통 에러 응답 DTO를 사용하는 것을 권장합니다. public record ErrorResponse(String code, String message) {}7. IllegalStateException을 400으로 처리하는 것이 적절한지 재검토 필요IllegalStateException은 재고 부족처럼 409 Conflict가 더 적절한 시나리오에서 발생할 수 있습니다. 단순히 400으로 묶기보다, 도메인 전용 예외 클래스(OutOfStockException 등)를 만들어 상황에 맞는 HTTP 상태코드를 반환하는 것이 더 명확합니다. ✅ 잘 된 점
요약 체크리스트
|
PR 리뷰: refactor/db-lock비관적 락 / 낙관적 락을 분리하고 글로벌 예외 핸들러를 추가한 방향은 좋습니다. 다만 몇 가지 개선이 필요한 부분이 있어 피드백 드립니다. 🔴 주요 이슈1. 낙관적 락에 재시도(Retry) 로직 없음 — CreateOrderWithOptimisticLockUseCase낙관적 락의 핵심 동작 방식은 충돌 감지 후 재시도입니다. 현재 구현에서는 ObjectOptimisticLockingFailureException이 발생하면 그대로 409 에러가 클라이언트에 전달됩니다. 이는 낙관적 락의 이점을 살리지 못합니다. 재시도를 아예 지원하지 않을 것이라면, 클라이언트에게 '재시도가 필요하다'는 명확한 응답(예: 409 + 안내 메시지)은 있어야 하지만, 근본적으로는 Spring Retry의 @retryable을 활용한 자동 재시도 구현을 권장합니다. 2. GlobalExceptionHandler에서 RuntimeException을 너무 광범위하게 409로 처리현재 코드:
RuntimeException 하위에는 NullPointerException, DataIntegrityViolationException, TransactionSystemException 등 수백 가지 예외가 있습니다. 이들이 모두 409로 처리되는 건 잘못된 시맨틱입니다. 의도가 낙관적 락 충돌 처리라면 다음과 같이 명시적으로 분리해야 합니다:
🟡 개선 권장 사항3. 예외 메시지를 클라이언트에 직접 노출 — 보안 주의e.getMessage()를 그대로 응답 body로 반환하면, 운영 환경에서 내부 구현 세부사항이 노출될 수 있습니다. 표준 에러 응답 객체(ErrorResponse record 등)를 별도로 만들어 사용하는 것을 권장합니다. 4. 사용자 조회 순서 문제 — CreateOrderWithOptimisticLockUseCase (PessimisticLock도 동일)현재 순서:
사용자가 존재하지 않을 경우, 이미 재고를 감소시킨 후 예외가 발생합니다. 트랜잭션 롤백으로 정합성 문제는 없지만, 불필요한 DB 작업을 줄이기 위해 사용자 검증을 먼저 수행하는 것이 좋습니다. 권장 순서: 사용자 조회 → 재고 조회 → 재고 감소 5. @lock(LockModeType.OPTIMISTIC) 중복 여부Inventory 엔티티에 @Version 필드가 이미 존재하면, JPA는 자동으로 낙관적 락을 적용합니다. @lock(LockModeType.OPTIMISTIC)은 기술적으로 불필요합니다. 메서드명에서 이미 의도가 드러나므로 어노테이션을 제거해도 무방합니다. 6. URL에 구현 세부사항 노출
학습/실험 목적의 프로젝트에서는 이해할 수 있지만, 실제 API 설계에서 내부 구현 방식(어떤 락을 사용하는지)이 URL에 드러나는 것은 적절하지 않습니다. 🟢 잘된 점
📝 테스트 커버리지이번 PR에 테스트가 포함되지 않았습니다. 특히 동시성 시나리오가 핵심인 기능이므로, 아래 케이스들에 대한 테스트가 있으면 더 좋겠습니다:
|
|
@claude 사용자 조회 순서 문제 변경해줘 |
|
Claude finished @TreeStone94's task in 1m 20s —— View job 사용자 조회 순서 변경 완료
두 UseCase 모두 사용자 조회 순서를 재고 조회 앞으로 변경했습니다. 변경 전: 변경 후: 존재하지 않는 사용자로 요청 시, 재고에 불필요한 DB 작업(조회 + 락)을 하기 전에 빠르게 실패합니다. 트랜잭션 롤백으로 데이터 정합성은 기존에도 보장되었지만, 이제 불필요한 DB 작업을 줄일 수 있습니다. |
불필요한 DB 작업을 줄이기 위해 사용자 검증을 먼저 수행하도록 순서를 변경합니다. 변경 전: 재고 조회 → 재고 감소 → 사용자 조회 변경 후: 사용자 조회 → 재고 조회 → 재고 감소 Co-authored-by: treestone <TreeStone94@users.noreply.github.com>
PR 리뷰: refactor/db-lock낙관적/비관적 락 구현 전반적으로 방향성은 좋습니다. 다만 몇 가지 수정이 필요한 사항들을 공유드립니다. Critical: @EnableRetry 누락@retryable이 동작하려면 반드시 @EnableRetry가 설정되어 있어야 합니다. 현재 ShopApiApplication 이나 별도 설정 클래스 어디에도 @EnableRetry가 없기 때문에, @retryable과 @recover는 완전히 무시되고 ObjectOptimisticLockingFailureException 발생 시 재시도 없이 바로 예외가 클라이언트에게 전달됩니다. ShopApiApplication.java 또는 별도 Config 클래스에 아래를 추가해야 합니다: Warning: @retryable + @transactional 동일 클래스 사용 시 프록시 순서 주의CreateOrderWithOptimisticLockUseCase에 @transactional(클래스 레벨)과 @retryable(메서드 레벨)이 함께 선언되어 있습니다. Spring Retry의 기본 Advisor order(1)가 @transactional의 기본 order보다 높아 현재는 우연히 올바르게 동작할 수 있지만, 이는 암묵적인 설정에 의존하는 불안정한 구조입니다. 권장 패턴: 트랜잭션 처리 메서드를 별도 빈으로 분리하여 명시적으로 프록시 순서를 보장하는 것이 안전합니다. Warning: @recover 메서드에도 @transactional 적용됨클래스 레벨에 @transactional이 선언되어 있어 recover() 메서드도 트랜잭션 내에서 실행됩니다. recover()는 DB 작업을 하지 않고 예외만 던지므로 불필요한 트랜잭션이 열립니다. @transactional(propagation = Propagation.NOT_SUPPORTED)를 recover()에 추가하거나, 위의 분리 패턴을 적용하면 해결됩니다. Warning: GlobalExceptionHandler - RuntimeException 범용 핸들러 문제RuntimeException을 catch-all로 사용하면 예상치 못한 예외(NPE, ClassCastException 등)도 전부 409로 응답하게 됩니다. 의도하지 않은 예외는 500으로 응답해야 정상입니다. 낙관적 락 재시도 실패를 위한 전용 예외 클래스를 만드는 것이 좋습니다. Minor: 에러 메시지 불일치두 UseCase 사이에 동일한 상황의 에러 메시지가 다릅니다.
또한 StockQuantity.decreaseStockQuantity()의 예외 메시지는 영어("Stock quantity cannot be less than zero")인데, 나머지는 한국어입니다. 일관성 있게 통일해 주세요. Minor: @lock(LockModeType.OPTIMISTIC) 명시 의미 재검토Inventory에 @Version이 선언된 이상 JPA는 기본적으로 낙관적 락을 적용합니다. findByProductIdWithOptimisticLock에 @lock(LockModeType.OPTIMISTIC)을 추가하는 것은 기능적으로는 문제없지만 중복입니다. 만약 낙관적 락 + 강제 버전 증가가 필요하다면 OPTIMISTIC_FORCE_INCREMENT를 사용하고, 그렇지 않다면 제거해도 무방합니다. 의도를 명확히 주석으로 남겨주면 더 좋겠습니다. Minor: 동시성 테스트 부재이 PR의 핵심 목적이 고동시성 처리인데, 이를 검증하는 테스트가 없습니다. CountDownLatch 또는 ExecutorService를 활용한 동시성 통합 테스트를 추가하면 실제 락 동작을 검증하고 회귀를 방지할 수 있습니다. 잘된 점
|
No description provided.