다양한 결제 수단을 단일 API로 처리할 수 있도록 도와주는 PortOne(구 IamPort)은 다양한 기업에서 사용되는 결제 연동 솔루션입니다.

V2는 현재 기준으로 정상적으로 서비스 중인 버전으로 V1은 일부 보안 규격이 지원 종료되어, 현재 개발하는 솔루션에서는 V2를 사용하는게 옳다고 판단하였습니다.
전체적인 결제의 흐름은 아래와 같습니다.

SDK를 통해 결제 시 PortOne API는 응답으로 아래와 같은 데이터를 반환해줍니다.

💡 paymentId : 고객사에서 채번한 주문건에 대한 고유 ID. V1기준 merchant_uid와 동일한 의미
💡 transcation Type : 결제/빌링키 발급/본인인증 등을 구분하기 위한 응답값
💡 txId : 포트원에서 채번한 결제건에 대한 고유 ID. V1기준 imp_uid와 동일한 의미
이전 PortOne V1을 통해 결제 시스템을 개발했을 때에는 imp_uid를 기준으로 결제 내역을 조회했지만,
V2기준에서는 paymentId를 기준으로 결제 내역을 조회합니다.

private void validationAmount(BigDecimal portOneAmount, Context context, ReservationType type) {
BigDecimal serverAmount = context.getAmount();
if (serverAmount.compareTo(portOneAmount) != 0) {
log.warn("금액 불일치: [서버 금액 = {}, 결제 금액 = {}, Reservation ID = {}, 예약 타입 = {}]",
serverAmount, portOneAmount, context.getReservationId(), type);
throw new 예외발생(ErrorCode.VALIDATION_FAILED, "금액 불일치. 잘못된 금액입니다.");
}
}
private void validationPaymentStatus(String status, Context context, ReservationType type) {
switch (status) {
case "CANCELLED" -> {
log.warn("취소된 결제 입니다 [Reservation ID = {}, 예약 타입 = {}]", context.getReservationId(), type);
throw new BusinessException(ErrorCode.VALIDATION_FAILED, "취소된 결제입니다.");
}
case "FAILED" -> {
log.warn("실패된 결제 입니다 [Reservation ID = {}, 예약 타입 = {}]", context.getReservationId(), type);
throw new BusinessException(ErrorCode.VALIDATION_FAILED, "실패된 결제입니다.");
}
case "PARTIAL_CANCELLED" -> {
log.warn("부분 취소된 결제 입니다 [Reservation ID = {}, 예약 타입 = {}]", context.getReservationId(), type);
throw new BusinessException(ErrorCode.VALIDATION_FAILED, "결제 부분 취소입니다. 관리자에게 문의해주세요.");
}
case "PAY_PENDING" -> {
log.warn("완료 대기 상태인 결제 입니다 [Reservation ID = {}, 예약 타입 = {}]", context.getReservationId(), type);
throw new BusinessException(ErrorCode.VALIDATION_FAILED, "결제 완료 대기 상태입니다. 관리자에게 문의해주세요.");
}
}
}
PortOne은 결제 수단마다 세부적인 응답 구조가 다 다릅니다.
이러한 구조마다 DTO를 생성하는 것은 불필요하다고 생각이 되어 JsonNode를 통해 파싱하기로 결정했습니다.
예시코드는 아래와 같습니다
JsonNode detail = 포트원에서_결제정보_가져오기(patmentId);
String status = detail.path("status").asText();
String method = detail.path("method").path("type").asText();
결제는 사용자의 금전이 직접 연결된 민감한 도메인입니다.
따라서 결제 데이터는 절대 프론트엔드에서 전달받은 값만을 신뢰해서는 안 되며, 반드시 백엔드에서 직접 검증 과정을 거쳐야 합니다.
또한 PortOne의 callback, redirectUrl은 사용자의 브라우저를 통해 전달되기 때문에, 창 닫기, 새로고침, 네트워크 이슈 등으로 인해 데이터 유실 가능성이 존재합니다.
이러한 문제를 방지하기 위해 PortOne에서도 Webhook 수신 기능 구현을 필수로 권장하고 있습니다.
따라서 본 프로젝트에서는 결제 로직의 유효성 검증과 함께, Webhook 수신을 통해 결제 결과를 안정적으로 동기화하는 구조까지 함께 구현할 계획입니다.