실제로 PortOne과 연동하여 테스트 결제를 진행하는 중 결제는 진행되었으나 결제 검증은 실패하는 상황이 발생
v2를 이용해서 결제 테스트를 진행하였으나 백엔드 코드는 v1 버전을 기준으로 작성되었음
| 항목 | v1 | v2 |
|---|---|---|
| 연동 방식 | SDK (IamportClient) | REST API 직접 호출 |
| 결제 고유 ID | imp_uid | paymentId (가맹점 지정) |
| 트랜잭션 ID | 없음 | txId (PortOne 발급) |
| 인증 | API Key + Secret | PortOne {v2Secret} 헤더 |
| 결제 조회 | iamportClient.paymentByImpUid() | GET /payments/{paymentId} |
| 결제 취소 | iamportClient.cancelPaymentByImpUid() | POST /payments/{paymentId}/cancel |
프론트 결제창 호출 시:
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} 조회 → 정상
// 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 (선택값)
// 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
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();
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}} # 추가
PortOne v2 결제 조회 실패: HTTP 404
→ getPortOneV2Payment(request.getPaymentId()) 에서 paymentId = txId였음
→ getPortOneV2Payment(request.getDbPaymentId()) 로 수정
paymentId 불일치 - 요청: 019ce0fd-5e4d-...(txId), 응답: PAY_597cc0...(dbPaymentId)
→ 검증 코드에서도 request.getPaymentId()(txId)와 응답의 id(dbPaymentId)를 비교하고 있었음
→ request.getDbPaymentId()와 비교하도록 수정
인증에 실패하였습니다. API키와 secret을 확인하세요.
→ v1 SDK IamportClient에 v2 Secret이 주입되어 있어서 인증 자체가 불가
→ 환불 로직도 v2 REST API로 전환하여 해결
v1과 v2는 연동 방식 자체가 다르다 - v1은 SDK, v2는 REST API 직접 호출. 혼용하면 인증부터 막힌다.
v2에서 ID가 3가지 - paymentId(가맹점 지정), txId(PortOne 발급), cancellationId(취소 ID). 각각 언제 쓰이는지 구분해야 한다.
외부 라이브러리 없이 REST API 호출 가능 - Java 11+의 java.net.http.HttpClient로 충분히 구현 가능. 간단한 연동에 굳이 외부 HTTP 클라이언트를 추가할 필요 없다.
@JsonAlias로 하위호환 유지 - DTO 필드명이 바뀌어도 @JsonAlias로 구버전 필드명을 같이 받을 수 있다.
Spring Boot 4.x는 Jackson 3.x - com.fasterxml.jackson.databind가 아니라 tools.jackson.databind 패키지를 사용해야 한다.