refactor: OrderService의 결제 & 예약 확정 로직에 이벤트 발행 추가

This commit is contained in:
이상진 2025-10-16 15:28:56 +09:00
parent 66bf68826b
commit a83352c733
4 changed files with 15 additions and 100 deletions

View File

@ -1,146 +1,69 @@
package com.sangdol.roomescape.order.business
import com.sangdol.common.persistence.IDGenerator
import com.sangdol.common.persistence.TransactionExecutionUtil
import com.sangdol.common.types.exception.ErrorCode
import com.sangdol.common.types.exception.RoomescapeException
import com.sangdol.roomescape.order.exception.OrderErrorCode
import com.sangdol.roomescape.order.exception.OrderException
import com.sangdol.roomescape.order.infrastructure.persistence.AttemptResult
import com.sangdol.roomescape.order.infrastructure.persistence.PaymentAttemptEntity
import com.sangdol.roomescape.order.infrastructure.persistence.PaymentAttemptRepository
import com.sangdol.roomescape.payment.business.PaymentService
import com.sangdol.roomescape.payment.dto.PaymentConfirmRequest
import com.sangdol.roomescape.payment.dto.PaymentGatewayResponse
import com.sangdol.roomescape.payment.exception.PaymentException
import com.sangdol.roomescape.reservation.business.ReservationService
import com.sangdol.roomescape.reservation.business.event.ReservationConfirmEvent
import com.sangdol.roomescape.reservation.dto.ReservationStateResponse
import com.sangdol.roomescape.schedule.business.ScheduleService
import com.sangdol.roomescape.schedule.dto.ScheduleStateResponse
import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.context.ApplicationEventPublisher
import org.springframework.stereotype.Service
private val log: KLogger = KotlinLogging.logger {}
@Service
class OrderService(
private val idGenerator: IDGenerator,
private val reservationService: ReservationService,
private val scheduleService: ScheduleService,
private val paymentService: PaymentService,
private val transactionExecutionUtil: TransactionExecutionUtil,
private val orderValidator: OrderValidator,
private val paymentAttemptRepository: PaymentAttemptRepository,
private val orderPostProcessorService: OrderPostProcessorService
private val eventPublisher: ApplicationEventPublisher
) {
fun confirm(reservationId: Long, paymentConfirmRequest: PaymentConfirmRequest) {
var trial: Long = 0
val paymentKey = paymentConfirmRequest.paymentKey
log.info { "[confirm] 결제 및 예약 확정 시작: reservationId=${reservationId}, paymentKey=${paymentKey}" }
try {
trial = transactionExecutionUtil.withNewTransaction(isReadOnly = false) {
getTrialAfterValidateCanConfirm(reservationId).also {
reservationService.markInProgress(reservationId)
}
} ?: run {
log.warn { "[confirm] 모든 paymentAttempts 조회 과정에서의 예상치 못한 null 응답: reservationId=${reservationId}" }
throw OrderException(OrderErrorCode.BOOKING_UNEXPECTED_ERROR)
transactionExecutionUtil.withNewTransaction(isReadOnly = false) {
validateCanConfirm(reservationId)
reservationService.markInProgress(reservationId)
}
val paymentClientResponse: PaymentGatewayResponse =
requestConfirmPayment(reservationId, paymentConfirmRequest)
paymentService.requestConfirm(reservationId, paymentConfirmRequest)
eventPublisher.publishEvent(ReservationConfirmEvent(reservationId))
orderPostProcessorService.processAfterPaymentConfirmation(reservationId, paymentClientResponse)
log.info { "[confirm] 결제 처리 및 예약 확정 이벤트 발행 완료" }
} catch (e: Exception) {
val errorCode: ErrorCode = if (e is RoomescapeException) {
e.errorCode
} else {
OrderErrorCode.BOOKING_UNEXPECTED_ERROR
OrderErrorCode.ORDER_UNEXPECTED_ERROR
}
throw OrderException(errorCode, e.message ?: errorCode.message, trial)
throw OrderException(errorCode, e.message ?: errorCode.message)
}
}
private fun getTrialAfterValidateCanConfirm(reservationId: Long): Long {
private fun validateCanConfirm(reservationId: Long) {
log.info { "[validateAndMarkInProgress] 예약 확정 가능 여부 검증 시작: reservationId=${reservationId}" }
val reservation: ReservationStateResponse = reservationService.findStatusWithLock(reservationId)
val schedule: ScheduleStateResponse = scheduleService.findStateWithLock(reservation.scheduleId)
try {
orderValidator.validateCanConfirm(reservation, schedule)
return getTrialIfSuccessAttemptNotExists(reservationId).also {
log.info { "[validateAndMarkInProgress] 예약 확정 가능 여부 검증 완료: reservationId=${reservationId}" }
}
} catch (e: OrderException) {
val errorCode = OrderErrorCode.NOT_CONFIRMABLE
throw OrderException(errorCode, e.message)
}
}
private fun getTrialIfSuccessAttemptNotExists(reservationId: Long): Long {
val paymentAttempts: List<PaymentAttemptEntity> = paymentAttemptRepository.findAllByReservationId(reservationId)
if (paymentAttempts.any { it.result == AttemptResult.SUCCESS }) {
log.info { "[validateCanConfirm] 이미 결제 완료된 예약: id=${reservationId}" }
throw OrderException(OrderErrorCode.BOOKING_ALREADY_COMPLETED)
}
return paymentAttempts.size.toLong()
}
private fun requestConfirmPayment(
reservationId: Long,
paymentConfirmRequest: PaymentConfirmRequest
): PaymentGatewayResponse {
log.info { "[requestConfirmPayment] 결제 및 이력 저장 시작: reservationId=${reservationId}, paymentKey=${paymentConfirmRequest.paymentKey}" }
val paymentResponse: PaymentGatewayResponse
var attempt: PaymentAttemptEntity? = null
try {
paymentResponse = paymentService.requestConfirm(paymentConfirmRequest)
attempt = PaymentAttemptEntity(
id = idGenerator.create(),
reservationId = reservationId,
result = AttemptResult.SUCCESS,
)
} catch (e: Exception) {
val errorCode: String = if (e is PaymentException) {
log.info { "[requestConfirmPayment] 결제 요청 실패: reservationId=${reservationId}, paymentKey=${paymentConfirmRequest.paymentKey}" }
e.errorCode.name
} else {
log.warn {
"[requestConfirmPayment] 결제 요청 실패: reservationId=${reservationId}, paymentKey=${paymentConfirmRequest.paymentKey}"
}
OrderErrorCode.BOOKING_UNEXPECTED_ERROR.name
}
attempt = PaymentAttemptEntity(
id = idGenerator.create(),
reservationId = reservationId,
result = AttemptResult.FAILED,
errorCode = errorCode,
message = e.message
)
throw e
} finally {
val savedAttempt: PaymentAttemptEntity? = attempt?.let {
log.info { "[requestPayment] 결제 요청 이력 저장 시작: id=${it.id}, reservationId=${it.reservationId}, result=${it.result}, errorCode=${it.errorCode}, message=${it.message}" }
paymentAttemptRepository.save(it)
}
savedAttempt?.also {
log.info { "[requestPayment] 결제 요청 이력 저장 완료: id=${savedAttempt.id}" }
} ?: run {
log.info { "[requestPayment] 결제 요청 이력 저장 실패: reservationId=${reservationId}" }
}
}
return paymentResponse
}
}

View File

@ -3,7 +3,6 @@ package com.sangdol.roomescape.order.business
import com.sangdol.common.utils.KoreaDateTime
import com.sangdol.roomescape.order.exception.OrderErrorCode
import com.sangdol.roomescape.order.exception.OrderException
import com.sangdol.roomescape.order.infrastructure.persistence.PaymentAttemptRepository
import com.sangdol.roomescape.reservation.dto.ReservationStateResponse
import com.sangdol.roomescape.reservation.infrastructure.persistence.ReservationStatus
import com.sangdol.roomescape.schedule.dto.ScheduleStateResponse
@ -29,7 +28,7 @@ class OrderValidator {
when (reservation.status) {
ReservationStatus.CONFIRMED -> {
log.info { "[validateCanConfirm] 이미 확정된 예약: id=${reservation.id}" }
throw OrderException(OrderErrorCode.BOOKING_ALREADY_COMPLETED)
throw OrderException(OrderErrorCode.ORDER_ALREADY_CONFIRMED)
}
ReservationStatus.EXPIRED -> {
log.info { "[validateCanConfirm] 만료된 예약: id=${reservation.id}" }

View File

@ -9,11 +9,11 @@ enum class OrderErrorCode(
override val message: String
) : ErrorCode {
NOT_CONFIRMABLE(HttpStatus.CONFLICT, "B000", "예약을 확정할 수 없어요."),
BOOKING_ALREADY_COMPLETED(HttpStatus.CONFLICT, "B001", "이미 완료된 예약이에요."),
ORDER_ALREADY_CONFIRMED(HttpStatus.CONFLICT, "B001", "이미 완료된 예약이에요."),
EXPIRED_RESERVATION(HttpStatus.CONFLICT, "B002", "결제 가능 시간이 지나 만료된 예약이에요. 처음부터 다시 시도해주세요."),
CANCELED_RESERVATION(HttpStatus.CONFLICT, "B003", "이미 취소된 예약이에요. 본인이 취소하지 않았다면 매장에 문의해주세요."),
PAST_SCHEDULE(HttpStatus.CONFLICT, "B004", "지난 일정은 예약할 수 없어요."),
BOOKING_UNEXPECTED_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "B999", "예상치 못한 예외가 발생했어요. 잠시 후 다시 시도해주세요.")
ORDER_UNEXPECTED_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "B999", "예상치 못한 예외가 발생했어요. 잠시 후 다시 시도해주세요.")
;
}

View File

@ -6,11 +6,4 @@ import com.sangdol.common.types.exception.RoomescapeException
class OrderException(
override val errorCode: ErrorCode,
override val message: String = errorCode.message,
var trial: Long = 0
) : RoomescapeException(errorCode, message)
class OrderErrorResponse(
val code: String,
val message: String,
val trial: Long
)