프로젝트에 토스페이먼츠 PG 연동하기

5win·2025년 1월 5일
0

1. 주문 ID의 형식 바꾸기

결제를 구현하기 전, 주문을 먼저 구현하면서 주문 ID인 orderId의 타입을 단순히 Long 으로 지정했었다.

그러나 토스페이먼츠 PG 연동을 위해서는 orderId 가 다음 조건을 만족해야 했다.

  1. 6자이상 64자 이하의 문자열
  2. 영문 대소문자, 숫자, 특수문자(-), 특수문자(_)

당연히 단순한 Long 타입은 알맞지 않았기 때문에 orderId 타입을 수정해야 했고, UUID 를 사용하기로 결정했다.


UUID를 사용한 이유는 다음과 같다.

  1. 6자 이상 64자 이하의 문자열이라는 조건에 부합한다.
  2. 영문 대소문자, 숫자, 특수문자(-), 특수문자(_) 를 사용해야 한다는 조건에 부합한다.
  3. 사실상 중복이 발생할 수 없는 안전한 유니크 값이다.
  4. UUID 생성 메서드를 지원하기에 클라이언트나 테스트용으로 발급하기도 용이하다.

다만, 한 가지 걸리는 점이 UUID 는 128비트의 값으로 표현되며, 문자열로 표현하면 하이픈(-) 포함 36자리의 길이를 가진다.

따라서 PK를 길이 36의 문자열(하이픈 제거하면 32자리)로 사용하면 성능적인 문제가 어느정도 있을 것이라 생각을 했다.

  • 정수에 비해, 단순 비교하는 연산의 비용이 큰 문제
  • 인덱스를 구성한다면, 노드의 레코드 하나당 32 bytes나 공간을 차지하는 문제

물론 이런 부분까지 고려할 정도의 성능을 요구하진 않겠지만, 그래도 고민해보는 것과 아닌 것에는 큰 차이가 있다고 생각한다.

아무튼 하이픈(-)은 제외하고 CHAR(32) 타입으로 저장한다면, 8 bits * 32 = 256 bits 의 크기를 차지하는데, 위에서 말했듯이 UUID 는 128 bits의 크기를 가진다.

즉, 128 bits의 공간이 낭비되고 있기 때문에 BINARY(16) 타입을 사용하여 공간을 절약할 수 있다.

이마저도 오버헤드가 크다고 생각된다면 64 bits 크기를 갖는 Snowflake ID 를 사용할 수 있지만 현재로서는 UUID 로도 충분하다.

2. 결제 로직 플로우

이전 포스트에서 설명했던 것 처럼 토스페이먼츠의 결제 플로우는 다음과 같다.

  1. 클라이언트에서 successUrl 과 함께 결제 요청
  2. 결제 정보와 함께 successUrl 로 리다이렉션
  3. 클라이언트의 결제 금액과 토스 서버로부터 받은 금액 비교하여 검증
  4. 결제 승인 API 호출 및 승인 완료
  5. Payment 객체 응답

이 사이에 내가 구현한 스프링 서버의 역할을 포함시켜야 하고, 그렇게 수정된 플로우는 다음과 같다.

  1. 클라이언트에서 successUrl 과 함께 결제 요청
  2. 결제 정보와 함께 successUrl 로 리다이렉션
  3. 클라이언트의 결제 금액과 토스 서버로부터 받은 금액 비교하여 검증
  4. 스프링 서버 결제 승인 API에 요청
  5. 스프링 서버에서 토스페이먼츠 결제 승인 API 호출 및 승인 완료
  6. DB에 Payment 객체 정보 저장

스프링 서버에서 대신 토스페이먼츠로 결제 승인 요청을 보내고, 받은 데이터를 DB에 저장하는 방식이다.

플로우는 정해졌으니 이제 구현을 해보자.


3. 결제 승인 API 구현

결제 승인 요청을 보내기 위한 비즈니스 로직을 구현했고, 역할은 다음과 같다.

  1. 클라이언트로부터 결제 정보 수신
  2. 토스페이먼츠 결제 승인 API 호출
  3. 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);
}

4. 테스트를 위한 결제 페이지 구현

결제 승인을 구현하고 테스트 코드까지 작성하더라도, 실제로 결제가 잘 동작하는지 확인하기 위해선 클라이언트가 필요했다.

다행히 토스페이먼츠에서 제공하는 테스트용 프론트엔드 코드가 존재해서 파라미터만 변경하고 이를 그대로 사용했다.

내 서버에 맞게 코드를 잘 수정한 뒤, 해당 페이지로 접속하면 다음과 같이 결제 위젯 페이지가 로딩된다.

토스페이를 선택한 뒤 결제를 진행하면

결제 성공 응답을 보여주는 페이지가 뜨면서, 토스앱에서 결제 알림이 온다.


결제 승인이 성공하면 Payment 데이터를 DB에 저장하면서 상태 값을 CONFIRMED 설정되도록 구현했는데, 잘 반영되어 저장된 것을 확인할 수 있었다.


결제 구현은 처음이어서, 결제 플로우와 필요한 요청 및 파라미터를 파악하느라 다소 시간이 걸리긴 했었다.

하지만 토스페이먼츠 측에서 매우 친절한 레퍼런스와 함께 테스트 페이지까지 제공하고 있어서 어려움이 줄었던 것 같다.

이제 막 결제 구현을 마쳤으니, 다음은 결제부터 주문까지 이어지는 로직 구현과 결제 취소를 구현해볼 예정이다 :)

0개의 댓글