홍순구 튜터님
[ 잘한 점 ]
Order 엔티티의 markAsPaid(), confirm(), cancel(), refund() 메서드에서 현재 상태를 검증한 뒤에만 다음 상태로 전이하는 방어적 상태 머신 패턴을 일관성 있게 적용한 것은 매우 훌륭합니다. PENDING → PAID → CONFIRMED의 정방향 흐름과 PAID → REFUNDED, PENDING → CANCELLED의 역방향 흐름이 명확히 분리되어 있어, 잘못된 상태 전이가 시스템 차원에서 원천 차단됩니다.
UserPoint엔티티에서 totalPoint, usedPoint, heldPoint를 분리하여 가용 포인트(getAvailablePoint())를 계산하는 구조는 결제 시스템에서 매우 중요한 설계입니다. 주문 생성 시 hold() → 주문 확정 시 commit() → 취소 시 release()로 이어지는 포인트 라이프사이클이 잘 설계되어 있고, findByUserIdForUpdate()를 통한 비관적 락(Pessimistic Lock)까지 적용하여 동시성 문제를 예방한 점이 인상적입니다.
[ 개선 제안 ]
현재 주문 생성 시 총 금액 계산과 주문 상품 생성에서 각각 productService.getProductById()를 루프 안에서 단건 호출하고 있어, 상품이 N개일 때 최소 2N번의 DB 쿼리가 발생합니다.
현재 스케줄러가 @Transactional로 전체 배치를 하나의 트랜잭션으로 묶고 있어, 대상 주문이 많을 경우 트랜잭션 타임아웃이나 커넥션 점유 시간 증가 문제가 발생할 수 있습니다. 개별 주문을 별도 트랜잭션으로 처리하도록 분리하면 한 건의 실패가 전체 롤백으로 이어지는 것도 방지할 수 있습니다.
[ 기술적 피드백 ]
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 생성 전략으로 교체하시는 것을 권장합니다.
재고 검증 로직에서 >=(크거나 같음) 연산자를 사용하고 있어, 정확히 재고만큼 주문한 경우에도 재고 부족 예외가 발생합니다.
예를 들어 재고가 5개인 상품을 5개 주문하면 5 >= 5가 true가 되어 주문이 거부됩니다. >로 수정해야 합니다.