refactor: PaymentClient를 사용하는 Request 객체 도입 및 Writer에서의 Client 로직 제거

This commit is contained in:
이상진 2025-08-18 16:43:56 +09:00
parent 9aaa988f76
commit b0b0eb6fcf
5 changed files with 108 additions and 85 deletions

View File

@ -0,0 +1,21 @@
package roomescape.payment.implement
import org.springframework.stereotype.Component
import roomescape.payment.infrastructure.client.v2.*
@Component
class PaymentRequester(
private val client: TosspaymentClientV2
) {
fun requestConfirmPayment(paymentKey: String, orderId: String, amount: Int): PaymentConfirmResponse {
val request = PaymentConfirmRequest(paymentKey, orderId, amount)
return client.confirm(request)
}
fun requestCancelPayment(paymentKey: String, amount: Int, cancelReason: String): PaymentCancelResponseV2 {
val request = PaymentCancelRequestV2(paymentKey, amount, cancelReason)
return client.cancel(request)
}
}

View File

@ -10,33 +10,18 @@ import roomescape.payment.exception.PaymentException
import roomescape.payment.infrastructure.client.v2.* import roomescape.payment.infrastructure.client.v2.*
import roomescape.payment.infrastructure.common.PaymentMethod import roomescape.payment.infrastructure.common.PaymentMethod
import roomescape.payment.infrastructure.persistence.v2.* import roomescape.payment.infrastructure.persistence.v2.*
import roomescape.reservation.web.ReservationCancelRequest
import roomescape.reservation.web.ReservationPaymentRequest import roomescape.reservation.web.ReservationPaymentRequest
import roomescape.reservation.web.toPaymentConfirmRequest
import java.time.LocalDateTime import java.time.LocalDateTime
private val log: KLogger = KotlinLogging.logger {} private val log: KLogger = KotlinLogging.logger {}
@Component @Component
class PaymentWriterV2( class PaymentWriterV2(
private val paymentClient: TosspaymentClientV2,
private val paymentRepository: PaymentRepositoryV2, private val paymentRepository: PaymentRepositoryV2,
private val paymentDetailRepository: PaymentDetailRepository, private val paymentDetailRepository: PaymentDetailRepository,
private val canceledPaymentRepository: CanceledPaymentRepositoryV2, private val canceledPaymentRepository: CanceledPaymentRepositoryV2,
private val tsidFactory: TsidFactory, private val tsidFactory: TsidFactory,
) { ) {
fun requestConfirmPayment(
reservationId: Long,
request: ReservationPaymentRequest
): PaymentConfirmResponse {
log.debug { "[PaymentWriterV2.requestConfirmPayment] 결제 승인 요청 시작: reservationId=${reservationId}, paymentKey=${request.paymentKey}" }
return paymentClient.confirm(request.toPaymentConfirmRequest()).also {
log.debug { "[PaymentWriterV2.requestConfirmPayment] 결제 승인 요청 완료: response=$it" }
}
}
fun createPayment( fun createPayment(
reservationId: Long, reservationId: Long,
request: ReservationPaymentRequest, request: ReservationPaymentRequest,
@ -48,12 +33,12 @@ class PaymentWriterV2(
id = tsidFactory.next(), reservationId, request.orderId, request.paymentType id = tsidFactory.next(), reservationId, request.orderId, request.paymentType
).also { ).also {
paymentRepository.save(it) paymentRepository.save(it)
saveDetail(paymentConfirmResponse, it.id) createDetail(paymentConfirmResponse, it.id)
log.debug { "[PaymentWriterV2.createPayment] 결제 승인 및 결제 정보 저장 완료: reservationId=${reservationId}, paymentId=${it.id}" } log.debug { "[PaymentWriterV2.createPayment] 결제 승인 및 결제 정보 저장 완료: reservationId=${reservationId}, paymentId=${it.id}" }
} }
} }
private fun saveDetail( private fun createDetail(
paymentResponse: PaymentConfirmResponse, paymentResponse: PaymentConfirmResponse,
paymentId: Long, paymentId: Long,
): PaymentDetailEntity { ): PaymentDetailEntity {
@ -72,50 +57,24 @@ class PaymentWriterV2(
throw PaymentException(PaymentErrorCode.NOT_SUPPORTED_PAYMENT_TYPE) throw PaymentException(PaymentErrorCode.NOT_SUPPORTED_PAYMENT_TYPE)
} }
fun requestCancelPayment(
reservationId: Long,
request: ReservationCancelRequest,
): PaymentCancelResponseV2 {
log.debug { "[PaymentWriterV2.requestConfirmPayment] 결제 취소 요청 시작: reservationId=$reservationId, request=${request}" }
val payment: PaymentEntityV2 = paymentRepository.findByReservationId(reservationId)
?: throw PaymentException(PaymentErrorCode.PAYMENT_NOT_FOUND)
val paymentCancelRequest = PaymentCancelRequestV2(
paymentKey = payment.paymentKey,
cancelReason = request.cancelReason,
amount = payment.totalAmount
)
return paymentClient.cancel(paymentCancelRequest).also {
log.debug { "[PaymentWriterV2.requestCancelPayment] 결제 취소 요청 완료: reservationId=${reservationId}, paymentKey=${payment.paymentKey}" }
}
}
fun createCanceledPayment( fun createCanceledPayment(
memberId: Long, memberId: Long,
reservationId: Long, payment: PaymentEntityV2,
request: ReservationCancelRequest, requestedAt: LocalDateTime,
paymentCancelResponse: PaymentCancelResponseV2, cancelResponse: PaymentCancelResponseV2
requestedAt: LocalDateTime
) { ) {
val payment: PaymentEntityV2 = paymentRepository.findByReservationId(reservationId) log.debug { "[PaymentWriterV2.cancelPayment] 취소된 결제 정보 저장 시작: paymentId=${payment.id}, paymentKey=${payment.paymentKey}" }
?.also { it.cancel() }
?: throw PaymentException(PaymentErrorCode.PAYMENT_NOT_FOUND)
val cancelDetail: CancelDetail = paymentCancelResponse.cancels val canceledPayment: CanceledPaymentEntityV2 = cancelResponse.cancels.toEntity(
CanceledPaymentEntityV2(
id = tsidFactory.next(), id = tsidFactory.next(),
canceledAt = cancelDetail.canceledAt,
requestedAt = requestedAt,
paymentId = payment.id, paymentId = payment.id,
canceledBy = memberId, cancelRequestedAt = requestedAt,
cancelReason = request.cancelReason, canceledBy = memberId
cancelAmount = cancelDetail.cancelAmount, )
cardDiscountAmount = cancelDetail.cardDiscountAmount,
transferDiscountAmount = cancelDetail.transferDiscountAmount, canceledPaymentRepository.save(canceledPayment).also {
easypayDiscountAmount = cancelDetail.easyPayDiscountAmount, payment.cancel()
).also { canceledPaymentRepository.save(it) } log.debug { "[PaymentWriterV2.cancelPayment] 취소된 결제 정보 저장 완료: paymentId=${payment.id}, canceledPaymentId=${it.id}, paymentKey=${payment.paymentKey}" }
}
} }
} }

View File

@ -5,6 +5,8 @@ import io.github.oshai.kotlinlogging.KotlinLogging
import jakarta.transaction.Transactional import jakarta.transaction.Transactional
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import roomescape.common.util.TransactionExecutionUtil import roomescape.common.util.TransactionExecutionUtil
import roomescape.payment.implement.PaymentFinderV2
import roomescape.payment.implement.PaymentRequester
import roomescape.payment.implement.PaymentWriterV2 import roomescape.payment.implement.PaymentWriterV2
import roomescape.payment.infrastructure.client.v2.PaymentConfirmResponse import roomescape.payment.infrastructure.client.v2.PaymentConfirmResponse
import roomescape.payment.infrastructure.persistence.v2.PaymentEntityV2 import roomescape.payment.infrastructure.persistence.v2.PaymentEntityV2
@ -13,7 +15,6 @@ import roomescape.reservation.implement.ReservationWriter
import roomescape.reservation.infrastructure.persistence.ReservationEntity import roomescape.reservation.infrastructure.persistence.ReservationEntity
import roomescape.reservation.infrastructure.persistence.ReservationStatus import roomescape.reservation.infrastructure.persistence.ReservationStatus
import roomescape.reservation.web.* import roomescape.reservation.web.*
import java.time.LocalDateTime
private val log: KLogger = KotlinLogging.logger {} private val log: KLogger = KotlinLogging.logger {}
@ -21,10 +22,11 @@ private val log: KLogger = KotlinLogging.logger {}
class ReservationWithPaymentServiceV2( class ReservationWithPaymentServiceV2(
private val reservationWriter: ReservationWriter, private val reservationWriter: ReservationWriter,
private val reservationFinder: ReservationFinder, private val reservationFinder: ReservationFinder,
private val paymentRequester: PaymentRequester,
private val paymentFinder: PaymentFinderV2,
private val paymentWriter: PaymentWriterV2, private val paymentWriter: PaymentWriterV2,
private val transactionExecutionUtil: TransactionExecutionUtil private val transactionExecutionUtil: TransactionExecutionUtil,
) { ) {
@Transactional @Transactional
fun createPendingReservation(memberId: Long, request: ReservationCreateRequest): ReservationCreateResponseV2 { fun createPendingReservation(memberId: Long, request: ReservationCreateRequest): ReservationCreateResponseV2 {
log.info { log.info {
@ -59,29 +61,20 @@ class ReservationWithPaymentServiceV2(
"예약 결제 시작: memberId=$memberId, reservationId=$reservationId, request=$request" "예약 결제 시작: memberId=$memberId, reservationId=$reservationId, request=$request"
} }
val paymentConfirmResponse = paymentWriter.requestConfirmPayment(reservationId, request) val paymentConfirmResponse: PaymentConfirmResponse = paymentRequester.requestConfirmPayment(
paymentKey = request.paymentKey,
return transactionExecutionUtil.withNewTransaction(isReadOnly = false) { orderId = request.orderId,
savePayment(memberId, reservationId, request, paymentConfirmResponse) amount = request.amount
}
}
private fun savePayment(
memberId: Long,
reservationId: Long,
request: ReservationPaymentRequest,
paymentConfirmResponse: PaymentConfirmResponse
): ReservationPaymentResponse {
val reservation =
reservationFinder.findPendingReservation(reservationId, memberId).also { it.confirm() }
val payment: PaymentEntityV2 = paymentWriter.createPayment(
reservationId = reservationId,
request = request,
paymentConfirmResponse = paymentConfirmResponse
) )
return ReservationPaymentResponse(reservationId, reservation.status, payment.id, payment.status) return transactionExecutionUtil.withNewTransaction(isReadOnly = false) {
.also { log.info { "[ReservationWithPaymentServiceV2.payReservation] 예약 결제 완료: response=${it}" } } val payment: PaymentEntityV2 = paymentWriter.createPayment(reservationId, request, paymentConfirmResponse)
val reservation: ReservationEntity =
reservationWriter.modifyStatusFromPendingToConfirmed(reservationId, memberId)
ReservationPaymentResponse(reservationId, reservation.status, payment.id, payment.status)
.also { log.info { "[ReservationWithPaymentServiceV2.payReservation] 예약 결제 완료: response=${it}" } }
}
} }
fun cancelReservation( fun cancelReservation(
@ -89,17 +82,27 @@ class ReservationWithPaymentServiceV2(
reservationId: Long, reservationId: Long,
request: ReservationCancelRequest request: ReservationCancelRequest
) { ) {
val requestedAt: LocalDateTime = LocalDateTime.now()
log.info { log.info {
"[ReservationWithPaymentServiceV2.cancelReservation] " + "[ReservationWithPaymentServiceV2.cancelReservation] " +
"예약 취소 시작: memberId=$memberId, reservationId=$reservationId, request=$request" "예약 취소 시작: memberId=$memberId, reservationId=$reservationId, request=$request"
} }
val paymentCancelResponse = paymentWriter.requestCancelPayment(reservationId, request) val reservation: ReservationEntity = reservationFinder.findById(reservationId)
val payment: PaymentEntityV2 = paymentFinder.findPaymentByReservationId(reservationId)
val paymentCancelResponse = paymentRequester.requestCancelPayment(
paymentKey = payment.paymentKey,
amount = payment.totalAmount,
cancelReason = request.cancelReason
)
transactionExecutionUtil.withNewTransaction(isReadOnly = false) { transactionExecutionUtil.withNewTransaction(isReadOnly = false) {
paymentWriter.createCanceledPayment(memberId, reservationId, request, paymentCancelResponse, requestedAt) paymentWriter.createCanceledPayment(memberId, payment, request.requestedAt, paymentCancelResponse)
reservationFinder.findById(reservationId).also { reservationWriter.cancelByUser(it, memberId) } reservationWriter.modifyStatusToCanceledByUser(reservation, memberId)
}.also {
log.info {
"[ReservationWithPaymentServiceV2.cancelReservation] " +
"예약 취소 완료: reservationId=$reservationId, memberId=$memberId, cancelReason=${request.cancelReason}"
}
} }
} }
} }

View File

@ -153,4 +153,26 @@ class ReservationValidator(
throw ReservationException(ReservationErrorCode.RESERVATION_NOT_PENDING) throw ReservationException(ReservationErrorCode.RESERVATION_NOT_PENDING)
} }
} }
fun validateIsPending(reservation: ReservationEntity) {
log.debug { "[ReservationValidator.validateIsPending] 시작: reservationId=${reservation.id}, status=${reservation.status}" }
if (reservation.status != ReservationStatus.PENDING) {
log.warn { "[ReservationValidator.validateIsPending] 예약 상태가 결제 대기 중이 아님: reservationId=${reservation.id}, status=${reservation.status}" }
throw ReservationException(ReservationErrorCode.RESERVATION_NOT_PENDING)
}
log.debug { "[ReservationValidator.validateIsPending] 완료: reservationId=${reservation.id}, status=${reservation.status}" }
}
fun validateModifyAuthority(reservation: ReservationEntity, memberId: Long) {
log.debug { "[ReservationValidator.validateModifyAuthority] 시작: reservationId=${reservation.id}, memberId=$memberId" }
if (reservation.member.id != memberId) {
log.error { "[ReservationValidator.validateModifyAuthority] 예약자 본인이 아님: reservationId=${reservation.id}, reservation.memberId=${reservation.member.id} memberId=$memberId" }
throw ReservationException(ReservationErrorCode.NOT_RESERVATION_OWNER)
}
log.debug { "[ReservationValidator.validateModifyAuthority] 완료: reservationId=${reservation.id}, memberId=$memberId" }
}
} }

View File

@ -3,9 +3,12 @@ package roomescape.reservation.implement
import com.github.f4b6a3.tsid.TsidFactory import com.github.f4b6a3.tsid.TsidFactory
import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import roomescape.common.config.next import roomescape.common.config.next
import roomescape.member.implement.MemberFinder import roomescape.member.implement.MemberFinder
import roomescape.reservation.exception.ReservationErrorCode
import roomescape.reservation.exception.ReservationException
import roomescape.reservation.infrastructure.persistence.ReservationEntity import roomescape.reservation.infrastructure.persistence.ReservationEntity
import roomescape.reservation.infrastructure.persistence.ReservationRepository import roomescape.reservation.infrastructure.persistence.ReservationRepository
import roomescape.reservation.infrastructure.persistence.ReservationStatus import roomescape.reservation.infrastructure.persistence.ReservationStatus
@ -100,7 +103,7 @@ class ReservationWriter(
log.debug { "[ReservationWriter.confirm] 완료: reservationId=$reservationId, status=${ReservationStatus.CONFIRMED_PAYMENT_REQUIRED}" } log.debug { "[ReservationWriter.confirm] 완료: reservationId=$reservationId, status=${ReservationStatus.CONFIRMED_PAYMENT_REQUIRED}" }
} }
fun cancelByUser(reservation: ReservationEntity, requesterId: Long) { fun modifyStatusToCanceledByUser(reservation: ReservationEntity, requesterId: Long) {
log.debug { "[ReservationWriter.cancel] 예약 취소 시작: reservationId=${reservation.id}, requesterId=$requesterId" } log.debug { "[ReservationWriter.cancel] 예약 취소 시작: reservationId=${reservation.id}, requesterId=$requesterId" }
memberFinder.findById(requesterId) memberFinder.findById(requesterId)
@ -110,4 +113,19 @@ class ReservationWriter(
log.debug { "[ReservationWriter.cancel] 예약 취소 완료: reservationId=${reservation.id}" } log.debug { "[ReservationWriter.cancel] 예약 취소 완료: reservationId=${reservation.id}" }
} }
} }
fun modifyStatusFromPendingToConfirmed(reservationId: Long, memberId: Long): ReservationEntity {
log.debug { "[ReservationWriter.confirmPendingReservation] 시작: reservationId=$reservationId, memberId=$memberId" }
return reservationRepository.findByIdOrNull(reservationId)?.also {
reservationValidator.validateIsPending(it)
reservationValidator.validateModifyAuthority(it, memberId)
it.confirm()
log.debug { "[ReservationWriter.confirmPendingReservation] 완료: reservationId=${it.id}, status=${it.status}" }
} ?: run {
log.warn { "[ReservationWriter.confirmPendingReservation] 예약을 찾을 수 없음: reservationId=$reservationId" }
throw ReservationException(ReservationErrorCode.RESERVATION_NOT_FOUND)
}
}
} }