[23기_김동욱] 동시성 & 결제 연동 미션 제출합니다.#23
[23기_김동욱] 동시성 & 결제 연동 미션 제출합니다.#23boogiewooki02 wants to merge 6 commits intoCEOS-Developers:boogiewooki02from
Conversation
yooniicode
left a comment
There was a problem hiding this comment.
결제/동시성 흐름 전반에 대한 고민이 README랑 코드 곳곳에 잘 드러나서 읽기 좋았습니다 ^.^
시험기간 잘 마무리하시고 세션 때 뵈어요~!
| ); | ||
|
|
||
| try { | ||
| PaymentResponse response = portOnePaymentClient.instantPay(paymentId, new PaymentCreateRequest( |
There was a problem hiding this comment.
이때 PortOne이 느려지거나 타임아웃나면 DB 커넥션을 그 시간 내내 점유할 텐데, 커넥션 풀 고갈나면 서비스 먹통의 가능성이 있습니다.
결제 전/후 DB 작업을 별도 트랜잭션으로 쪼개고, PG 호출은 트랜잭션 밖에서 하시기를 추천드려용
| } catch (BusinessException e) { | ||
| payment.markFailed(); | ||
| throw e; |
There was a problem hiding this comment.
catch 내부에서는 메서드 전체가 트랜잭션이 걸려있어서, throw하면 전부 롤백됩니당
markFailed를 호출해서 상태를 변경했더라도, 같이 날아가게 되어 Payment가 DB에 남지 않아요!
실패 이력을 남기고자 하는 의도셨다면, Requires_new 이용하셔서 별도 트랜잭션이 필요합니당
| public class PaymentIdGenerator { | ||
|
|
||
| private static final DateTimeFormatter PAYMENT_ID_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmssSSS"); | ||
|
|
||
| public String generate() { | ||
| int randomSuffix = ThreadLocalRandom.current().nextInt(1000, 10000); | ||
| return PAYMENT_ID_FORMAT.format(LocalDateTime.now()) + "_" + randomSuffix; | ||
| } | ||
| } |
There was a problem hiding this comment.
서버가 여러 대이거나 TPS가 높은 환경에서는 yyyyMMdd_HHmmssSSS + 4자리 랜덤 조합이어도 충돌 가능성이 있어 보입니다. 충돌나면 paymentId unique 제약에서 DataIntegrityViolationException이 터질 텐데 재시도 로직은 따로 없는 것 같아용
이 방식을 선택하신 이유가 있으실까요? ULID나 UUID v7를 혹시 고려해보셨는지 궁금해요!
| for (Reservation reservation : expiredReservations) { | ||
| reservation.expire(); | ||
| reservedSeatRepository.deleteByReservation(reservation); |
There was a problem hiding this comment.
1분마다 도는데 만료 예약이 100개면 delete 쿼리 100번이 날아가게 됩니당.
deleteAllByReservationIdIn(List) 같은 bulk delete로 바꾸는 걸 고려해보셨으면 좋겠어요!
| portOnePaymentClient.cancel(payment.getPaymentId()); | ||
| payment.cancel(); |
There was a problem hiding this comment.
이것도 instantPay와 같은 이슈인데요, PortOne cancel API 성공 후 payment.cancel()에서 뭔가 터지거나 트랜잭션이 롤백되면 PG 쪽은 취소됐는데 DB는 PAID 그대로 남아서 데이터 불일치가 발생합니다. 반대 케이스(DB는 CANCELLED인데 PG는 PAID)도 마찬가지구요!
| private void validateDuplicateSeatsInRequest(List<SeatPosition> seatPositions) { | ||
| Set<SeatPosition> uniqueSeats = Set.copyOf(seatPositions); | ||
| if (uniqueSeats.size() != seatPositions.size()) { | ||
| throw new BusinessException(ReservationErrorCode.ALREADY_RESERVED_SEAT); |
There was a problem hiding this comment.
요청 내 중복 좌석의 검증인데 이미 예약된 좌석 코드를 던지면 혼란스러울 수 있을 것 같아욥
| public static Reservation createPending(User user, Schedule schedule, LocalDateTime expiresAt) { | ||
| return Reservation.builder() | ||
| .status(ReservationStatus.PENDING_PAYMENT) | ||
| .user(user) | ||
| .schedule(schedule) | ||
| .expiresAt(expiresAt) | ||
| .build(); |
| @OneToOne(mappedBy = "reservation", fetch = FetchType.LAZY) | ||
| private Payment payment; |
There was a problem hiding this comment.
역방향의 OneToOne은 프록시를 만들기 위해 존재 여부를 확인해야 해서 Hibernate에서 LAZY가 잘 안 걸리는 것으로 알고 있어용. Reservation 조회할 때마다 Payment select가 나갈 수 있으니 쿼리 로그 한 번 확인해보시면 좋을 것 같아요! (참고: https://loosie.tistory.com/788)
No description provided.