목차
-
SpringBoot에서 사업자 번호 없이 프로젝트 내에서 토스, 카카오페이 결제를 만들어보자.
-
이 글에서는 테스트 결제 부분에 대해서 설명합니다.
-
Front-end(React) 구성
-
Back-end(SpringBoot)
-
SpringBoot 패키지 내 build.gradle에 해당 내용 추가
-
PaymentController
-
PaymentDto
-
PaymentEntity
-
PaymentRepository
-
PaymentService
-
결제 완료
-
결제 검증 실패
-
DB에 반영이 되었을 때
-
프론트앤드(React)에서 결제 완료 후 백앤드로 결제 검증 후 DB 저장
-
주의
-
Docs
SpringBoot에서 사업자 번호 없이 프로젝트 내에서 토스, 카카오페이 결제를 만들어보자.
💡 포트원(PortOne) 사용 이유 : 팀 프로젝트 과정에서 결제 서비스를 추가 할 상황이 있었는데 카카오페이 테스트결제를 넣어도 되지만, 클라이언트 단에서 실제로 결제를 하는 것 같은 효과를 내고 싶었고, 카카오 페이 테스트 결제에서는 Back-end단에서 결제가 정확히 완료 되었는지 취소 되었는지 불투명하여서 결제 관련 API를 찾다가 포트원 API를 찾음.
포트원 지원 결제수단 목록
card
(신용카드)trans
(실시간계좌이체)vbank
(가상계좌)phone
(휴대폰소액결제)paypal
(페이팔 SPB 일반결제)applepay
(애플페이)naverpay
(네이버페이)samsungpay
(삼성페이)kpay
(KPay앱 )kakaopay
(카카오페이)payco
(페이코)lpay
(LPAY)ssgpay
(SSG페이)tosspay
(토스간편결제)cultureland
(컬쳐랜드)smartculture
(스마트문상)culturegift
(문화상품권)happymoney
(해피머니)booknlife
(도서문화상품권)point
(베네피아 포인트 등 포인트 결제 )wechat
(위쳇페이)alipay
(알리페이)unionpay
(유니온페이)tenpay
(텐페이)pinpay
(핀페이)ssgpay_bank
(SSG 은행계좌)skpay
(11Pay (구.SKPay))naverpay_card
(네이버페이 - 카드)naverpay_point
(네이버페이 - 포인트)paypal
(페이팔)toss_brandpay
(토스페이먼츠 브랜드페이)
- 포트원 API 장점 : Back-end(Spring)에서 하나의 구현으로 여러 결제수단을 동시에 사용 가능
이 글에서는 테스트 결제 부분에 대해서 설명합니다.
- 실 결제는 사업자 번호와 가맹점 신청이 있어야 가능합니다.
- 우선 포트원(PortOne)에 회원가입
https://portone.io/
- 결제 수단 추가 및 API Key 발급
https://admin.portone.io/integration
- “결제 연동” 메뉴로 이동
- “내 식별코드 - API Keys” 클릭
API Key가 없으면 “발급”
- 결제 수단 추가
- 필자는 “카카오페이” 추가
Front-end(React) 구성
- React상에서 구현
public - index.html의태그에 포트원 SDK 추가
<script src="https://cdn.iamport.kr/v1/iamport.js"></script>
- Payment.js
import React, {useState} from 'react';
function App() {
const [amount, setAmount] = useState(0);
const handleChange = (e) => {
setAmount(e.target.value);
}
const ClickChargeBtn = (pg_method, amount, nickname, redirect_url) => {
const { IMP } = window;
IMP.init('imp.........'); // 가맹점 번호 지정
IMP.request_pay({
pg : `${pg_method}`, // 결제 방식 지정
pay_method : 'card',
merchant_uid: `mid_${new Date().getTime()}`, // 현재 시간
name : '결제 품목 및 제목 지정',
amount : `${amount}`, // 충전할 금액
buyer_email : '구매자 이메일',
buyer_name : `${nickname}`, // 충전 요청한 유저의 닉네임
buyer_tel : '010-1222-2222',
buyer_addr : '서울특별시 강남구 삼성동',
buyer_postcode : '123-456',
m_redirect_url: `${redirect_url}` // 만약 새창에서 열린다면 결제 완료 후 리다이렉션할 주소
}, function (rsp) { // callback
if (rsp.success) { // 만약 결제가 성공적으로 이루어졌다면
alert("결제 성공");
} else {
alert("결제 실패");
}
}
);
}
return (
<div className="App">
<h1>Test</h1>
<p>금액<input type="number" className='amount' onChange={handleChange}></input></p>
<button onClick={() => ClickChargeBtn('kakaopay', amount, 'nickname', 'http://localhost:3000/redirect')}>카카오페이</button>
<button onClick={() => ClickChargeBtn('tosspay', amount, 'nickname', 'http://localhost:3000/redirect')}>토스페이</button>
</div>
);
}
export default App;
- 결제에 성공을 하면 해당 JSON을 반환해준다.
- 여기에서 백엔드를 활용해 이 결제에 대하여 정확히 잘 결제가 되었는지, 충전 금액에 대한 변조는 없는지에 대하여 2차로 검증을 하고, DB에 저장을 하는 과정을 작성해보겠습니다.
Back-end(SpringBoot)
SpringBoot 패키지 내 build.gradle에 해당 내용 추가
dependencies {
implementation 'com.github.iamport:iamport-rest-client-java:0.2.21'
}
repositories {
allprojects {
repositories {
maven { url 'https://jitpack.io' }
}
}
}
PaymentController
package com.test.springtest.payment.controller;
import com.siot.IamportRestClient.exception.IamportResponseException;
import com.test.springtest.payment.dto.PaymentDto;
import com.test.springtest.payment.service.PaymentService;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.io.IOException;
@Controller
public class PaymentController {
private final PaymentService paymentService;
public PaymentController(PaymentService paymentService) {
this.paymentService = paymentService;
}
@ResponseBody
@RequestMapping("/verify/{imp_uid}") // https://URL/verify/{거래고유번호}
public PaymentDto paymentByImpUid(@PathVariable("imp_uid") String imp_uid)
throws IamportResponseException, IOException {
return paymentService.verifyPayment(imp_uid); // 결제 검증 및 DB 값 삽입
}
}
PaymentDto
package com.test.springtest.payment.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
@AllArgsConstructor
@Getter
@Builder
@Setter
public class PaymentDto {
private String impuid; // 거래 고유 번호
private String name; // 상품명
private String status; // 결제여부 paid : 1 , 그 외 실패
private Long amount; // 결제금액
}
PaymentEntity
package com.test.springtest.payment.entity;
import com.test.springtest.payment.dto.PaymentDto;
import jakarta.persistence.*;
import lombok.*;
@Getter
@Setter
@Entity
@Table(name="payment_list", indexes={@Index(name = "impuid_index", columnList = "impuid", unique = true)}) // impuid 인덱스 생성
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PaymentEntity {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long payment_id;
// DB 내부 결제 고유 번호
@Column(name="impuid")
private String impuid;
// 결제 고유 번호
@Column(length = 255)
private String name;
// 상품명
@Column
private String status;
// 결제 여부
@Column
private Long amount;
// 금액
// Dto -> Entity 변환
public PaymentEntity(PaymentDto dto) {
this.impuid = dto.getImpuid();
this.amount = dto.getAmount();
this.name = dto.getName();
this.status = dto.getStatus();
}
}
PaymentRepository
package com.test.springtest.payment.repository;
import com.test.springtest.payment.entity.PaymentEntity;
import org.springframework.data.jpa.repository.JpaRepository;
public interface PaymentRepository extends JpaRepository<PaymentEntity, String> {
public long countByImpuidContainsIgnoreCase(String impuid); // 결제 고유 번호 중복 확인
}
PaymentService
package com.test.springtest.payment.service;
import com.siot.IamportRestClient.IamportClient;
import com.siot.IamportRestClient.exception.IamportResponseException;
import com.siot.IamportRestClient.response.IamportResponse;
import com.siot.IamportRestClient.response.Payment;
import com.test.springtest.payment.dto.PaymentDto;
import com.test.springtest.payment.entity.PaymentEntity;
import com.test.springtest.payment.repository.PaymentRepository;
import org.springframework.stereotype.Service;
import java.io.IOException;
@Service
public class PaymentService {
private final IamportClient iamportClient;
private final PaymentRepository paymentRepository;
public PaymentService(PaymentRepository paymentRepository) {
// 포트원 토큰 발급을 위해 API 키 입력
this.iamportClient = new IamportClient("REST_API_KEY",
"REST_API_SECRET");
this.paymentRepository = paymentRepository;
}
public PaymentDto verifyPayment(String imp_uid) throws IamportResponseException, IOException {
IamportResponse<Payment> iamportResponse = iamportClient.paymentByImpUid(imp_uid); // 결제 검증 시작
Long amount = (iamportResponse.getResponse().getAmount()).longValue(); // 결제 금액
String name = iamportResponse.getResponse().getName(); // 상품명
String status = iamportResponse.getResponse().getStatus(); // paid 이면 1
PaymentDto paymentDto = PaymentDto.builder() // Dto 변환
.impuid(imp_uid)
.amount(amount)
.status(status)
.name(name)
.build();
if (paymentRepository.countByImpuidContainsIgnoreCase(imp_uid) == 0) { // 중복하는 값이 없으면
if (iamportResponse.getResponse().getStatus().equals("paid")) { // 결제가 정상적으로 이루어졌으면
PaymentEntity paymentEntity = new PaymentEntity(paymentDto); // Dto -> Entity 변환
paymentRepository.save(paymentEntity); // 변환된 Entity DB 저장
return paymentDto; // 클라이언트에게 Dto 값 JSON 형태 반환
} else {
paymentDto.setStatus("결제 오류입니다. 다시 시도해주세요."); // 클라이언트에게 Status 값 오류 코드 보냄
return paymentDto;
}
} else {
paymentDto.setStatus("이미 결제 되었습니다."); // 클라이언트에게 Status 값 오류 코드 보냄
return paymentDto;
}
}
}
결제 완료
결제 검증 실패
DB에 반영이 되었을 때
프론트앤드(React)에서 결제 완료 후 백앤드로 결제 검증 후 DB 저장
import React, {useState} from 'react';
import axios from 'axios';
function App() {
const [amount, setAmount] = useState(0);
const handleChange = (e) => {
setAmount(e.target.value);
}
const ClickChargeBtn = (pg_method, amount, nickname, redirect_url) => {
const { IMP } = window;
IMP.init('imp가맹점번호'); // 가맹점 번호 지정
IMP.request_pay({
pg : `${pg_method}`, // 결제 방식 지정
pay_method : 'card',
merchant_uid: `mid_${new Date().getTime()}`, // 현재 시간
name : '포인트 충전',
amount : `${amount}`, // 충전할 금액
buyer_email : 'Iamport@chai.finance',
buyer_name : `${nickname}`, // 충전 요청한 유저의 닉네임
buyer_tel : '010-1234-5678',
buyer_addr : '서울특별시 강남구 삼성동',
buyer_postcode : '123-456',
m_redirect_url: `${redirect_url}` // 만약 새창에서 열린다면 결제 완료 후 리다이렉션할 주소
}, function (rsp) { // callback
if (rsp.success) { // 만약 결제가 성공적으로 이루어졌다면
axios.get(`http://localhost:8080/verify/` + rsp.imp_uid)
.then((response) => {
console.log(response);
})
.catch((error) => {
console.error(error);
});
alert("결제 성공");
console.log(rsp);
} else {
alert("결제 실패");
console.log(rsp);
}
}
);
}
return (
<div className="App">
<h1>Test</h1>
<p>금액<input type="number" className='amount' onChange={handleChange}></input></p>
<button onClick={() => ClickChargeBtn('kakaopay', amount, 'nickname', 'http://localhost:3000/redirect')}>카카오페이</button>
<button onClick={() => ClickChargeBtn('tosspay', amount, 'nickname', 'http://localhost:3000/redirect')}>토스페이</button>
</div>
);
}
export default App;
주의
- CORS 문제 발생할 수도 있습니다.
- 예제 및 테스트 이기에 Spring, React 코드 예외처리 미흡
Docs
SpringBoot에서 사업자 번호 없이 프로젝트 내에서 토스, 카카오페이 결제를 만들어보자.
💡 포트원(PortOne) 사용 이유 : 팀 프로젝트 과정에서 결제 서비스를 추가 할 상황이 있었는데 카카오페이 테스트결제를 넣어도 되지만, 클라이언트 단에서 실제로 결제를 하는 것 같은 효과를 내고 싶었고, 카카오 페이 테스트 결제에서는 Back-end단에서 결제가 정확히 완료 되었는지 취소 되었는지 불투명하여서 결제 관련 API를 찾다가 포트원 API를 찾음.
포트원 지원 결제수단 목록
card
(신용카드)trans
(실시간계좌이체)vbank
(가상계좌)phone
(휴대폰소액결제)paypal
(페이팔 SPB 일반결제)applepay
(애플페이)naverpay
(네이버페이)samsungpay
(삼성페이)kpay
(KPay앱 )kakaopay
(카카오페이)payco
(페이코)lpay
(LPAY)ssgpay
(SSG페이)tosspay
(토스간편결제)cultureland
(컬쳐랜드)smartculture
(스마트문상)culturegift
(문화상품권)happymoney
(해피머니)booknlife
(도서문화상품권)point
(베네피아 포인트 등 포인트 결제 )wechat
(위쳇페이)alipay
(알리페이)unionpay
(유니온페이)tenpay
(텐페이)pinpay
(핀페이)ssgpay_bank
(SSG 은행계좌)skpay
(11Pay (구.SKPay))naverpay_card
(네이버페이 - 카드)naverpay_point
(네이버페이 - 포인트)paypal
(페이팔)toss_brandpay
(토스페이먼츠 브랜드페이)
- 포트원 API 장점 : Back-end(Spring)에서 하나의 구현으로 여러 결제수단을 동시에 사용 가능
이 글에서는 테스트 결제 부분에 대해서 설명합니다.
- 실 결제는 사업자 번호와 가맹점 신청이 있어야 가능합니다.
- 우선 포트원(PortOne)에 회원가입
https://portone.io/
- 결제 수단 추가 및 API Key 발급
https://admin.portone.io/integration
- “결제 연동” 메뉴로 이동
- “내 식별코드 - API Keys” 클릭
API Key가 없으면 “발급”
- 결제 수단 추가
- 필자는 “카카오페이” 추가
Front-end(React) 구성
- React상에서 구현
public - index.html의태그에 포트원 SDK 추가
<script src="https://cdn.iamport.kr/v1/iamport.js"></script>
- Payment.js
import React, {useState} from 'react';
function App() {
const [amount, setAmount] = useState(0);
const handleChange = (e) => {
setAmount(e.target.value);
}
const ClickChargeBtn = (pg_method, amount, nickname, redirect_url) => {
const { IMP } = window;
IMP.init('imp.........'); // 가맹점 번호 지정
IMP.request_pay({
pg : `${pg_method}`, // 결제 방식 지정
pay_method : 'card',
merchant_uid: `mid_${new Date().getTime()}`, // 현재 시간
name : '결제 품목 및 제목 지정',
amount : `${amount}`, // 충전할 금액
buyer_email : '구매자 이메일',
buyer_name : `${nickname}`, // 충전 요청한 유저의 닉네임
buyer_tel : '010-1222-2222',
buyer_addr : '서울특별시 강남구 삼성동',
buyer_postcode : '123-456',
m_redirect_url: `${redirect_url}` // 만약 새창에서 열린다면 결제 완료 후 리다이렉션할 주소
}, function (rsp) { // callback
if (rsp.success) { // 만약 결제가 성공적으로 이루어졌다면
alert("결제 성공");
} else {
alert("결제 실패");
}
}
);
}
return (
<div className="App">
<h1>Test</h1>
<p>금액<input type="number" className='amount' onChange={handleChange}></input></p>
<button onClick={() => ClickChargeBtn('kakaopay', amount, 'nickname', 'http://localhost:3000/redirect')}>카카오페이</button>
<button onClick={() => ClickChargeBtn('tosspay', amount, 'nickname', 'http://localhost:3000/redirect')}>토스페이</button>
</div>
);
}
export default App;
- 결제에 성공을 하면 해당 JSON을 반환해준다.
- 여기에서 백엔드를 활용해 이 결제에 대하여 정확히 잘 결제가 되었는지, 충전 금액에 대한 변조는 없는지에 대하여 2차로 검증을 하고, DB에 저장을 하는 과정을 작성해보겠습니다.
Back-end(SpringBoot)
SpringBoot 패키지 내 build.gradle에 해당 내용 추가
dependencies {
implementation 'com.github.iamport:iamport-rest-client-java:0.2.21'
}
repositories {
allprojects {
repositories {
maven { url 'https://jitpack.io' }
}
}
}
PaymentController
package com.test.springtest.payment.controller;
import com.siot.IamportRestClient.exception.IamportResponseException;
import com.test.springtest.payment.dto.PaymentDto;
import com.test.springtest.payment.service.PaymentService;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.io.IOException;
@Controller
public class PaymentController {
private final PaymentService paymentService;
public PaymentController(PaymentService paymentService) {
this.paymentService = paymentService;
}
@ResponseBody
@RequestMapping("/verify/{imp_uid}") // https://URL/verify/{거래고유번호}
public PaymentDto paymentByImpUid(@PathVariable("imp_uid") String imp_uid)
throws IamportResponseException, IOException {
return paymentService.verifyPayment(imp_uid); // 결제 검증 및 DB 값 삽입
}
}
PaymentDto
package com.test.springtest.payment.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
@AllArgsConstructor
@Getter
@Builder
@Setter
public class PaymentDto {
private String impuid; // 거래 고유 번호
private String name; // 상품명
private String status; // 결제여부 paid : 1 , 그 외 실패
private Long amount; // 결제금액
}
PaymentEntity
package com.test.springtest.payment.entity;
import com.test.springtest.payment.dto.PaymentDto;
import jakarta.persistence.*;
import lombok.*;
@Getter
@Setter
@Entity
@Table(name="payment_list", indexes={@Index(name = "impuid_index", columnList = "impuid", unique = true)}) // impuid 인덱스 생성
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PaymentEntity {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long payment_id;
// DB 내부 결제 고유 번호
@Column(name="impuid")
private String impuid;
// 결제 고유 번호
@Column(length = 255)
private String name;
// 상품명
@Column
private String status;
// 결제 여부
@Column
private Long amount;
// 금액
// Dto -> Entity 변환
public PaymentEntity(PaymentDto dto) {
this.impuid = dto.getImpuid();
this.amount = dto.getAmount();
this.name = dto.getName();
this.status = dto.getStatus();
}
}
PaymentRepository
package com.test.springtest.payment.repository;
import com.test.springtest.payment.entity.PaymentEntity;
import org.springframework.data.jpa.repository.JpaRepository;
public interface PaymentRepository extends JpaRepository<PaymentEntity, String> {
public long countByImpuidContainsIgnoreCase(String impuid); // 결제 고유 번호 중복 확인
}
PaymentService
package com.test.springtest.payment.service;
import com.siot.IamportRestClient.IamportClient;
import com.siot.IamportRestClient.exception.IamportResponseException;
import com.siot.IamportRestClient.response.IamportResponse;
import com.siot.IamportRestClient.response.Payment;
import com.test.springtest.payment.dto.PaymentDto;
import com.test.springtest.payment.entity.PaymentEntity;
import com.test.springtest.payment.repository.PaymentRepository;
import org.springframework.stereotype.Service;
import java.io.IOException;
@Service
public class PaymentService {
private final IamportClient iamportClient;
private final PaymentRepository paymentRepository;
public PaymentService(PaymentRepository paymentRepository) {
// 포트원 토큰 발급을 위해 API 키 입력
this.iamportClient = new IamportClient("REST_API_KEY",
"REST_API_SECRET");
this.paymentRepository = paymentRepository;
}
public PaymentDto verifyPayment(String imp_uid) throws IamportResponseException, IOException {
IamportResponse<Payment> iamportResponse = iamportClient.paymentByImpUid(imp_uid); // 결제 검증 시작
Long amount = (iamportResponse.getResponse().getAmount()).longValue(); // 결제 금액
String name = iamportResponse.getResponse().getName(); // 상품명
String status = iamportResponse.getResponse().getStatus(); // paid 이면 1
PaymentDto paymentDto = PaymentDto.builder() // Dto 변환
.impuid(imp_uid)
.amount(amount)
.status(status)
.name(name)
.build();
if (paymentRepository.countByImpuidContainsIgnoreCase(imp_uid) == 0) { // 중복하는 값이 없으면
if (iamportResponse.getResponse().getStatus().equals("paid")) { // 결제가 정상적으로 이루어졌으면
PaymentEntity paymentEntity = new PaymentEntity(paymentDto); // Dto -> Entity 변환
paymentRepository.save(paymentEntity); // 변환된 Entity DB 저장
return paymentDto; // 클라이언트에게 Dto 값 JSON 형태 반환
} else {
paymentDto.setStatus("결제 오류입니다. 다시 시도해주세요."); // 클라이언트에게 Status 값 오류 코드 보냄
return paymentDto;
}
} else {
paymentDto.setStatus("이미 결제 되었습니다."); // 클라이언트에게 Status 값 오류 코드 보냄
return paymentDto;
}
}
}
결제 완료
결제 검증 실패
DB에 반영이 되었을 때
프론트앤드(React)에서 결제 완료 후 백앤드로 결제 검증 후 DB 저장
import React, {useState} from 'react';
import axios from 'axios';
function App() {
const [amount, setAmount] = useState(0);
const handleChange = (e) => {
setAmount(e.target.value);
}
const ClickChargeBtn = (pg_method, amount, nickname, redirect_url) => {
const { IMP } = window;
IMP.init('imp가맹점번호'); // 가맹점 번호 지정
IMP.request_pay({
pg : `${pg_method}`, // 결제 방식 지정
pay_method : 'card',
merchant_uid: `mid_${new Date().getTime()}`, // 현재 시간
name : '포인트 충전',
amount : `${amount}`, // 충전할 금액
buyer_email : 'Iamport@chai.finance',
buyer_name : `${nickname}`, // 충전 요청한 유저의 닉네임
buyer_tel : '010-1234-5678',
buyer_addr : '서울특별시 강남구 삼성동',
buyer_postcode : '123-456',
m_redirect_url: `${redirect_url}` // 만약 새창에서 열린다면 결제 완료 후 리다이렉션할 주소
}, function (rsp) { // callback
if (rsp.success) { // 만약 결제가 성공적으로 이루어졌다면
axios.get(`http://localhost:8080/verify/` + rsp.imp_uid)
.then((response) => {
console.log(response);
})
.catch((error) => {
console.error(error);
});
alert("결제 성공");
console.log(rsp);
} else {
alert("결제 실패");
console.log(rsp);
}
}
);
}
return (
<div className="App">
<h1>Test</h1>
<p>금액<input type="number" className='amount' onChange={handleChange}></input></p>
<button onClick={() => ClickChargeBtn('kakaopay', amount, 'nickname', 'http://localhost:3000/redirect')}>카카오페이</button>
<button onClick={() => ClickChargeBtn('tosspay', amount, 'nickname', 'http://localhost:3000/redirect')}>토스페이</button>
</div>
);
}
export default App;
주의
- CORS 문제 발생할 수도 있습니다.
- 예제 및 테스트 이기에 Spring, React 코드 예외처리 미흡