From 313d9850d1154a184a0de8a6bf51dbf7e68a0f36 Mon Sep 17 00:00:00 2001 From: pricelees Date: Wed, 16 Jul 2025 12:09:17 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20TossPaymentClient=20=EC=BD=94?= =?UTF-8?q?=ED=8B=80=EB=A6=B0=20=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client/TossPaymentClient.kt | 178 ++++++++++-------- .../client/TossPaymentClientTest.java | 117 ------------ .../client/TossPaymentClientTest.kt | 139 ++++++++++++++ 3 files changed, 237 insertions(+), 197 deletions(-) delete mode 100644 src/test/java/roomescape/payment/infrastructure/client/TossPaymentClientTest.java create mode 100644 src/test/java/roomescape/payment/infrastructure/client/TossPaymentClientTest.kt diff --git a/src/main/java/roomescape/payment/infrastructure/client/TossPaymentClient.kt b/src/main/java/roomescape/payment/infrastructure/client/TossPaymentClient.kt index 9b2cba2c..e46b95da 100644 --- a/src/main/java/roomescape/payment/infrastructure/client/TossPaymentClient.kt +++ b/src/main/java/roomescape/payment/infrastructure/client/TossPaymentClient.kt @@ -1,95 +1,113 @@ -package roomescape.payment.infrastructure.client; +package roomescape.payment.infrastructure.client -import java.io.IOException; -import java.io.InputStream; -import java.util.Map; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.http.HttpStatusCode; -import org.springframework.http.MediaType; -import org.springframework.http.client.ClientHttpResponse; -import org.springframework.stereotype.Component; -import org.springframework.web.client.RestClient; - -import com.fasterxml.jackson.databind.ObjectMapper; - -import roomescape.common.exception.ErrorType; -import roomescape.common.exception.RoomescapeException; -import roomescape.payment.web.PaymentApprove; -import roomescape.payment.web.PaymentCancel; +import com.fasterxml.jackson.databind.ObjectMapper +import io.github.oshai.kotlinlogging.KLogger +import io.github.oshai.kotlinlogging.KotlinLogging +import org.springframework.http.HttpRequest +import org.springframework.http.HttpStatus +import org.springframework.http.HttpStatusCode +import org.springframework.http.MediaType +import org.springframework.http.client.ClientHttpResponse +import org.springframework.stereotype.Component +import org.springframework.web.client.RestClient +import roomescape.common.exception.ErrorType +import roomescape.common.exception.RoomescapeException +import roomescape.payment.web.PaymentApprove +import roomescape.payment.web.PaymentCancel +import java.io.IOException +import java.util.Map @Component -public class TossPaymentClient { +class TossPaymentClient( + private val log: KLogger = KotlinLogging.logger {}, + tossPaymentClientBuilder: RestClient.Builder +) { + companion object { + private const val CONFIRM_URL: String = "/v1/payments/confirm" + private const val CANCEL_URL: String = "/v1/payments/{paymentKey}/cancel" + } - private static final Logger log = LoggerFactory.getLogger(TossPaymentClient.class); + private val tossPaymentClient: RestClient = tossPaymentClientBuilder.build() - private final RestClient tossPaymentClient; + fun confirmPayment(paymentRequest: PaymentApprove.Request): PaymentApprove.Response { + logPaymentInfo(paymentRequest) - public TossPaymentClient(RestClient.Builder tossPaymentClientBuilder) { - this.tossPaymentClient = tossPaymentClientBuilder.build(); - } + return tossPaymentClient.post() + .uri(CONFIRM_URL) + .contentType(MediaType.APPLICATION_JSON) + .body(paymentRequest) + .retrieve() + .onStatus( + { status: HttpStatusCode -> status.is4xxClientError || status.is5xxServerError }, + { req: HttpRequest, res: ClientHttpResponse -> handlePaymentError(res) } + ) + .body(PaymentApprove.Response::class.java) + ?: throw RoomescapeException(ErrorType.PAYMENT_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR) + } - public PaymentApprove.Response confirmPayment(PaymentApprove.Request paymentRequest) { - logPaymentInfo(paymentRequest); - return tossPaymentClient.post() - .uri("/v1/payments/confirm") - .contentType(MediaType.APPLICATION_JSON) - .body(paymentRequest) - .retrieve() - .onStatus(status -> status.is4xxClientError() || status.is5xxServerError(), - (req, res) -> handlePaymentError(res)) - .body(PaymentApprove.Response.class); - } + fun cancelPayment(cancelRequest: PaymentCancel.Request): PaymentCancel.Response { + logPaymentCancelInfo(cancelRequest) + val param = Map.of("cancelReason", cancelRequest.cancelReason) - public PaymentCancel.Response cancelPayment(PaymentCancel.Request cancelRequest) { - logPaymentCancelInfo(cancelRequest); - Map param = Map.of("cancelReason", cancelRequest.cancelReason); + return tossPaymentClient.post() + .uri(CANCEL_URL, cancelRequest.paymentKey) + .contentType(MediaType.APPLICATION_JSON) + .body(param) + .retrieve() + .onStatus( + { status: HttpStatusCode -> status.is4xxClientError || status.is5xxServerError }, + { req: HttpRequest, res: ClientHttpResponse -> handlePaymentError(res) } + ) + .body(PaymentCancel.Response::class.java) + ?: throw RoomescapeException(ErrorType.PAYMENT_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR) + } - return tossPaymentClient.post() - .uri("/v1/payments/{paymentKey}/cancel", cancelRequest.paymentKey) - .contentType(MediaType.APPLICATION_JSON) - .body(param) - .retrieve() - .onStatus(status -> status.is4xxClientError() || status.is5xxServerError(), - (req, res) -> handlePaymentError(res)) - .body(PaymentCancel.Response.class); - } + private fun logPaymentInfo(paymentRequest: PaymentApprove.Request) { + log.info { + "결제 승인 요청: paymentKey=${paymentRequest.paymentKey}, orderId=${paymentRequest.orderId}, " + + "amount=${paymentRequest.amount}, paymentType=${paymentRequest.paymentType}" + } + } - private void logPaymentInfo(PaymentApprove.Request paymentRequest) { - log.info("결제 승인 요청: paymentKey={}, orderId={}, amount={}, paymentType={}", - paymentRequest.paymentKey, paymentRequest.orderId, paymentRequest.amount, - paymentRequest.paymentType); - } + private fun logPaymentCancelInfo(cancelRequest: PaymentCancel.Request) { + log.info { + "결제 취소 요청: paymentKey=${cancelRequest.paymentKey}, amount=${cancelRequest.amount}, " + + "cancelReason=${cancelRequest.cancelReason}" + } + } - private void logPaymentCancelInfo(PaymentCancel.Request cancelRequest) { - log.info("결제 취소 요청: paymentKey={}, amount={}, cancelReason={}", - cancelRequest.paymentKey, cancelRequest.amount, cancelRequest.cancelReason); - } + @Throws(IOException::class) + private fun handlePaymentError( + res: ClientHttpResponse + ): Nothing { + val statusCode = res.statusCode + val errorType = getErrorTypeByStatusCode(statusCode) + val errorResponse = getErrorResponse(res) - private void handlePaymentError(ClientHttpResponse res) - throws IOException { - HttpStatusCode statusCode = res.getStatusCode(); - ErrorType errorType = getErrorTypeByStatusCode(statusCode); - TossPaymentErrorResponse errorResponse = getErrorResponse(res); + throw RoomescapeException( + errorType, + "[ErrorCode = ${errorResponse.code}, ErrorMessage = ${errorResponse.message}]", + statusCode + ) + } - throw new RoomescapeException(errorType, - String.format("[ErrorCode = %s, ErrorMessage = %s]", errorResponse.code, errorResponse.message), - statusCode); - } + @Throws(IOException::class) + private fun getErrorResponse( + res: ClientHttpResponse + ): TossPaymentErrorResponse { + val body = res.body + val objectMapper = ObjectMapper() + val errorResponse = objectMapper.readValue(body, TossPaymentErrorResponse::class.java) + body.close() + return errorResponse + } - private TossPaymentErrorResponse getErrorResponse(ClientHttpResponse res) throws IOException { - InputStream body = res.getBody(); - ObjectMapper objectMapper = new ObjectMapper(); - TossPaymentErrorResponse errorResponse = objectMapper.readValue(body, TossPaymentErrorResponse.class); - body.close(); - return errorResponse; - } - - private ErrorType getErrorTypeByStatusCode(HttpStatusCode statusCode) { - if (statusCode.is4xxClientError()) { - return ErrorType.PAYMENT_ERROR; - } - return ErrorType.PAYMENT_SERVER_ERROR; - } + private fun getErrorTypeByStatusCode( + statusCode: HttpStatusCode + ): ErrorType { + if (statusCode.is4xxClientError) { + return ErrorType.PAYMENT_ERROR + } + return ErrorType.PAYMENT_SERVER_ERROR + } } diff --git a/src/test/java/roomescape/payment/infrastructure/client/TossPaymentClientTest.java b/src/test/java/roomescape/payment/infrastructure/client/TossPaymentClientTest.java deleted file mode 100644 index 29069c64..00000000 --- a/src/test/java/roomescape/payment/infrastructure/client/TossPaymentClientTest.java +++ /dev/null @@ -1,117 +0,0 @@ -package roomescape.payment.infrastructure.client; - -import static org.assertj.core.api.Assertions.*; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.AssertionsForClassTypes.*; -import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; -import static org.springframework.test.web.client.match.MockRestRequestMatchers.*; -import static org.springframework.test.web.client.response.MockRestResponseCreators.*; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.client.RestClientTest; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.test.web.client.MockRestServiceServer; - -import roomescape.common.exception.ErrorType; -import roomescape.common.exception.RoomescapeException; -import roomescape.payment.SampleTossPaymentConst; -import roomescape.payment.web.PaymentApprove; -import roomescape.payment.web.PaymentCancel; - -@RestClientTest(TossPaymentClient.class) -class TossPaymentClientTest { - - @Autowired - private TossPaymentClient tossPaymentClient; - - @Autowired - private MockRestServiceServer mockServer; - - @Test - @DisplayName("결제를 승인한다.") - void confirmPayment() { - // given - mockServer.expect(requestTo("/v1/payments/confirm")) - .andExpect(method(HttpMethod.POST)) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(content().json(SampleTossPaymentConst.paymentRequestJson)) - .andRespond(withStatus(HttpStatus.OK) - .contentType(MediaType.APPLICATION_JSON) - .body(SampleTossPaymentConst.confirmJson)); - - // when - PaymentApprove.Request paymentRequest = SampleTossPaymentConst.paymentRequest; - PaymentApprove.Response paymentResponse = tossPaymentClient.confirmPayment(paymentRequest); - - // then - assertThat(paymentResponse.paymentKey).isEqualTo(paymentRequest.paymentKey); - assertThat(paymentResponse.orderId).isEqualTo(paymentRequest.orderId); - } - - @Test - @DisplayName("결제를 취소한다.") - void cancelPayment() { - // given - mockServer.expect(requestTo("/v1/payments/5EnNZRJGvaBX7zk2yd8ydw26XvwXkLrx9POLqKQjmAw4b0e1/cancel")) - .andExpect(method(HttpMethod.POST)) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(content().json(SampleTossPaymentConst.cancelRequestJson)) - .andRespond(withStatus(HttpStatus.OK) - .contentType(MediaType.APPLICATION_JSON) - .body(SampleTossPaymentConst.cancelJson)); - - // when - PaymentCancel.Request cancelRequest = SampleTossPaymentConst.cancelRequest; - PaymentCancel.Response paymentCancelResponse = tossPaymentClient.cancelPayment(cancelRequest); - - // then - assertThat(paymentCancelResponse.cancelStatus).isEqualTo("DONE"); - assertThat(paymentCancelResponse.cancelReason).isEqualTo(cancelRequest.cancelReason); - } - - @Test - @DisplayName("결제 승인 중 400 에러가 발생한다.") - void confirmPaymentWithError() { - // given - mockServer.expect(requestTo("/v1/payments/confirm")) - .andExpect(method(HttpMethod.POST)) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(content().json(SampleTossPaymentConst.paymentRequestJson)) - .andRespond(withStatus(HttpStatus.BAD_REQUEST) - .contentType(MediaType.APPLICATION_JSON) - .body(SampleTossPaymentConst.tossPaymentErrorJson)); - - // when & then - assertThatThrownBy(() -> tossPaymentClient.confirmPayment(SampleTossPaymentConst.paymentRequest)) - .isInstanceOf(RoomescapeException.class) - .hasFieldOrPropertyWithValue("errorType", ErrorType.PAYMENT_ERROR) - .hasFieldOrPropertyWithValue("invalidValue", - "[ErrorCode = ERROR_CODE, ErrorMessage = Error message]") - .hasFieldOrPropertyWithValue("httpStatus", HttpStatus.BAD_REQUEST); - } - - @Test - @DisplayName("결제 취소 중 500 에러가 발생한다.") - void cancelPaymentWithError() { - // given - mockServer.expect(requestTo("/v1/payments/5EnNZRJGvaBX7zk2yd8ydw26XvwXkLrx9POLqKQjmAw4b0e1/cancel")) - .andExpect(method(HttpMethod.POST)) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(content().json(SampleTossPaymentConst.cancelRequestJson)) - .andRespond(withStatus(HttpStatus.INTERNAL_SERVER_ERROR) - .contentType(MediaType.APPLICATION_JSON) - .body(SampleTossPaymentConst.tossPaymentErrorJson)); - - // when & then - assertThatThrownBy(() -> tossPaymentClient.cancelPayment(SampleTossPaymentConst.cancelRequest)) - .isInstanceOf(RoomescapeException.class) - .hasFieldOrPropertyWithValue("errorType", ErrorType.PAYMENT_SERVER_ERROR) - .hasFieldOrPropertyWithValue("invalidValue", - "[ErrorCode = ERROR_CODE, ErrorMessage = Error message]") - .hasFieldOrPropertyWithValue("httpStatus", HttpStatus.INTERNAL_SERVER_ERROR); - } -} diff --git a/src/test/java/roomescape/payment/infrastructure/client/TossPaymentClientTest.kt b/src/test/java/roomescape/payment/infrastructure/client/TossPaymentClientTest.kt new file mode 100644 index 00000000..a36e409e --- /dev/null +++ b/src/test/java/roomescape/payment/infrastructure/client/TossPaymentClientTest.kt @@ -0,0 +1,139 @@ +package roomescape.payment.infrastructure.client + +import io.kotest.assertions.assertSoftly +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.client.RestClientTest +import org.springframework.http.HttpMethod +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.test.web.client.MockRestServiceServer +import org.springframework.test.web.client.ResponseActions +import org.springframework.test.web.client.match.MockRestRequestMatchers.* +import org.springframework.test.web.client.response.MockRestResponseCreators.withStatus +import org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess +import roomescape.common.exception.ErrorType +import roomescape.common.exception.RoomescapeException +import roomescape.payment.SampleTossPaymentConst +import roomescape.payment.web.PaymentApprove +import roomescape.payment.web.PaymentCancel + +@RestClientTest(TossPaymentClient::class) +class TossPaymentClientTest( + @Autowired val client: TossPaymentClient, + @Autowired val mockServer: MockRestServiceServer +) : FunSpec() { + + init { + context("결제 승인 요청") { + fun commonAction(): ResponseActions = mockServer.expect { + requestTo("/v1/payments/confirm") + }.andExpect { + method(HttpMethod.POST) + }.andExpect { + content().contentType(MediaType.APPLICATION_JSON) + }.andExpect { + content().json(SampleTossPaymentConst.paymentRequestJson) + } + + test("성공 응답") { + commonAction().andRespond { + withSuccess() + .contentType(MediaType.APPLICATION_JSON) + .body(SampleTossPaymentConst.confirmJson) + .createResponse(it) + } + + // when + val paymentRequest = SampleTossPaymentConst.paymentRequest + val paymentResponse: PaymentApprove.Response = client.confirmPayment(paymentRequest) + + assertSoftly(paymentResponse) { + this.paymentKey shouldBe paymentRequest.paymentKey + this.orderId shouldBe paymentRequest.orderId + this.totalAmount shouldBe paymentRequest.amount + } + } + + test("400 에러 발생") { + commonAction().andRespond { + withStatus(HttpStatus.BAD_REQUEST) + .contentType(MediaType.APPLICATION_JSON) + .body(SampleTossPaymentConst.tossPaymentErrorJson) + .createResponse(it) + } + + // when + val paymentRequest = SampleTossPaymentConst.paymentRequest + + // then + val exception = shouldThrow { + client.confirmPayment(paymentRequest) + } + + assertSoftly(exception) { + this.errorType shouldBe ErrorType.PAYMENT_ERROR + this.invalidValue shouldBe "[ErrorCode = ERROR_CODE, ErrorMessage = Error message]" + this.httpStatus shouldBe HttpStatus.BAD_REQUEST + } + + } + } + + context("결제 취소 요청") { + fun commonAction(): ResponseActions = mockServer.expect { + requestTo("/v1/payments/${SampleTossPaymentConst.paymentKey}/cancel") + }.andExpect { + method(HttpMethod.POST) + }.andExpect { + content().contentType(MediaType.APPLICATION_JSON) + }.andExpect { + content().json(SampleTossPaymentConst.cancelRequestJson) + } + + test("성공 응답") { + commonAction().andRespond { + withSuccess() + .contentType(MediaType.APPLICATION_JSON) + .body(SampleTossPaymentConst.cancelJson) + .createResponse(it) + } + + // when + val cancelRequest: PaymentCancel.Request = SampleTossPaymentConst.cancelRequest + val cancelResponse: PaymentCancel.Response = client.cancelPayment(cancelRequest) + + assertSoftly(cancelResponse) { + this.cancelStatus shouldBe "DONE" + this.cancelReason shouldBe cancelRequest.cancelReason + this.cancelAmount shouldBe cancelRequest.amount + } + } + + test("500 에러 발생") { + commonAction().andRespond { + withStatus(HttpStatus.INTERNAL_SERVER_ERROR) + .contentType(MediaType.APPLICATION_JSON) + .body(SampleTossPaymentConst.tossPaymentErrorJson) + .createResponse(it) + } + + // when + val cancelRequest: PaymentCancel.Request = SampleTossPaymentConst.cancelRequest + + // then + val exception = shouldThrow { + client.cancelPayment(cancelRequest) + } + + assertSoftly(exception) { + this.errorType shouldBe ErrorType.PAYMENT_SERVER_ERROR + this.invalidValue shouldBe "[ErrorCode = ERROR_CODE, ErrorMessage = Error message]" + this.httpStatus shouldBe HttpStatus.INTERNAL_SERVER_ERROR + } + } + } + } +}