Skip to content

[23기_김동욱] 동시성 & 결제 연동 미션 제출합니다.#23

Open
boogiewooki02 wants to merge 6 commits intoCEOS-Developers:boogiewooki02from
boogiewooki02:boogiewooki02
Open

[23기_김동욱] 동시성 & 결제 연동 미션 제출합니다.#23
boogiewooki02 wants to merge 6 commits intoCEOS-Developers:boogiewooki02from
boogiewooki02:boogiewooki02

Conversation

@boogiewooki02
Copy link
Copy Markdown

No description provided.

Copy link
Copy Markdown
Collaborator

@yooniicode yooniicode left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

결제/동시성 흐름 전반에 대한 고민이 README랑 코드 곳곳에 잘 드러나서 읽기 좋았습니다 ^.^
시험기간 잘 마무리하시고 세션 때 뵈어요~!

);

try {
PaymentResponse response = portOnePaymentClient.instantPay(paymentId, new PaymentCreateRequest(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이때 PortOne이 느려지거나 타임아웃나면 DB 커넥션을 그 시간 내내 점유할 텐데, 커넥션 풀 고갈나면 서비스 먹통의 가능성이 있습니다.

결제 전/후 DB 작업을 별도 트랜잭션으로 쪼개고, PG 호출은 트랜잭션 밖에서 하시기를 추천드려용

Comment on lines +45 to +47
} catch (BusinessException e) {
payment.markFailed();
throw e;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

catch 내부에서는 메서드 전체가 트랜잭션이 걸려있어서, throw하면 전부 롤백됩니당
markFailed를 호출해서 상태를 변경했더라도, 같이 날아가게 되어 Payment가 DB에 남지 않아요!

실패 이력을 남기고자 하는 의도셨다면, Requires_new 이용하셔서 별도 트랜잭션이 필요합니당

Comment on lines +10 to +18
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;
}
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

서버가 여러 대이거나 TPS가 높은 환경에서는 yyyyMMdd_HHmmssSSS + 4자리 랜덤 조합이어도 충돌 가능성이 있어 보입니다. 충돌나면 paymentId unique 제약에서 DataIntegrityViolationException이 터질 텐데 재시도 로직은 따로 없는 것 같아용

이 방식을 선택하신 이유가 있으실까요? ULID나 UUID v7를 혹시 고려해보셨는지 궁금해요!

Comment on lines +30 to +32
for (Reservation reservation : expiredReservations) {
reservation.expire();
reservedSeatRepository.deleteByReservation(reservation);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1분마다 도는데 만료 예약이 100개면 delete 쿼리 100번이 날아가게 됩니당.
deleteAllByReservationIdIn(List) 같은 bulk delete로 바꾸는 걸 고려해보셨으면 좋겠어요!

Comment on lines +60 to +61
portOnePaymentClient.cancel(payment.getPaymentId());
payment.cancel();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이것도 instantPay와 같은 이슈인데요, PortOne cancel API 성공 후 payment.cancel()에서 뭔가 터지거나 트랜잭션이 롤백되면 PG 쪽은 취소됐는데 DB는 PAID 그대로 남아서 데이터 불일치가 발생합니다. 반대 케이스(DB는 CANCELLED인데 PG는 PAID)도 마찬가지구요!

Comment on lines +88 to +91
private void validateDuplicateSeatsInRequest(List<SeatPosition> seatPositions) {
Set<SeatPosition> uniqueSeats = Set.copyOf(seatPositions);
if (uniqueSeats.size() != seatPositions.size()) {
throw new BusinessException(ReservationErrorCode.ALREADY_RESERVED_SEAT);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요청 내 중복 좌석의 검증인데 이미 예약된 좌석 코드를 던지면 혼란스러울 수 있을 것 같아욥

Comment on lines +52 to +58
public static Reservation createPending(User user, Schedule schedule, LocalDateTime expiresAt) {
return Reservation.builder()
.status(ReservationStatus.PENDING_PAYMENT)
.user(user)
.schedule(schedule)
.expiresAt(expiresAt)
.build();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createPending 팩토리 메서드로 생성 경로를 잘 정의해주셨는데, @builder가 public 생성자에 달려 있어서 빌더도 외부에 노출되어 있어요.

createPending 하나만 통로로 두고 싶으시면 @builder(access = AccessLevel.PRIVATE)로 닫아주는 것이 어떨까요?!

Comment on lines +41 to +42
@OneToOne(mappedBy = "reservation", fetch = FetchType.LAZY)
private Payment payment;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

역방향의 OneToOne은 프록시를 만들기 위해 존재 여부를 확인해야 해서 Hibernate에서 LAZY가 잘 안 걸리는 것으로 알고 있어용. Reservation 조회할 때마다 Payment select가 나갈 수 있으니 쿼리 로그 한 번 확인해보시면 좋을 것 같아요! (참고: https://loosie.tistory.com/788)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants