본문 바로가기

프로젝트

[SpringBoot] 경매사이트 만들기 - 결제 서비스 (feat.아임포트, 카카오) - 2

https://record-developer.tistory.com/144

 

[SpringBoot] 경매사이트 만들기 - 결제 서비스 (feat.아임포트, 카카오) - 1

들어가기전.. 들어가기전 먼저 최종 계획을 말해보자면... 결제 서비스로 유저가 결제한뒤 받은 금액에서 수수료5%를 제외하고 나머지 금액을 경매물건을 올린 유저에게 이체시켜주는 시스템을

record-developer.tistory.com

이번포스팅은 위의 포스팅을 이어서 결제 검증을 하고, 결제 검증이 실패했을때 결제 승인 취소를하는 포스팅을 해보겠따!

 

결제 검증을 해야하는 이유?

결제 검증을 해야하는 이유는 아임포트 연동 가이드에 나와있는데 요약하자면

유저가 스크립트 조작해서 가격 변동시킬 수 있다 -> 이를 방지하기 위해 서버에서 옳바른 가격인지 검증한다.

이말이다.

https://docs.iamport.kr/implementation/payment - 아임포트 일반결제 연동 가이드 참조


결제 검증하기!

검증에 앞서 아임포트에서 제공되는 api를 사용하는 방법은 2가지가 있다.

  • 첫번째 : 직접 아임포트 api 사이트에서 원하는 api 주소를 가지고 사용하기
  • 두번째 : 이부분은 아임포트를 사용하는 다른 개발자분들이 협력해만든 모듈이라고 들었는데 여튼 만들어진 모듈을 사용하는 방법이 있다.

여기서 필자는 두번째 방법을 이용했고 기본적으로 깃허브엔 maven에 의존성추가하는 방법만 나와 있긴한데 당연히 gradle에다 추가도 가능하다.( 난 gradle씀)

 

 

■ 아임포트 api 제공 사이트

https://api.iamport.kr/

 

API-아임포트

 

api.iamport.kr

 

Maven에 의존성 추가하기

step1

	<repositories>
		<repository>
		    <id>jitpack.io</id>
		    <url>https://jitpack.io</url>
		</repository>
	</repositories>

 

step2

	<dependency>
	    <groupId>com.github.iamport</groupId>
	    <artifactId>iamport-rest-client-java</artifactId>
	    <version>0.2.21</version>
	</dependency>

 

Gradle에 의존성 추가하기 

step1

	allprojects {
		repositories {
			...
			maven { url 'https://jitpack.io' }
		}
	}

 

step2

	dependencies {
		implementation 'com.github.iamport:iamport-rest-client-java:0.2.21'
	}

 

 

이렇게 maven과 gradle 두가지 중 본인 프로젝트에서 사용중인 걸로 추가 시켜주면된다ㅎㅎ

 

 

 

그리고 결제정보를 저장해놓을 테이블이 필요하다고 판단돼서 결제정보 테이블 추가했다!

 

▶ ERD수정 

결제정보 테이블 추가

 

▶PaymentsInfo (결제 정보 테이블)

package org.my.toyproject.model;

import ...

/**
 * 결제 정보 entity
 * 
 * 결제 번호
 * 결제 방법
 * 주문 번호
 * 구매 번호
 * 가격
 * 구매자 주소
 * 구매자 우편
 * 판매 게시글
 * 구매자 
 * @author USER
 *
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@SequenceGenerator(
		name = "PAYMENTS_INFO_SEQ_GENERATOR",
		sequenceName = "PAYMENTS_INFO_SEQ",
		initialValue = 1,
		allocationSize = 1)
public class PaymentsInfo {
	
	@Id
	@GeneratedValue(
			strategy = GenerationType.SEQUENCE,
			generator = "PAYMENTS_INFO_SEQ_GENERATOR"
			)
	private long paymentsNo;
	
	@Column(nullable = false, length = 100)
	private String payMethod;
	
	@Column(nullable = false, length = 100)
	private String impUid;
	
	@Column(nullable = false, length = 100)
	private String merchantUid;
	
	@Column(nullable = false)
	private int amount;
	
	@Column(nullable = false, length = 100)
	private String buyerAddr;
	
	@Column(nullable = false, length = 100)
	private String buyerPostcode;
	
	@OneToOne
	@JoinColumn(name = "actionBoardNo")
	private ActionBoard actionBoard;
	
	@ManyToOne
	@JoinColumn(name = "memberNo")
	private Member member;
}

그럼 이제 검증부분을 로직을 한번 보자.

 

clients-side

payments.js -> requestPay()

/*******************************
결제 하기
********************************/
function requestPay() {
	// IMP.request_pay(param, callback) 결제창 호출
	IMP.init("imp03270447");
	IMP.request_pay({ // param
		...
        // 결제 파라미터들
        ...
	}, function (rsp) { // callback
        if (rsp.success) {// 결제성공시 로직
            let data = {
				imp_uid:rsp.imp_uid,
				amount:rsp.paid_amount,
				actionBoardNo:$("#actinoBoardNo").val()
			};
            //결제 검증
            $.ajax({
				type:"POST",
				url:"verifyIamport",
				data:JSON.stringify(data),
				contentType:"application/json; charset=utf-8",
				dataType:"json",
				success: function(result) {
					alert("결제검증 완료");
					//self.close();
				},
				error: function(result){
					alert(result.responseText);
					cancelPayments(rsp);
				}
			});
			
        } else {// 결제 실패 시 로직
			alert("결재 실패");
			alert(rsp.error_msg);
			console.log(rsp);            
        }
	});
}//requestPay

 

위의 함수에서 callback함수에서 결제가 성공적으로 이뤄졌다는 판단이 됐을때 data변수에 필요한 정보들을 넣고 json방식으로 서버에 보내주고, 서버에선 주어진 데이터로 검증을 시작한다.

 


 

sever-side

PaymentsApiController.java

package org.my.toyproject.controller.api;

import ...

/**
 * 결제 관련 기능을 동작하게하는 컨트롤러
 * 검증...
 * @author USER
 *
 */
@RestController
public class PaymentsApiController {
	//토큰 발급을 위해 아임포트에서 제공해주는 rest api 사용.(gradle로 의존성 추가)
	private final IamportClient iamportClientApi;
	
	//생성자로 rest api key와 secret을 입력해서 토큰 바로생성.
	public PaymentsApiController() {
		this.iamportClientApi = new IamportClient("6796671545054859",
				"064a5442d844755e7f75228e97c52f81a82e80bd67136a309ba026caa2165e21bbf44deb0b6b0638");
	}
	
	@Autowired
	private PaymentService paymentService;
	
	/**
	 * impUid로 결제내역 조회.
	 * @param impUid
	 * @return
	 * @throws IamportResponseException
	 * @throws IOException
	 */
	public IamportResponse<Payment> paymentLookup(String impUid) throws IamportResponseException, IOException{
		return iamportClientApi.paymentByImpUid(impUid);
	}
	
	/**
	 * impUid를 결제 번호로 찾고, 조회해야하는 경우.<br>
	 * 오버로딩.
	 * @param paymentsNo
	 * @return
	 * @throws IamportResponseException
	 * @throws IOException
	 */
	public IamportResponse<Payment> paymentLookup(long paymentsNo) throws IamportResponseException, IOException{
		PaymentsInfo paymentsInfo = paymentService.paymentLookupService(paymentsNo);
		return iamportClientApi.paymentByImpUid(paymentsInfo.getImpUid());
	}
	
	/**
	 * 결제검증을 위한 메서드<br>
	 * map에는 imp_uid, amount, actionBoardNo 이 키값으로 넘어옴.
	 * @param map
	 * @return
	 * @throws IamportResponseException
	 * @throws IOException
	 * @throws verifyIamportException
	 */
	@PostMapping("verifyIamport")
	public IamportResponse<Payment> verifyIamport(@RequestBody Map<String,String> map) throws IamportResponseException, IOException, verifyIamportException{
		String impUid = map.get("imp_uid");//실제 결제금액 조회위한 아임포트 서버쪽에서 id
		long actionBoardNo = Long.parseLong(map.get("actionBoardNo")); //DB에서 물건 가격 조회를 위한 번호
		int amount = Integer.parseInt(map.get("amount"));//실제로 유저가 결제한 금액
		
		//아임포트 서버쪽에 결제된 정보 조회.
		//paymentByImpUid 는 아임포트에 제공해주는 api인 결제내역 조회(/payments/{imp_uid})의 역할을 함. 
		IamportResponse<Payment> irsp = paymentLookup(impUid);

		paymentService.verifyIamportService(irsp, amount, actionBoardNo);
		
		return irsp;
	}
	
	...
	
	
}

 

이미 만들어진 모듈을 사용하기 때문에 우리는 간단하게 토큰 발급받고, 원하는 기능을 사용하면된다.

이때 토큰을 발급 받는 방법은 아임포트 관리자에서 REST API key , REST API Secret 을 복사하고

IamportClient 객체 생성자에 넣어주면된다.

 

예를들면 

IamportClient api = new IamportClient( REST API key, REST API Secret );

위의 방식처럼 객체를 만들게되면 토큰을 발급 받게된다. 이때 필자는 프로젝트에선 컨트롤러에 생성자를 만들어서 컨트롤러 클래스가 불러와질때 생성자를 통해 바로 토큰 발급받을수 있도록 했다.

 

 

paymentLookup 메서드

결제한 내역을 아임포트 서버에서 조회하는 메서드다.

기본적으로 imp_uid 값을 통해 내역을 조회하게 되는데 이때, imp_uid값을 매개변수로 받을수 있는 경우와 그렇지 않은 경우로 나눠서 만들어줬다.

경우를 나눈이유는 나중에 유저가 결제한걸 환불요청 했을땐 결제 번호를 통해서 찾을수 있게끔 만든건데,,,

지금 다시보니깐 어차피 imp_uid도 유일값이기 때문에 굳이 결제 번호를 거칠 필요없이 바로 imp_uid를 사용해도 될꺼같긴하다. --> 이부분은 후에 만들어보고 불피요하다 생각되면 삭제하겠음!

 

 

verifyIamport 메서드

여기가 이제 결제 검증을 동작하게 하는부분이다. 컨트롤러에선 결제내역만 조회하고, 얻은 데이터들로 Service쪽에서 검증이 이뤄지고, 옳바른 결제라면 DB에 값을 추가하고 비정상 결제라면 예외 처리를 진행했다.

 

 

 

server-side

PaymentService.java

package org.my.toyproject.service;

import ...

/**
 * 결제 관련 서비스를 제공해주는 로직
 * @author USER
 *
 */
@Service
public class PaymentService {
	@Autowired
	private PaymentRepository paymentRepository;
	
	@Autowired
	private ActionBoardService actionBoardService;
	
	@Autowired
	private MemberService memberService;
	
	
	/**
	 * 은행이름에 따른 코드들을 반환해줌<br>
	 * KG이니시스 기준.
	 * @param bankName
	 * @return
	 */
	public String code(String bankName) {
		String code="";
		if(bankName.equals("우리은행")||bankName.equals("우리")) code="20";
		else if(bankName.equals("국민은행")||bankName.equals("국민")) code="04";
		return code;
	}
	
	/**
	 * 현재 결제번호에 해당하는 정보를 갖고와서 반환해줌.
	 * @param paymentsNo
	 * @return
	 */
	@Transactional
	public PaymentsInfo paymentLookupService(long paymentsNo) {
		PaymentsInfo paymentsInfo = paymentRepository.getById(paymentsNo);
		return paymentsInfo;
	}
	
	/**
	 * 아임포트 서버쪽 결제내역과 DB에 물건가격을 비교하는 서비스. <br>
	 * 다름 -> 예외 발생시키고 GlobalExceptionHandler쪽에서 예외처리 <br>
	 * 같음 -> 결제정보를 DB에 저장(PaymentsInfo 테이블)
	 * @param irsp (아임포트쪽 결제 내역 조회 정보)
	 * @param actionBoardNo (내 DB에서 물건가격 알기위한 경매게시글 번호)
	 * @throws verifyIamportException
	 */
	@Transactional
	public void verifyIamportService(IamportResponse<Payment> irsp, int amount, long actionBoardNo) throws verifyIamportException {
		ActionBoard actionBoard = actionBoardService.findPostByActionBoardNo(actionBoardNo);

		
		//실제로 결제된 금액과 아임포트 서버쪽 결제내역 금액과 같은지 확인
		//이때 가격은 BigDecimal이란 데이터 타입으로 주로 금융쪽에서 정확한 값표현을 위해씀.
		//int형으로 비교해주기 위해 형변환 필요.
		if(irsp.getResponse().getAmount().intValue()!=amount)
			throw new verifyIamportException();
				
		//DB에서 물건가격과 실제 결제금액이 일치하는지 확인하기. 만약 다르면 예외 발생시키기.
		if(amount!=actionBoard.getImmediatly()) 
			throw new verifyIamportException();
		
		//아임포트에서 서버쪽 결제내역과 DB의 결제 내역 금액이 같으면 DB에 결제 정보를 삽입.
		Member member = memberService.findByEmail(irsp.getResponse().getBuyerEmail());
		
		PaymentsInfo paymentsInfo = PaymentsInfo.builder()
				.payMethod(irsp.getResponse().getPayMethod())
				.impUid(irsp.getResponse().getImpUid())
				.merchantUid(irsp.getResponse().getMerchantUid())
				.amount(irsp.getResponse().getAmount().intValue())
				.buyerAddr(irsp.getResponse().getBuyerAddr())
				.buyerPostcode(irsp.getResponse().getBuyerPostcode())
				.member(member)
				.actionBoard(actionBoard)
				.build();
		
		paymentRepository.save(paymentsInfo);
	}
	
	/**
	 * 결제 취소할때 필요한 파라미터들을
	 * CancelData에 셋업해주고 반환함.
	 * @param map
	 * @param impUid
	 * @param bankAccount
	 * @param code
	 * @return
	 * @throws RefundAmountIsDifferent 
	 */
	@Transactional
	public CancelData cancelData(Map<String,String> map, 
			IamportResponse<Payment> lookUp,
			PrincipalDetail principal, String code) throws RefundAmountIsDifferent {
		//아임포트 서버에서 조회된 결제금액 != 환불(취소)될 금액 이면 예외발생
		if(lookUp.getResponse().getAmount()!=new BigDecimal(map.get("checksum"))) 
			throw new RefundAmountIsDifferent();
		
		CancelData data = new CancelData(lookUp.getResponse().getImpUid(),true);
		data.setReason(map.get("reason"));
		data.setChecksum(new BigDecimal(map.get("checksum")));
		data.setRefund_holder(map.get("refundHolder"));
		data.setRefund_bank(code);
		data.setRefund_account(principal.getBankName());
		return data;
	}
	
	
}

 

검증에서 필요한 메서드는 verifyIamportService 이기 때문에 일단 이부분만 살펴보자

 

verifyIamportService

해당 메서드에선 두번의 비교를 통해 검증을 진행한다.

1. 아임포트 서버쪽에서 결제 내역과 실제 결제된 금액을 비교

2. 실제 결제된 금액과 내 DB에서 물건가격과 비교

이렇게 총 두번의 검증을 거치게 되고 검증이 완료됐을때 PaymentsInfo 테이블에 결제정보를 insert 해주면된다.

 

만약 1,2 번 검증과정에서 가격이 다른상황이 발생하면 예외를 발생시켰는데 여기서

verifyIamportException는 내가만든 예외명이다. 이 예외가 발생되면

GlobalExceptionHandler에서 예외를 처리하게 된다.

 

GlobalExceptionHandler.java

 

package org.my.toyproject.handler;

import ...

@ControllerAdvice
@RestController
public class GlobalExceptionHandler {
	
	/**
	 * 게시글작성시 모든값이 제대로 입력되지 않았을때 발생하는 예외
	 * @return
	 */
	@ExceptionHandler(DataIntegrityViolationException.class)
	public ResponseEntity<String> dataIntegrityViolationError(){
		String message = "값이 제대로 입력되지 않았습니다.(DataIntegrityViolationException)";
		return new ResponseEntity<>(message, HttpStatus.BAD_REQUEST);
	}
	
	/**
	 * 실제 결제금액과 DB의 결제 금액이 다를때 발생하는 예외
	 * @return
	 */
	@ExceptionHandler(verifyIamportException.class)
	public ResponseEntity<String> verifyIamportException(){
		return new ResponseEntity<>("실제 결제금액과 서버에서 결제금액이 다릅니다.",HttpStatus.BAD_REQUEST);
	}
	
	/**
	 * 환불가능 금액과 아임포트 서버에서 조회한 결제했던 금액이 일치하지 않을때 발생하는 예외
	 * @return
	 */
	@ExceptionHandler(RefundAmountIsDifferent.class)
	public ResponseEntity<String> RefundAmountIsDifferent(){
		return new ResponseEntity<String>("환불가능 금액과 결제했던 금액이 일치하지 않습니다.",HttpStatus.BAD_REQUEST);
	}
	
	/**
	 * 결제관련 예외
	 * @return
	 */
	@ExceptionHandler(IamportResponseException.class)
	public ResponseEntity<String> IamportResponseException(){
		return new ResponseEntity<>("결제관련해서 에러가 발생",HttpStatus.INTERNAL_SERVER_ERROR);
	}
	
	/**
	 * 그외 모든 에러는 서버에러로~
	 * @param e
	 * @return
	 */
	@ExceptionHandler(Exception.class)
	public ResponseEntity<String> handleArgumentException(Exception e) {
		String message = "서버 에러 입니다.(Exception)";
		return new ResponseEntity<String>(message, HttpStatus.INTERNAL_SERVER_ERROR);
	}
	
	
	
}

 

 


위에서 결제 검증을 통해 옳바른 결제인지 아닌지를 판단하고, 옳바른 결제라면 DB에 insert하도록 했다.

그럼 검증했는데 옳바른 결제가 아닐땐???

그때는 결제 취소를 통해서 승인됐던 결제금액을 승인취소 시켜줘야한다~ 이에 관한 기능이 결제취소란 api다.

함보자~

 

결제 취소되면 바로 푸시 알림옴!

결제 승인이 취소됐을때


 

 

결제취소 하기!

먼저 결제 취소시 api에 필요한 파라미터를 살펴보자.

아임포트 환불요청 가이드 참조

data 부분을 보면 필요한 정보들이 써져있다. 사실 더 정확히 보려면 앞서 링크에서 걸어줬던 아임포트 api 사이트에서 살펴보면된다.

아임포트 api 사이트 - 결제취소 api 파라미터

위의 사진은 아임포트 api 사이트에서 제공해주는 결제취소 요청시 필요한 파라미터들을 보여준다.

여기서 필자가  clients-side에서 server-side로 넘겨준 파라미터는

imp_uid (merchant_uid 대신 사용)

reason

checksum

refund_holder

이다. 그리고 server-side에선 

refund-bank

refund-account

를 추가해서 사용했다.

 

checksum을 사용한이유( 아임포트에서 권장 )

아임포트 환불요청 가이드

이처럼 권장을하고 있고, checksum을 넣어주는건 크게 어려운부분이 아니라 넣어줬다.


paymensts.js -> requestPay()

 

function requestPay() {
	// IMP.request_pay(param, callback) 결제창 호출
	IMP.init("imp03270447");
	IMP.request_pay({ // param
		...
        //결제 파라미터
        ...
	}, function (rsp) { // callback
        if (rsp.success) {// 결제성공시 로직
            let data = {
				imp_uid:rsp.imp_uid,
				amount:rsp.paid_amount,
				actionBoardNo:$("#actinoBoardNo").val()
			};
            
            $.ajax({
				type:"POST",
				url:"verifyIamport",
				data:JSON.stringify(data),
				contentType:"application/json; charset=utf-8",
				dataType:"json",
				success: function(result) {
					alert("결제검증 완료");
					//self.close();
				},
				error: function(result){
                //검증실패!!
					alert(result.responseText);
					cancelPayments(rsp);
				}
			});
			
        } else {// 결제 실패 시 로직
			alert("결재 실패");
			alert(rsp.error_msg);
			console.log(rsp);            
        }
	});
}//requestPay

 

위의 js함수를 보면 결제를 시도 -> 결제 검증 -> 검증 성공 or 실패로 나뉜다.

이때 검증 실패가 됐을때 cancelPayments(rsp) 라는 함수가 rsp(결제 승인됐을때 정보를 담은 변수) 매개변수를 갖고 실행되게 된다.

cancelPayments()가 바로 결제 취소를 실행하는 함수다.

 

 

 

paymensts.js -> cancelPayments()

 

/*******************************
결제 취소
********************************/
function cancelPayments(temp){
	let data = null;
	if(temp!=null){//결제금액이 달라졌을때 결제취소
			data={
			impUid:temp.imp_uid,
			reason:"결제 금액 위/변조. 결제 금액이 일치안함",
			checksum:temp.paid_amount,
			refundHolder:temp.buyer_name
		};
	}else{//유저가 환불을 요청했을때 데이터
		//화면만들고 작성예정	
	}

	$.ajax({
		type:"POST",
		url:"cancelPayments",
		data:JSON.stringify(data),
		contentType:"application/json; charset=utf-8",
		success: function(result){
			alert("결제금액 환불완료");
			//self.close();//팝업창닫기
			//결제 취소화면으로 이동해주기.
		},
		error: function(result){
			alert("결제금액 환불못함. 이유: "+result.responseText);
		}
	});
}//cancelPayments

 

sever-side로 보내줄 data는 매개변수가 있냐 없냐에 따라서 다르게 data를 설정해 줄예정이다.

매개변수가 있을때 -> 결제 검증을 실패하고 승인 취소를 해야하는 경우

매개변수가 없을때 -> 유저가 결제된걸 나중에 환불요청 했을때

 

 

 

PaymentsApiController.java

 

package org.my.toyproject.controller.api;

import ...

/**
 * 결제 관련 기능을 동작하게하는 컨트롤러
 * 검증...
 * @author USER
 *
 */
@RestController
public class PaymentsApiController {
	//토큰 발급을 위해 아임포트에서 제공해주는 rest api 사용.(gradle로 의존성 추가)
	private final IamportClient iamportClientApi;
	
	//생성자로 rest api key와 secret을 입력해서 토큰 바로생성.
	public PaymentsApiController() {
		this.iamportClientApi = new IamportClient("6796671545054859",
				"064a5442d844755e7f75228e97c52f81a82e80bd67136a309ba026caa2165e21bbf44deb0b6b0638");
	}
	
	@Autowired
	private PaymentService paymentService;
	
	/**
	 * impUid로 결제내역 조회.
	 * @param impUid
	 * @return
	 * @throws IamportResponseException
	 * @throws IOException
	 */
	public IamportResponse<Payment> paymentLookup(String impUid) throws IamportResponseException, IOException{
		return iamportClientApi.paymentByImpUid(impUid);
	}
	
	/**
	 * impUid를 결제 번호로 찾고, 조회해야하는 경우.<br>
	 * 오버로딩.
	 * @param paymentsNo
	 * @return
	 * @throws IamportResponseException
	 * @throws IOException
	 */
	public IamportResponse<Payment> paymentLookup(long paymentsNo) throws IamportResponseException, IOException{
		PaymentsInfo paymentsInfo = paymentService.paymentLookupService(paymentsNo);
		return iamportClientApi.paymentByImpUid(paymentsInfo.getImpUid());
	}
	
	/**
	 * 결제검증을 위한 메서드<br>
	 * map에는 imp_uid, amount, actionBoardNo 이 키값으로 넘어옴.
	 * @param map
	 * @return
	 * @throws IamportResponseException
	 * @throws IOException
	 * @throws verifyIamportException
	 */
	@PostMapping("verifyIamport")
	public IamportResponse<Payment> verifyIamport(@RequestBody Map<String,String> map) throws IamportResponseException, IOException, verifyIamportException{
		String impUid = map.get("imp_uid");//실제 결제금액 조회위한 아임포트 서버쪽에서 id
		long actionBoardNo = Long.parseLong(map.get("actionBoardNo")); //DB에서 물건 가격 조회를 위한 번호
		int amount = Integer.parseInt(map.get("amount"));//실제로 유저가 결제한 금액
		
		//아임포트 서버쪽에 결제된 정보 조회.
		//paymentByImpUid 는 아임포트에 제공해주는 api인 결제내역 조회(/payments/{imp_uid})의 역할을 함. 
		IamportResponse<Payment> irsp = paymentLookup(impUid);

		paymentService.verifyIamportService(irsp, amount, actionBoardNo);
		
		return irsp;
	}
	
	/**
	 * 결제한 금액을 취소요청이 들어오면 실행되는 메서드<br>
	 * 환불될 금액과 아임포트 서버에서 조회한 결제 금액이 다르면 환불 or 취소 안됨.
	 * @param map
	 * @param principal
	 * @return
	 * @throws IamportResponseException
	 * @throws IOException
	 * @throws RefundAmountIsDifferent
	 */
	@PostMapping("cancelPayments")
	public IamportResponse<Payment> cancelPayments(@RequestBody Map<String,String> map,
			@AuthenticationPrincipal PrincipalDetail principal) throws IamportResponseException, IOException, RefundAmountIsDifferent {
		
		//조회
		IamportResponse<Payment> lookUp = null;
		if(map.containsKey("impUid")) lookUp = paymentLookup(map.get("impUid"));//들어온 정보에 imp_uid가 있을때
		else if(map.containsKey("paymentsNo")) lookUp = paymentLookup(map.get("paymentsNo"));//imp_uid가 없을때
		 
		String code = paymentService.code(principal.getBankName());//은행코드
		CancelData data = paymentService.cancelData(map,lookUp,principal,code);//취소데이터 셋업
		IamportResponse<Payment> cancel = iamportClientApi.cancelPaymentByImpUid(data);//취소
		
		return cancel;
	}
	
	
}

 

이때는 cancelPayments 메서드만 확인해주면된다.

그리고 결제를 취소하기 위해선 앞서 보여줬던 파라미터들을 셋업해야 하는데 이 모듈에선 CancelData라는 클래에스에서 셋업할수 있게끔 해놨다.

 

cnacelPayments 메서드

CancelData를 가지고 결제 취소를 동작하게함.

 

 

CancelData.java

 

package com.siot.IamportRestClient.request;

import java.math.BigDecimal;

import com.google.gson.annotations.SerializedName;

public class CancelData {

	@SerializedName("imp_uid")
	private String imp_uid;
	
	@SerializedName("merchant_uid")
	private String merchant_uid;
	
	@SerializedName("amount")
	private BigDecimal amount;

	@SerializedName("tax_free")
	private BigDecimal tax_free;

	@SerializedName("checksum")
	private BigDecimal checksum;
	
	@SerializedName("reason")
	private String reason;
	
	@SerializedName("refund_holder")
	private String refund_holder;
	
	@SerializedName("refund_bank")
	private String refund_bank;
	
	@SerializedName("refund_account")
	private String refund_account;
	
	@SerializedName("escrow_confirmed")
	private boolean escrow_confirmed;

	@SerializedName("extra")
	private ExtraRequesterEntry extra;
	
	public CancelData(String uid, boolean imp_uid_or_not) {
		if ( imp_uid_or_not ) {
			this.imp_uid = uid;
		} else {
			this.merchant_uid = uid;
		}
	}
	
	public CancelData(String uid, boolean imp_uid_or_not, BigDecimal amount) {
		this(uid, imp_uid_or_not);
		this.amount = amount;
	}

	public void setTax_free(BigDecimal tax_free) {
		this.tax_free = tax_free;
	}

	public void setChecksum(BigDecimal checksum) {
		this.checksum = checksum;
	}

	public void setReason(String reason) {
		this.reason = reason;
	}

	public void setRefund_holder(String refund_holder) {
		this.refund_holder = refund_holder;
	}

	public void setRefund_bank(String refund_bank) {
		this.refund_bank = refund_bank;
	}

	public void setRefund_account(String refund_account) {
		this.refund_account = refund_account;
	}

	public void setEscrowConfirmed(boolean escrow_confirmed) {
		this.escrow_confirmed = escrow_confirmed;
	}

	public ExtraRequesterEntry getExtra() {
		return extra;
	}

	public void setExtra(ExtraRequesterEntry extra) {
		this.extra = extra;
	}
}

 

이 객체를 생성시 아임포트 api 사이트에서 말했던것처럼 imp_uid를 사용하냐 안하냐와 부분환불을 하냐 안하냐에 따라서 객체를 다르게 생성하게 된다.

그리고 참고로 여기선 refund_tel값이 없다는걸 알아두자.

 

 

PaymentService.java

 

package org.my.toyproject.service;

import ...

/**
 * 결제 관련 서비스를 제공해주는 로직
 * @author USER
 *
 */
@Service
public class PaymentService {
	@Autowired
	private PaymentRepository paymentRepository;
	
	@Autowired
	private ActionBoardService actionBoardService;
	
	@Autowired
	private MemberService memberService;
	
	
	/**
	 * 은행이름에 따른 코드들을 반환해줌<br>
	 * KG이니시스 기준.
	 * @param bankName
	 * @return
	 */
	public String code(String bankName) {
		String code="";
		if(bankName.equals("우리은행")||bankName.equals("우리")) code="20";
		else if(bankName.equals("국민은행")||bankName.equals("국민")) code="04";
		return code;
	}
	
	/**
	 * 현재 결제번호에 해당하는 정보를 갖고와서 반환해줌.
	 * @param paymentsNo
	 * @return
	 */
	@Transactional
	public PaymentsInfo paymentLookupService(long paymentsNo) {
		PaymentsInfo paymentsInfo = paymentRepository.getById(paymentsNo);
		return paymentsInfo;
	}
	
	/**
	 * 아임포트 서버쪽 결제내역과 DB에 물건가격을 비교하는 서비스. <br>
	 * 다름 -> 예외 발생시키고 GlobalExceptionHandler쪽에서 예외처리 <br>
	 * 같음 -> 결제정보를 DB에 저장(PaymentsInfo 테이블)
	 * @param irsp (아임포트쪽 결제 내역 조회 정보)
	 * @param actionBoardNo (내 DB에서 물건가격 알기위한 경매게시글 번호)
	 * @throws verifyIamportException
	 */
	@Transactional
	public void verifyIamportService(IamportResponse<Payment> irsp, int amount, long actionBoardNo) throws verifyIamportException {
		ActionBoard actionBoard = actionBoardService.findPostByActionBoardNo(actionBoardNo);

		
		//실제로 결제된 금액과 아임포트 서버쪽 결제내역 금액과 같은지 확인
		//이때 가격은 BigDecimal이란 데이터 타입으로 주로 금융쪽에서 정확한 값표현을 위해씀.
		//int형으로 비교해주기 위해 형변환 필요.
		if(irsp.getResponse().getAmount().intValue()!=amount)
			throw new verifyIamportException();
				
		//DB에서 물건가격과 실제 결제금액이 일치하는지 확인하기. 만약 다르면 예외 발생시키기.
		if(amount!=actionBoard.getImmediatly()) 
			throw new verifyIamportException();
		
		//아임포트에서 서버쪽 결제내역과 DB의 결제 내역 금액이 같으면 DB에 결제 정보를 삽입.
		Member member = memberService.findByEmail(irsp.getResponse().getBuyerEmail());
		
		PaymentsInfo paymentsInfo = PaymentsInfo.builder()
				.payMethod(irsp.getResponse().getPayMethod())
				.impUid(irsp.getResponse().getImpUid())
				.merchantUid(irsp.getResponse().getMerchantUid())
				.amount(irsp.getResponse().getAmount().intValue())
				.buyerAddr(irsp.getResponse().getBuyerAddr())
				.buyerPostcode(irsp.getResponse().getBuyerPostcode())
				.member(member)
				.actionBoard(actionBoard)
				.build();
		
		paymentRepository.save(paymentsInfo);
	}
	
	/**
	 * 결제 취소할때 필요한 파라미터들을
	 * CancelData에 셋업해주고 반환함.
	 * @param map
	 * @param impUid
	 * @param bankAccount
	 * @param code
	 * @return
	 * @throws RefundAmountIsDifferent 
	 */
	@Transactional
	public CancelData cancelData(Map<String,String> map, 
			IamportResponse<Payment> lookUp,
			PrincipalDetail principal, String code) throws RefundAmountIsDifferent {
		//아임포트 서버에서 조회된 결제금액 != 환불(취소)될 금액 이면 예외발생
		if(!lookUp.getResponse().getAmount().equals(new BigDecimal(map.get("checksum")))) 
			throw new RefundAmountIsDifferent();
		
		CancelData data = new CancelData(lookUp.getResponse().getImpUid(),true);
		data.setReason(map.get("reason"));
		data.setChecksum(new BigDecimal(map.get("checksum")));
		data.setRefund_holder(map.get("refundHolder"));
		data.setRefund_bank(code);
		data.setRefund_account(principal.getBankName());
		return data;
	}
	
	
}

 

결제 취소를 위해 여기서 알아야하는 메서드는

code , cancelData 메서드다.

 

code 메서드

결제 취소를 요처하는 사용자의 은행명을 가지고 아임포트 api 사이트에서 제공해주는 은행코드를 구함.

사실 이부분은 간단하지만 고민이 좀 있다...

사용자가 사용하는 은행에따라 코드도 다르지만, 사용하는 PG사에 따라서 또 달라진다. 물론 나는지금 KG이니시스 하나만 사용하기에 무리없지만 여러개의 PG사를 사용한다면 조금 귀찮아질수도 있다.

그래서 그냥 아이에 PG사별 클래스를 만들고 클래스안에다 은행별로 코드를 반환시키고 서비스쪽에선 커맨드패턴으로 한줄로 실행할까 생각중이긴하다. 근데 또 굳이 하나만 사용하는데 그럴필요가 있나 생각들기도 하고,,,, 고민좀 해봐야겠다.

 

cancelData 메서드

이 메서드는 그냥 CancelData를 셋업하고 반환시켜주는 메서드다.

다만 결제 조회내역 금액과 checksum(환불가능 금액)이 다르다면 RefundAmountIsDifferent 예외가 발생하도록 했다.

(RefundAmountIsDifferent 사용자 정의 Exception임)

그리고 금액을 비교할때 데이터 타입이 Bigdecimal 타입인데, 이 타입은 Object 취급되기 때문에

equals로 비교해줘야한다. ( == , != 안됨)

 

 


이렇게 결제 검증 및 환불(취소)까지 아임포트를 이용해 프로젝트에 어떻게 적용 시켰는지 알아봤다.

근데, 검증 부분에서 아임포트에선 웹훅을 권장하는데 웹훅을 사용하는 이유는 결제도중 인터넷 끊김 등의 변수가 발생했을때 결제가 정상적으로 완료됐는지 푸시알림 해주는걸로 알고있다. 여기서 나는 적용하진 않았지만, 시간될때 한번 적용해보려고 한다.

 

 

★이슈사항

문제
결제 검증부분에서 DB데이터와 아임포터 서버 데이터 비교했을때
다르면 예외를 발생시켰는데 어떤 예외명으로 발생시켜야할지..?
해결
사용자 정의 예외클래스를 만들어서 해결함.

 

 

문제
결제를 했을때 나중에 결제 취소, 조회 등을 할때
결제 정보를 저장해놓은 테이블이 필요.
해결
PaymentsInfo 테이블 추가.

 

 

문제
아임포트 Rest API사용할때 아임포트에서 제공해주는 API url이
아니라 의존성 추가해서 모듈을 사용하길 원했음.
해결
maven { url 'https://jitpack.io' }
implementation 'com.github.iamport:iamport-rest-client-java:0.2.21'
이렇게 의존성 추가했기 때문에 만들어진 모듈에서 토근 발급 및
각종 api 사용 가능하도록 했음.

 

대체적으로 아임포트에서 가이드가 잘나와있기 때문에 이슈사항이 크게 없다고 생각한다.

 

★다음에 할꺼

지금까지 한거 다시한번 살펴보기. 수정이나 추가할게 있는지.

금융결제원 api 사용해서 판매자한테 돈 이체(송금) 하기.

입찰 crud.

소셜로그인.

 

할게 많긴한데,,, 댓글, 신고, 추천 등...ㅠㅠㅠ 아무래도 혼자서 하다보니 개발속도가 좀 더딘듯한 느낌이 있지만

그래도 무슨일이 있어도 끝까지 완성시킬꺼다. 꼭!