-
Notifications
You must be signed in to change notification settings - Fork 82
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[1단계 방탈출 결제 / 배포] 몰리(김지민) 미션 제출합니다. #46
Changes from 25 commits
37f366d
60459e1
02cfe1d
cf2aaeb
f14d05e
6526ae9
89e452c
3750a51
40b81f8
70cb8a0
4664d31
c5f4a34
8d4e573
0cb75e4
43d3457
cf8f2e9
fc576c4
846f338
f7c1236
e0f50e2
06bbf14
e8ca752
72a0361
09a27a8
a8095d6
aab08d3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package roomescape.common.exception; | ||
|
||
public class ClientException extends RuntimeException { | ||
public ClientException(final String message) { | ||
super(message); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package roomescape.payment.client; | ||
|
||
import roomescape.payment.dto.request.ConfirmPaymentRequest; | ||
import roomescape.payment.model.Payment; | ||
|
||
public interface PaymentClient { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 인터페이스화 한거 너무 좋네요! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 꼭 toss 말고 다른 결제 client를 사용하게 될 수도 있어서 추상화가 필요한 부분이라고 생각했어요 ㅎㅎ |
||
|
||
Payment confirm(ConfirmPaymentRequest confirmPaymentRequest); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package roomescape.payment.client; | ||
|
||
import org.springframework.boot.context.properties.ConfigurationProperties; | ||
|
||
@ConfigurationProperties(value = "payment") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 해당 어노테이션은 어떤 역할을 해줘요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Spring boot가 자동으로 외부 설정 파일(properties 또는 yml)의 속성값을 인식하여 클래스의 필드값으로 바인딩해주는 어노테이션입니다 ! 기존에 사용했던 @value보다 쉽게 값들을 주입받을 수 있고 클래스로 더 편하게 관리할 수 있다고 생각하여 사용했습니다 😁 |
||
public class PaymentProperties { | ||
|
||
private final String secretKey; | ||
private final String password; | ||
|
||
public PaymentProperties(final String secretKey, final String password) { | ||
this.secretKey = secretKey; | ||
this.password = password; | ||
} | ||
|
||
public String getSecretKey() { | ||
return secretKey; | ||
} | ||
|
||
public String getPassword() { | ||
return password; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
package roomescape.payment.client; | ||
|
||
import java.nio.charset.StandardCharsets; | ||
import java.util.Base64; | ||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.http.HttpHeaders; | ||
import org.springframework.http.client.ClientHttpRequestFactory; | ||
import org.springframework.http.client.SimpleClientHttpRequestFactory; | ||
import org.springframework.web.client.RestClient; | ||
import roomescape.payment.client.toss.TossPaymentErrorHandler; | ||
|
||
@Configuration | ||
public class PaymentRestClientConfiguration { | ||
|
||
private static final String BASIC_PREFIX = "Basic "; | ||
|
||
private final PaymentProperties paymentProperties; | ||
|
||
public PaymentRestClientConfiguration(final PaymentProperties paymentProperties) { | ||
this.paymentProperties = paymentProperties; | ||
} | ||
|
||
@Bean | ||
public RestClient tossRestClient() { | ||
byte[] encodedBytes = Base64.getEncoder() | ||
.encode((paymentProperties.getSecretKey() + paymentProperties.getPassword()) | ||
.getBytes(StandardCharsets.UTF_8)); | ||
|
||
String authorizations = BASIC_PREFIX + new String(encodedBytes); | ||
|
||
return RestClient.builder() | ||
.baseUrl("https://api.tosspayments.com/v1/payments") | ||
.defaultHeader(HttpHeaders.AUTHORIZATION, authorizations) | ||
.defaultStatusHandler(new TossPaymentErrorHandler()) | ||
.requestFactory(getClientHttpRequestFactory()) | ||
.build(); | ||
} | ||
|
||
private ClientHttpRequestFactory getClientHttpRequestFactory() { | ||
SimpleClientHttpRequestFactory simpleClientHttpRequestFactory = new SimpleClientHttpRequestFactory(); | ||
simpleClientHttpRequestFactory.setConnectTimeout(3); | ||
simpleClientHttpRequestFactory.setReadTimeout(30); | ||
Comment on lines
+42
to
+43
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 타임 아웃 설정 시간 기준
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 굿굿! 좋네요 💯 |
||
return simpleClientHttpRequestFactory; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package roomescape.payment.client.toss; | ||
|
||
public record TossClientErrorResponse(String code, | ||
String message) { | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
package roomescape.payment.client.toss; | ||
|
||
|
||
import java.util.Arrays; | ||
|
||
public enum TossErrorCodeNotForUser { | ||
INVALID_API_KEY, | ||
NOT_FOUND_TERMINAL_ID, | ||
INVALID_AUTHORIZE_AUTH, | ||
INVALID_UNREGISTERED_SUBMALL, | ||
NOT_REGISTERED_BUSINESS, | ||
UNAPPROVED_ORDER_ID, | ||
UNAUTHORIZED_KEY, | ||
FORBIDDEN_REQUEST, | ||
INCORRECT_BASIC_AUTH_FORMAT, | ||
; | ||
Comment on lines
+7
to
+16
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 조앤 이 부분도 수업 시간을 통해 다시 생각해보게 되면서 추가한 부분인데요 ! 1차 리뷰 요청 당시에는 토스 측의 에러 메시지를 전부 그대로 매핑하여 에러를 터뜨렸었어요.
그런데 위 질문을 통해 고민해본 결과 토스 client 요청 중 발생할 수 있는 에러 중에서 사용자에게 알릴 코드와 아닌 코드를 분리해서 처리하는 것이 적절하다고 생각했어요.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 예를 들어 토스쪽에서 해당 에러코드를 breaking change 내는 경우, 사용하는 쪽에서 1:1로 매핑하고 있다면 일일이 다 바꿔줘야하는 불편함이 있을 것 같아요. 또한 그걸 모르고 계속 유지보수한다면 예상치 못한 상황이 발생할 수도 있고요ㅎㅎ 다 다를 것 같긴 한데 제 경험으로는 요구사항을 바탕으로 발생가능한 예외 상황에 대해 다같이 논의해요! 저는 좀 스타트업쪽이라 그런데 큰회사는 요구사항이 찍혀내려오는 경우도 있다고 들었어요. 저도 좀 비슷하게 생각하긴 해요. 에러라는게 사실 피드백인데, 다음에 어떤 행위를 해야 개선할 수 있는지 가이드가 되지 않는다면 의미 없다고 생각하긴 해요. 하지만 사용성의 측면에서 유저에게 지금이 어떤 상황인지 정도를 알리는 용도로서 적절한 메시지로 변환해서 내릴 순 있을 것 같아요. |
||
|
||
public static boolean hasContains(final String message) { | ||
return Arrays.stream(TossErrorCodeNotForUser.values()) | ||
.anyMatch(tossErrorCodeNotForUser -> tossErrorCodeNotForUser.name().equals(message)); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package roomescape.payment.client.toss; | ||
|
||
import org.springframework.stereotype.Component; | ||
import org.springframework.web.client.RestClient; | ||
import roomescape.payment.client.PaymentClient; | ||
import roomescape.payment.dto.request.ConfirmPaymentRequest; | ||
import roomescape.payment.model.Payment; | ||
|
||
@Component | ||
public class TossPaymentClient implements PaymentClient { | ||
|
||
private final RestClient restClient; | ||
|
||
public TossPaymentClient(final RestClient restClient) { | ||
this.restClient = restClient; | ||
} | ||
|
||
@Override | ||
public Payment confirm(ConfirmPaymentRequest confirmPaymentRequest) { | ||
return restClient.post() | ||
.uri("/confirm") | ||
.body(confirmPaymentRequest) | ||
.retrieve() | ||
.toEntity(Payment.class) | ||
.getBody(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package roomescape.payment.client.toss; | ||
|
||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import java.io.IOException; | ||
import org.springframework.http.client.ClientHttpResponse; | ||
import org.springframework.web.client.ResponseErrorHandler; | ||
import roomescape.common.exception.ClientException; | ||
|
||
public class TossPaymentErrorHandler implements ResponseErrorHandler { | ||
private static final ObjectMapper objectMapper = new ObjectMapper(); | ||
|
||
@Override | ||
public boolean hasError(final ClientHttpResponse response) throws IOException { | ||
return response.getStatusCode().is4xxClientError() || response.getStatusCode().is5xxServerError(); | ||
} | ||
|
||
@Override | ||
public void handleError(final ClientHttpResponse response) throws IOException { | ||
TossClientErrorResponse tossClientErrorResponse = objectMapper.readValue(response.getBody(), | ||
TossClientErrorResponse.class); | ||
|
||
if (TossErrorCodeNotForUser.hasContains(tossClientErrorResponse.code())) { | ||
throw new ClientException("결제 오류입니다. 같은 문제가 반복된다면 문의해주세요."); | ||
} | ||
|
||
throw new ClientException(tossClientErrorResponse.message()); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package roomescape.payment.dto.request; | ||
|
||
import roomescape.reservation.dto.request.CreateMyReservationRequest; | ||
|
||
public record ConfirmPaymentRequest(String paymentKey, | ||
String orderId, | ||
Long amount) { | ||
public static ConfirmPaymentRequest from(final CreateMyReservationRequest createMyReservationRequest) { | ||
return new ConfirmPaymentRequest( | ||
createMyReservationRequest.paymentKey(), | ||
createMyReservationRequest.orderId(), | ||
createMyReservationRequest.amount()); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
package roomescape.payment.model; | ||
|
||
public class Payment { | ||
private String paymentKey; | ||
private String orderId; | ||
private Long amount; | ||
|
||
public Payment(final String paymentKey, final String orderId, final Long amount) { | ||
this.paymentKey = paymentKey; | ||
this.orderId = orderId; | ||
this.amount = amount; | ||
} | ||
|
||
public String getPaymentKey() { | ||
return paymentKey; | ||
} | ||
|
||
public String getOrderId() { | ||
return orderId; | ||
} | ||
|
||
public Long getAmount() { | ||
return amount; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
package roomescape.payment.service; | ||
|
||
import org.springframework.stereotype.Service; | ||
import roomescape.payment.client.PaymentClient; | ||
import roomescape.payment.dto.request.ConfirmPaymentRequest; | ||
import roomescape.payment.model.Payment; | ||
|
||
@Service | ||
public class PaymentService { | ||
|
||
private final PaymentClient paymentClient; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 다른 서비스에서 Client를 직접 호출하는 게 아니라 Service로 한번 더 감싸주셨군요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 현재는 "결제 승인"이라는 기능에 대해 단순 외부 client 연동만 하면 된다는 미션 요구사항이 제공되었지만, |
||
|
||
public PaymentService(PaymentClient paymentClient) { | ||
this.paymentClient = paymentClient; | ||
} | ||
|
||
public Payment createPayment(ConfirmPaymentRequest paymentRequest) { | ||
return paymentClient.confirm(paymentRequest); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍