[TIL] PortOne V1 > V2 변경하기

김재진·2026년 3월 12일

내일배움캠프

목록 보기
57/70

문제 상황

실제로 PortOne과 연동하여 테스트 결제를 진행하는 중 결제는 진행되었으나 결제 검증은 실패하는 상황이 발생

원인 분석

v2를 이용해서 결제 테스트를 진행하였으나 백엔드 코드는 v1 버전을 기준으로 작성되었음

v1 vs v2 핵심 차이점

항목v1v2
연동 방식SDK (IamportClient)REST API 직접 호출
결제 고유 IDimp_uidpaymentId (가맹점 지정)
트랜잭션 ID없음txId (PortOne 발급)
인증API Key + SecretPortOne {v2Secret} 헤더
결제 조회iamportClient.paymentByImpUid()GET /payments/{paymentId}
결제 취소iamportClient.cancelPaymentByImpUid()POST /payments/{paymentId}/cancel

v2 결제 흐름에서 ID가 헷갈렸던 부분

프론트 결제창 호출 시:
  PortOne.requestPayment({ paymentId: dbPaymentId })  ← 가맹점이 직접 지정하는 ID

결제 완료 후 응답:
  response.txId  ← PortOne이 발급한 트랜잭션 ID (v1의 imp_uid 역할)

PortOne v2 REST API 조회:
  GET /payments/{paymentId}  ← 여기서 paymentId는 결제창에 넘긴 paymentId (= dbPaymentId)

txId로 /payments/{txId} 조회 → 404 발생
dbPaymentId로 /payments/{dbPaymentId} 조회 → 정상

해결 과정

1. PaymentVerifyRequest 수정 (v2 기준 필드로 변경)

// Before (v1)
private String impUid;      // PortOne 발급 ID
private String merchantUid; // 가맹점 주문 ID

// After (v2, @JsonAlias로 하위호환 유지)
@JsonAlias({"impUid", "imp_uid"})
private String paymentId;   // txId가 매핑됨

@JsonAlias({"merchantUid", "merchant_uid"})
private String dbPaymentId; // 결제창에 넘긴 paymentId

private String txId;        // PortOne 트랜잭션 ID (선택값)

2. PaymentService - 결제 검증을 v2 REST API로 전환

// Before (v1 SDK)
IamportResponse<Payment> response = iamportClient.paymentByImpUid(impUid);

// After (v2 REST API - JDK 내장 HttpClient 사용)
private JsonNode getPortOneV2Payment(String dbPaymentId) throws IOException, InterruptedException {
    String requestUrl = portOneBaseUrl + "/payments/" + dbPaymentId;
    HttpClient client = HttpClient.newHttpClient();
    HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(requestUrl))
            .header("Authorization", "PortOne " + portOneV2Secret)
            .GET()
            .build();
    // ...
}

포인트: API 조회 시 txId가 아니라 dbPaymentId를 사용해야 한다.
PortOne v2 응답의 id 필드 = 결제창에 넘긴 paymentId = dbPaymentId

3. RefundService - 환불도 v2 REST API로 전환

v1 SDK(IamportClient)에 v2 Secret을 넣으면 인증 자체가 실패하기 때문에 환불도 v2로 전환

// Before (v1 SDK)
CancelData cancelData = new CancelData(payment.getPaymentId(), true, payment.getTotalAmount());
IamportResponse<Payment> response = iamportClient.cancelPaymentByImpUid(cancelData);

// After (v2 REST API)
String requestUrl = portOneBaseUrl + "/payments/" + payment.getDbPaymentId() + "/cancel";
String requestBody = objectMapper.writeValueAsString(Map.of("reason", reason));

HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create(requestUrl))
        .header("Authorization", "PortOne " + portOneV2Secret)
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(requestBody))
        .build();

4. application.yml 설정 추가

portone:
  api:
    key: ${PORTONE_API_KEY}
    secret: ${PORTONE_API_SECRET}
    base-url: ${PORTONE_API_BASE_URL:https://api.portone.io}  # 추가
    v2-secret: ${PORTONE_V2_API_SECRET:${PORTONE_API_SECRET}} # 추가

오류 해결 과정 (삽질 기록)

1차 오류: HTTP 404

PortOne v2 결제 조회 실패: HTTP 404

getPortOneV2Payment(request.getPaymentId()) 에서 paymentId = txId였음
getPortOneV2Payment(request.getDbPaymentId()) 로 수정

2차 오류: paymentId 불일치

paymentId 불일치 - 요청: 019ce0fd-5e4d-...(txId), 응답: PAY_597cc0...(dbPaymentId)

→ 검증 코드에서도 request.getPaymentId()(txId)와 응답의 id(dbPaymentId)를 비교하고 있었음
request.getDbPaymentId()와 비교하도록 수정

3차 오류: 환불 인증 실패

인증에 실패하였습니다. API키와 secret을 확인하세요.

→ v1 SDK IamportClient에 v2 Secret이 주입되어 있어서 인증 자체가 불가
→ 환불 로직도 v2 REST API로 전환하여 해결

배운 점

  1. v1과 v2는 연동 방식 자체가 다르다 - v1은 SDK, v2는 REST API 직접 호출. 혼용하면 인증부터 막힌다.

  2. v2에서 ID가 3가지 - paymentId(가맹점 지정), txId(PortOne 발급), cancellationId(취소 ID). 각각 언제 쓰이는지 구분해야 한다.

  3. 외부 라이브러리 없이 REST API 호출 가능 - Java 11+의 java.net.http.HttpClient로 충분히 구현 가능. 간단한 연동에 굳이 외부 HTTP 클라이언트를 추가할 필요 없다.

  4. @JsonAlias로 하위호환 유지 - DTO 필드명이 바뀌어도 @JsonAlias로 구버전 필드명을 같이 받을 수 있다.

  5. Spring Boot 4.x는 Jackson 3.x - com.fasterxml.jackson.databind가 아니라 tools.jackson.databind 패키지를 사용해야 한다.

profile
개발공부 처음해보는 사람

0개의 댓글