홍순구 튜터님

[ 잘한 점 ]

  1. 결제 상태 전이(State Machine)의 견고한 설계

Order 엔티티의 markAsPaid(), confirm(), cancel(), refund() 메서드에서 현재 상태를 검증한 뒤에만 다음 상태로 전이하는 방어적 상태 머신 패턴을 일관성 있게 적용한 것은 매우 훌륭합니다. PENDING → PAID → CONFIRMED의 정방향 흐름과 PAID → REFUNDED, PENDING → CANCELLED의 역방향 흐름이 명확히 분리되어 있어, 잘못된 상태 전이가 시스템 차원에서 원천 차단됩니다.

  1. 포인트 가점유(Hold) 메커니즘

UserPoint엔티티에서 totalPoint, usedPoint, heldPoint를 분리하여 가용 포인트(getAvailablePoint())를 계산하는 구조는 결제 시스템에서 매우 중요한 설계입니다. 주문 생성 시 hold() → 주문 확정 시 commit() → 취소 시 release()로 이어지는 포인트 라이프사이클이 잘 설계되어 있고, findByUserIdForUpdate()를 통한 비관적 락(Pessimistic Lock)까지 적용하여 동시성 문제를 예방한 점이 인상적입니다.

[ 개선 제안 ]

  1. OrderService.createOrder() 내 상품 조회 N+1 문제

현재 주문 생성 시 총 금액 계산과 주문 상품 생성에서 각각 productService.getProductById()를 루프 안에서 단건 호출하고 있어, 상품이 N개일 때 최소 2N번의 DB 쿼리가 발생합니다.

  1. OrderScheduler.autoConfirmOrders()의 트랜잭션 범위

현재 스케줄러가 @Transactional로 전체 배치를 하나의 트랜잭션으로 묶고 있어, 대상 주문이 많을 경우 트랜잭션 타임아웃이나 커넥션 점유 시간 증가 문제가 발생할 수 있습니다. 개별 주문을 별도 트랜잭션으로 처리하도록 분리하면 한 건의 실패가 전체 롤백으로 이어지는 것도 방지할 수 있습니다.

[ 기술적 피드백 ]

  1. 주문번호 UUID 충돌 위험

Order.generateOrderNumber() 메서드에서 주문번호를 UUID.randomUUID().toString().replace("-", "").substring(0, 10).toUpperCase()로 생성하고 있습니다.

16진수 10자리의 이론적 가짓수는 약 1조(16^10 ≈ 1.1조)개로 보이지만, 생일 역설(Birthday Paradox)에 의해 실제 충돌 확률은 약 130만 건부터 50%에 근접합니다. orderNumber컬럼에 unique = true 제약이 걸려 있으므로, 대규모 주문 발생 시 DataIntegrityViolationException으로 인한 시스템 장애가 유발될 수 있습니다.

주석에 // TODO: TSID 라이브러리 추가 후 변경 예정이라고 남겨두신 것으로 보아 이미 인지하고 계신 것 같습니다. TSID(Time-Sorted Unique Identifier)나 Snowflake ID 같은 분산 환경에서도 시간 순서와 유일성이 동시에 보장되는 ID 생성 전략으로 교체하시는 것을 권장합니다.

  1. ProductService.isOrderItemEnough() 비교 연산자 오류 (버그)

재고 검증 로직에서 >=(크거나 같음) 연산자를 사용하고 있어, 정확히 재고만큼 주문한 경우에도 재고 부족 예외가 발생합니다.

예를 들어 재고가 5개인 상품을 5개 주문하면 5 >= 5가 true가 되어 주문이 거부됩니다. >로 수정해야 합니다.