결제를 구현하기 전, 주문을 먼저 구현하면서 주문 ID인 orderId
의 타입을 단순히 Long
으로 지정했었다.
그러나 토스페이먼츠 PG 연동을 위해서는 orderId
가 다음 조건을 만족해야 했다.
당연히 단순한 Long
타입은 알맞지 않았기 때문에 orderId
타입을 수정해야 했고, UUID
를 사용하기로 결정했다.
UUID를 사용한 이유는 다음과 같다.
다만, 한 가지 걸리는 점이 UUID
는 128비트의 값으로 표현되며, 문자열로 표현하면 하이픈(-) 포함 36자리의 길이를 가진다.
따라서 PK를 길이 36의 문자열(하이픈 제거하면 32자리)로 사용하면 성능적인 문제가 어느정도 있을 것이라 생각을 했다.
물론 이런 부분까지 고려할 정도의 성능을 요구하진 않겠지만, 그래도 고민해보는 것과 아닌 것에는 큰 차이가 있다고 생각한다.
아무튼 하이픈(-)은 제외하고 CHAR(32)
타입으로 저장한다면, 8 bits * 32 = 256 bits 의 크기를 차지하는데, 위에서 말했듯이 UUID
는 128 bits의 크기를 가진다.
즉, 128 bits의 공간이 낭비되고 있기 때문에 BINARY(16)
타입을 사용하여 공간을 절약할 수 있다.
이마저도 오버헤드가 크다고 생각된다면 64 bits 크기를 갖는 Snowflake ID
를 사용할 수 있지만 현재로서는 UUID
로도 충분하다.
이전 포스트에서 설명했던 것 처럼 토스페이먼츠의 결제 플로우는 다음과 같다.
successUrl
과 함께 결제 요청successUrl
로 리다이렉션Payment
객체 응답이 사이에 내가 구현한 스프링 서버의 역할을 포함시켜야 하고, 그렇게 수정된 플로우는 다음과 같다.
successUrl
과 함께 결제 요청successUrl
로 리다이렉션Payment
객체 정보 저장스프링 서버에서 대신 토스페이먼츠로 결제 승인 요청을 보내고, 받은 데이터를 DB에 저장하는 방식이다.
플로우는 정해졌으니 이제 구현을 해보자.
결제 승인 요청을 보내기 위한 비즈니스 로직을 구현했고, 역할은 다음과 같다.
Payment
저장클래스명은 TossPaymentService
로 지었고, 코드는 길지 않다.
private final PaymentRepository paymentRepository;
private final TossPaymentClient tossPaymentClient;
@Transactional
public void confirm(TossPaymentConfirmRequest confirmRequest) {
paymentRepository.findById(confirmRequest.getPaymentKey())
.ifPresent(v -> {
throw new DuplicatePaymentKeyException();
});
var response = tossPaymentClient.confirmPayment(confirmRequest);
Payment payment = Objects.requireNonNull(response.getBody())
.toEntity();
if (response.getStatusCode().is2xxSuccessful()) {
payment.setConfirmed();
}
paymentRepository.save(payment);
}
이 때, 토스페이먼츠 결제 승인 API를 호출하기 위해 RestClient
를 사용했는데,
외부 API 로직 모킹을 통한 Testability
보장을 위해 별도의 클래스인 TossPaymentClient
에 구현하였다.
TossPaymentClient
코드는 아래와 같다.
private final RestClient restClient;
@Value("${spring.payment.toss.secret-key}")
private String secretKey;
@Value("${spring.payment.toss.base-url}")
private String baseUrl;
public ResponseEntity<TossPaymentConfirmResponse> confirmPayment(TossPaymentConfirmRequest confirmRequest) {
Encoder encoder = Base64.getEncoder();
byte[] encodedBytes = encoder.encode((secretKey + ":").getBytes(StandardCharsets.UTF_8));
String authorization = "Basic " + new String(encodedBytes);
return restClient.post()
.uri(baseUrl + "/confirm")
.contentType(MediaType.APPLICATION_JSON)
.header("Authorization", authorization)
.body(confirmRequest)
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError, this::confirm4xxErrorHandler)
.onStatus(HttpStatusCode::is5xxServerError, this::confirm5xxErrorHandler)
.toEntity(TossPaymentConfirmResponse.class);
}
결제 승인을 구현하고 테스트 코드까지 작성하더라도, 실제로 결제가 잘 동작하는지 확인하기 위해선 클라이언트가 필요했다.
다행히 토스페이먼츠에서 제공하는 테스트용 프론트엔드 코드가 존재해서 파라미터만 변경하고 이를 그대로 사용했다.
내 서버에 맞게 코드를 잘 수정한 뒤, 해당 페이지로 접속하면 다음과 같이 결제 위젯 페이지가 로딩된다.
토스페이를 선택한 뒤 결제를 진행하면
결제 성공 응답을 보여주는 페이지가 뜨면서, 토스앱에서 결제 알림이 온다.
결제 승인이 성공하면 Payment
데이터를 DB에 저장하면서 상태 값을 CONFIRMED
설정되도록 구현했는데, 잘 반영되어 저장된 것을 확인할 수 있었다.
결제 구현은 처음이어서, 결제 플로우와 필요한 요청 및 파라미터를 파악하느라 다소 시간이 걸리긴 했었다.
하지만 토스페이먼츠 측에서 매우 친절한 레퍼런스와 함께 테스트 페이지까지 제공하고 있어서 어려움이 줄었던 것 같다.
이제 막 결제 구현을 마쳤으니, 다음은 결제부터 주문까지 이어지는 로직 구현과 결제 취소를 구현해볼 예정이다 :)