refactor: PaymentService 내 결제 확정 로직에 이벤트 발행 추가 및 테스트

This commit is contained in:
이상진 2025-10-16 13:36:27 +09:00
parent dbd2b9fb0c
commit c1eb1aa2b4
2 changed files with 131 additions and 2 deletions

View File

@ -8,9 +8,11 @@ import com.sangdol.roomescape.payment.exception.PaymentErrorCode
import com.sangdol.roomescape.payment.exception.PaymentException
import com.sangdol.roomescape.payment.infrastructure.client.TosspayClient
import com.sangdol.roomescape.payment.infrastructure.persistence.*
import com.sangdol.roomescape.payment.mapper.toEvent
import com.sangdol.roomescape.payment.mapper.toResponse
import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.context.ApplicationEventPublisher
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
@ -24,12 +26,14 @@ class PaymentService(
private val canceledPaymentRepository: CanceledPaymentRepository,
private val paymentWriter: PaymentWriter,
private val transactionExecutionUtil: TransactionExecutionUtil,
private val eventPublisher: ApplicationEventPublisher
) {
fun requestConfirm(request: PaymentConfirmRequest): PaymentGatewayResponse {
fun requestConfirm(reservationId: Long, request: PaymentConfirmRequest): PaymentGatewayResponse {
log.info { "[requestConfirm] 결제 요청 시작: paymentKey=${request.paymentKey}" }
try {
return paymentClient.confirm(request.paymentKey, request.orderId, request.amount).also {
log.info { "[requestConfirm] 결제 완료: paymentKey=${request.paymentKey}" }
eventPublisher.publishEvent(it.toEvent(reservationId))
log.info { "[requestConfirm] 결제 및 이벤트 발행 완료: paymentKey=${request.paymentKey}" }
}
} catch (e: Exception) {
when(e) {

View File

@ -0,0 +1,125 @@
package com.sangdol.roomescape.payment
import com.ninjasquad.springmockk.MockkBean
import com.sangdol.roomescape.payment.business.PaymentService
import com.sangdol.roomescape.payment.business.domain.PaymentMethod
import com.sangdol.roomescape.payment.business.domain.UserFacingPaymentErrorCode
import com.sangdol.roomescape.payment.business.event.PaymentEvent
import com.sangdol.roomescape.payment.business.event.PaymentEventListener
import com.sangdol.roomescape.payment.exception.ExternalPaymentException
import com.sangdol.roomescape.payment.exception.PaymentErrorCode
import com.sangdol.roomescape.payment.exception.PaymentException
import com.sangdol.roomescape.payment.infrastructure.client.TosspayClient
import com.sangdol.roomescape.supports.FunSpecSpringbootTest
import com.sangdol.roomescape.supports.PaymentFixture
import io.kotest.assertions.assertSoftly
import io.kotest.matchers.shouldBe
import io.mockk.*
import org.junit.jupiter.api.assertThrows
class PaymentServiceTest(
private val paymentService: PaymentService,
@MockkBean(relaxed = true) private val paymentEventListener: PaymentEventListener,
@MockkBean(relaxed = true) private val tosspayClient: TosspayClient
) : FunSpecSpringbootTest() {
init {
afterTest {
clearAllMocks()
}
context("결제를 승인한다.") {
val request = PaymentFixture.confirmRequest
test("결제 정상 승인 및 이벤트 발행 확인") {
val tosspayAPIResponse = PaymentFixture.confirmResponse(
paymentKey = request.paymentKey,
amount = request.amount,
orderId = request.orderId,
method = PaymentMethod.CARD
)
val paymentEventSlot = slot<PaymentEvent>()
every {
tosspayClient.confirm(request.paymentKey, request.orderId, request.amount)
} returns tosspayAPIResponse
every {
paymentEventListener.handlePaymentEvent(capture(paymentEventSlot))
} just runs
paymentService.requestConfirm(12345L, request)
assertSoftly(paymentEventSlot.captured) {
this.paymentKey shouldBe request.paymentKey
this.orderId shouldBe request.orderId
this.totalAmount shouldBe request.amount
this.method shouldBe PaymentMethod.CARD
}
}
context("외부 API 요청 과정에서 예외가 발생하면, 예외를 PaymentException으로 변환한 뒤 던진다.") {
test("외부 API가 4xx 응답을 보내면 ${PaymentErrorCode.PAYMENT_CLIENT_ERROR}로 변환하여 예외를 던진다.") {
val expectedErrorCode = PaymentErrorCode.PAYMENT_CLIENT_ERROR
val exception = ExternalPaymentException(400, "INVALID_REQUEST", "잘못된 요청입니다.")
every {
tosspayClient.confirm(request.paymentKey, request.orderId, request.amount)
} throws exception
assertThrows<PaymentException> {
paymentService.requestConfirm(12345L, request)
}.also {
it.errorCode shouldBe expectedErrorCode
it.message shouldBe expectedErrorCode.message
}
}
test("외부 API가 5xx 응답을 보내면 ${PaymentErrorCode.PAYMENT_PROVIDER_ERROR}로 변환하여 예외를 던진다.") {
val expectedErrorCode = PaymentErrorCode.PAYMENT_PROVIDER_ERROR
val exception = ExternalPaymentException(500, "UNKNOWN_PAYMENT_ERROR", "결제에 실패했어요. 같은 문제가 반복된다면 은행이나 카드사로 문의해주세요.")
every {
tosspayClient.confirm(request.paymentKey, request.orderId, request.amount)
} throws exception
assertThrows<PaymentException> {
paymentService.requestConfirm(12345L, request)
}.also {
it.errorCode shouldBe expectedErrorCode
it.message shouldBe expectedErrorCode.message
}
}
test("외부 API의 에러코드가 ${UserFacingPaymentErrorCode::class.simpleName}에 있으면 해당 예외 메시지를 담아 던진다.") {
val expectedErrorCode = PaymentErrorCode.PAYMENT_CLIENT_ERROR
val exception = ExternalPaymentException(400, "EXCEED_MAX_CARD_INSTALLMENT_PLAN", "설정 가능한 최대 할부 개월 수를 초과했습니다.")
every {
tosspayClient.confirm(request.paymentKey, request.orderId, request.amount)
} throws exception
assertThrows<PaymentException> {
paymentService.requestConfirm(12345L, request)
}.also {
it.errorCode shouldBe expectedErrorCode
it.message shouldBe "${expectedErrorCode.message}(${exception.message})"
}
}
test("외부 API에서 예상치 못한 예외가 발생한 경우 ${PaymentErrorCode.PAYMENT_UNEXPECTED_ERROR}로 변환한다.") {
val expectedErrorCode = PaymentErrorCode.PAYMENT_UNEXPECTED_ERROR
every {
tosspayClient.confirm(request.paymentKey, request.orderId, request.amount)
} throws Exception("unexpected")
assertThrows<PaymentException> {
paymentService.requestConfirm(12345L, request)
}.also {
it.errorCode shouldBe expectedErrorCode
it.message shouldBe expectedErrorCode.message
}
}
}
}
}
}