스프링 MVC 패턴의 쇼핑몰 프로젝트에 적용한 예시이다. (자바 버전: 11, 빌드 툴: Maven, 프레임워크: 스프링 부트)
* 공식문서를 참고하면 비교적 쉽게 구현할 수 있다.
https://docs.iamport.kr/
https://github.com/iamport/iamport-rest-client-java
https://guide.iamport.kr/d5e9a573-c083-4c0e-bec4-edd894c520b7
아임포트 회원가입 후에 시작 https://admin.iamport.kr/auth/signin
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>com.github.iamport</groupId>
<artifactId>iamport-rest-client-java</artifactId>
<version>0.2.22</version>
</dependency>
</dependencies>
pom.xml에 JitPack 리파지토리와 아임포트 의존성을 추가해준다.
<!-- jQuery -->
<script type="text/javascript" src="https://code.jquery.com/jquery-1.12.4.min.js" ></script>
<!-- iamport.payment.js -->
<script type="text/javascript" src="https://cdn.iamport.kr/js/iamport.payment-{SDK-최신버전}.js"></script>
결제버튼이 있는 페이지에 위 script를 추가한다. 글을 쓰는 시점에서 1.1.8을 사용했다. SDK 최신버전확인
function proceedPay(){
$.ajax({
url : '/payment/proceed',
type : 'POST',
async : true,
dataType : "Json",
data :
$('#orderForm').serialize(),
success : function(data){
if(data.cnt > 0){
requestPay(data)
}else{
alert(data.msg)
}
},
error : function (e){
alert("에러")
}
});
}
결제버튼에 라는 js함수를 연결해주었다. 아임포트에 결제요청을 할 때 필요한 주문고유번호를 DB에서 꺼내오기를 권하고 있기 때문에, 이 함수는 결제요청을 하기 전 DB에 주문고유번호를 저장하는 역할을 한다.(내 경우 MySQL의 orders라는 테이블에 auto increment, primary key로 지정한 order_no이 주문고유번호 역할을 한다.)
사용자가 #orderForm 안에 주문자 정보를 적은 후 결제버튼을 눌렀기 때문에 해당 내용을 .serialize로 가져와서 컨트롤러로 넘기게 된다. url은 임의로 'payment/proceed'로 지정했다.
@PostMapping("/payment/proceed")
@ResponseBody
public Map<Object, Object> paymentOk(HttpServletRequest req, @ModelAttribute OrderDTO odto) {
HttpSession session = req.getSession();
User user = (User) session.getAttribute("userDetail");
MemberDTO mdto = memberMapper.getMemberById(user.getUsername());
List<ProductDTO> plist = (List<ProductDTO>) session.getAttribute("cart");
int total_cost = 0;
int total_price = 0;
int total_income = 0;
String order_products = "";
// 카트 안 상품목록의 원가, 판매가, 이익을 더한다
for(ProductDTO pdto : plist) {
total_cost += pdto.getProd_cost();
total_price += pdto.getProd_price();
total_income += pdto.getProd_income();
order_products += pdto.getProd_name() + ",";
}
// orderDTO 내용을 완성
odto.setMem_no(mdto.getMem_no());
odto.setOrder_cost(total_cost);
odto.setOrder_price(total_price);
odto.setOrder_income(total_income);
odto.setOrder_products(order_products);
// orderDTO insert
int res = orderMapper.insertOrder(odto);
Map<Object, Object> map = new HashMap<>();
if (res > 0) {
map.put("cnt", 1);
// orderDTO의 고유 no값 가져오기
OrderDTO odto2 = orderMapper.getOrderLast(mdto.getMem_no());
int order_no = odto2.getOrder_no();
// oderDTO 내용을 ajax로 넘기기
map.put("no", order_no);
map.put("products", odto2.getOrder_products());
map.put("name", odto2.getOrder_name());
map.put("price", odto2.getOrder_price());
map.put("addr", odto2.getOrder_addr());
//session.removeAttribute("cart");
} else {
map.put("cnt", 0);
map.put("msg", "주문을 실패했습니다. 다시 시도해주세요.");
}
return map;
}
OrderDTO 안에는 사용자가 결제버튼을 누르기 전에 입력한 주문자명, 주소 외에도 구매한 상품의 원가, 판매가, 이익, 상품명 등이 저장된다.
따라서 우선 @ModelAttribute로 serialize된 폼의 내용을 받아와 주문내역 dto를 생성하고, 세션에 저장된 카트 리스트를 불러와 for문을 돌며 카트 안 상품목록의 원가, 판매가, 이익을 더한다.
이렇게 orderDTO를 완성한 뒤 orders 테이블에 insert해준다. insert가 잘 되었다면 해당 orderDTO의 내용을 map에 담아 return해준다.
ajax로 돌아와서, map을 인자로 requestPay라는 함수를 호출한다.
우선 아임포트 관리자 콘솔에서 결제 연동 > 내 식별코드 / API Keys 메뉴에서 가맹점 식별코드를 복사해온다.
그 다음 테스트/실 연동 메뉴에서 결제대행사를 추가해준다.
function requestPay(data) {
var IMP = window.IMP; // 생략 가능
IMP.init("가맹점 코드"); // 예: imp00000000
//IMP.request_pay(param, callback) 결제창 호출
IMP.request_pay({ // param
pg: "html5_inicis.INIpayTest", //결제대행사 설정에 따라 다르며 공식문서 참고
pay_method: "card", //결제방법 설정에 따라 다르며 공식문서 참고
merchant_uid: data.no, //주문(db에서 불러옴) 고유번호
name: data.products,
amount: data.price,
buyer_email: "",
buyer_name: data.name,
//buyer_tel: "010-4242-4242",
buyer_addr: data.addr,
//buyer_postcode: "01181"
}, function (rsp) { // callback
if (rsp.success) {
// 결제 성공 시: 결제 승인 또는 가상계좌 발급에 성공한 경우
// jQuery로 HTTP 요청
jQuery.ajax({
url: "/payment/verify/"+ rsp.imp_uid,
method: "POST",
}).done(function (data) {
// 위의 rsp.paid_amount 와 data.response.amount를 비교한후 로직 실행 (iamport 서버검증)
if(rsp.paid_amount == data.response.amount){
succeedPay(rsp.imp_uid, rsp.merchant_uid);
} else {
alert("결제 검증 실패");
}
})
} else {
var msg = '결제에 실패하였습니다.';
msg += '에러내용 : ' + rsp.error_msg;
alert(msg);
}
});
}
컨트롤러에서 넘긴 map을 인자로 받는 requestPay()함수를 추가해준다. 여기서는 IMP 객체를 만들고 IMP.request_pay를 호출하여 결제요청을 한다. 각 변수에 data에서 넘어온 map의 키값을 지정해준다. 테스트용으로 간단하게 이름과 주소만 받았으므로 tel이나 postcode는 컨트롤러에서 넘어오지 않아 생략한다. email은 필수로, 주석처리하면 에러가 나기 때문에 ""로 지정해준다.
참고로 여기서 공식문서에 적힌대로 headers: { "Content-Type": "application/json" }, 를 지정해주면 에러가 나서 headers를 아예 지워주었다.
결제 성공시 로직은 ajax로 처리한다. url은 아임포트에서 제공하는 API로 imp_uid는 결제response로 보내준 주문고유번호다.
@Controller
public class HomeController {
// Iamport
private IamportClient iamportClient;
private IamportAPI iamportApi;
public HomeController(IamportAPI api) {
this.iamportApi = api;
String IAMPORT_API = api.getApi();
String IAMPORT_API_SECRET = api.getApiSecret();
this.iamportClient = new IamportClient(IAMPORT_API,IAMPORT_API_SECRET);
}
@PostMapping("/payment/verify/{imp_uid}")
@ResponseBody
public IamportResponse<Payment> paymentByImpUid(Model model, Locale locale, HttpSession session
, @PathVariable(value= "imp_uid") String imp_uid) throws IamportResponseException, IOException{
return iamportClient.paymentByImpUid(imp_uid);
}
}
컨트롤러에 API url을 매핑해주고 결제정보를 리턴해주는 메소드이다.
우선 IamportClient 객체는 아임포트 의존성 주입으로 선언할 수 있고, IamportAPI는 API Key값을 숨기기 위해 임의로 만든 클래스다. 검증을 위해 사용하는 IamportClient 객체를 생성하기 위해서는 api키값과 api secret 키값이 필요하기 때문이다.
properties 파일을 하나 생성해 키값들을 적어주고, 그 키값을 불러오는 클래스로서 IamportAPI를 생성한 것이다. properties의 키값을 컨트롤러 생성자에서 바로 불러올 수는 없었는데 그 이유와 과정은 별도의 포스트에 정리해두었다. 아임포트 API KEY 숨기기, @value null
다시 ajax로 돌아가 결제성공시의 금액(rsp.paid_amount)과 검증한 금액(data.response.amount)이 같은지 비교한다. 두 금액이 같다면 마지막으로 주문고유번호와 결제고유번호를 인자로 succeedPay()를 호출하게된다.
* 공식문서에는 검증과정에서 액세스 토큰도 발급받도록 나와있다. 추후 개선 예정.
function succeedPay(imp_uid, merchant_uid){
$.ajax({
url : '/payment/succeed',
type : 'POST',
async : true,
dataType : "Json",
data :{
imp_uid: imp_uid, // 결제 고유번호
merchant_uid: merchant_uid // 주문번호
},
success : function(data){
if(data.cnt > 0){
var msg = '결제 및 검증이 완료되었습니다.'
alert(msg)
location.href="/mypage/order"
}else{
var msg = '결제가 완료되었으나 에러가 발생했습니다.'
alert(msg)
location.href="/mypage/order"
}
},
error : function (e){
alert("에러")
}
});
}
처음 결제버튼을 눌렀을 때 insert했던 DB의 결제상태를 (결제대기->결제완료) 업데이트하는 함수이다.
@PostMapping("/payment/succeed")
@ResponseBody
public Map<Object, Object> updateStatus(HttpServletRequest req){
String imp_uid = req.getParameter("imp_uid");
int order_no = Integer.parseInt(req.getParameter("merchant_uid"));
int status = 1;
Map<Object, Object> map = new HashMap<>();
//주문번호, 결제고유번호, 결제상태를 인자로 넘겨준다
int res = orderMapper.updateStatus(order_no, imp_uid, status);
if (res > 0) {
map.put("cnt", 1);
}else {
map.put("cnt", 0);
}
//장바구니 지우기
HttpSession session = req.getSession();
session.removeAttribute("cart");
return map;
}
조금만 손 보면 실 결제에 사용하는 결제페이지를 쉽게 구현할 수 있다. 굉장히 유용한 API인 것 같다.
참고로 테스트용으로 생성한 모듈의 경우 그날 자정이나 새벽 시간대에 결제취소 처리가 되며, 결제 최소금액은(KG이니시스 기준) 100원이므로 테스트용 상품을 등록할 때 참고하자.