지난 포스팅 채팅 마이크로 서비스 개발 완료 후, 이어서 결제 마이크로 서비스를 개발 과정을 포스팅 해보도록 하겠습니다.
클릭👉 github 주소 참고
기능 흐름도를 보면 아래와 같습니다.
좌측 메뉴에서 결제 연동 탭으로 이동
내 식별 코드를 클릭하면 결제창 연동 및 API 호출 시 필요한 연동 정보를 확인할 수 있습니다.
iamport 결제 연동만 필요하신분은 주석으로 처리된 iamport 연동 부분만 작성하시면 됩니다.
repositories {
maven { url 'https://jitpack.io' }
mavenCentral()
}
implementation 'com.github.iamport:iamport-rest-client-java:0.1.6'
IamportClient를 생성할 때 restApiKey와 restApiSecret이 필요합니다! (결제 연동 탭에서 확인했던 정보)
저는 application.yml에 해당 내용을 따로 작성해주고 @Value 애노테이션을 사용하여 값을 가져왔습니다!
@RestController
@Slf4j
@RequiredArgsConstructor
public class PaymentController {
@Value("${iamport.key}")
private String restApiKey;
@Value("${iamport.secret}")
private String restApiSecret;
private IamportClient iamportClient;
private final PaymentService paymentService;
@PostConstruct
public void init() {
this.iamportClient = new IamportClient(restApiKey, restApiSecret);
}
//iamport api 연동
@PostMapping("/verifyIamport/{imp_uid}")
public IamportResponse<Payment> paymentByImpUid(@PathVariable("imp_uid") String imp_uid) throws IOException {
log.info("info : " + iamportClient.getAuth());
return iamportClient.paymentByImpUid(imp_uid);
}
//식구페이 머니 조회
@GetMapping("/payMoney/{memberNo}")
public ResponseEntity<PaymentResponseDto> getPayMoney(@PathVariable("memberNo") Integer memberNo) {
PaymentResponseDto paymentResponseDto = paymentService.getPayMoney(memberNo);
return ResponseEntity.ok().body(paymentResponseDto);
}
//식구페이 머니 충전
@PostMapping("/payMoney/charge")
public void chargePayMoney(@RequestBody PaymentRequestDto paymentRequestDto) {
paymentService.chargePayMoney(paymentRequestDto);
}
//식구페이로 거래하기
@PostMapping("/payMoney/trade")
public void tradePayMoney(@RequestBody PaymentRequestDto paymentRequestDto, @RequestParam(value = "sellerNo", required = false) Integer sellerNo) {
paymentService.tradePayMoney(paymentRequestDto, sellerNo);
}
//식구페이 머니 환불
@PatchMapping("/payMoney/refund")
public void refundPayMoney(@RequestBody PaymentRequestDto paymentRequestDto) {
paymentService.refundPayMoney(paymentRequestDto);
}
}
iamport:
key: {본인 key}
secret: {본인 정보에 맞게 넣으시면 됩니다}
@Service
@RequiredArgsConstructor
@Slf4j
public class PaymentService {
private final PaymentRepository paymentRepository;
/**
* 식구페이 머니 충전 메서드
* iamport로 결제 완료 되면 페이 머니로 충전
* @param : PaymentRequestDto paymentRequestDto
*/
@Transactional
public void chargePayMoney(PaymentRequestDto paymentRequestDto) {
if (!paymentRepository.existsByMemberNo(paymentRequestDto.getMemberNo())) {
Payment payment=Payment.builder()
.payMoney(paymentRequestDto.getPayMoney())
.memberNo(paymentRequestDto.getMemberNo())
.build();
paymentRepository.save(payment);
}
else{
paymentRepository.existsByMemberNoUpdatePayMoney(paymentRequestDto);
}
}
/**
* 식구페이 머니 환불 메서드
* 원하는 금액 환불후 계좌 송금(실제로 계좌로 이체되진 않음)
* 환불할 금액이 없을 경우 예외 처리
* 사용자가 모르고 환불요청을 두번 이상 연속 했을 경우를 대비해(적절한 ui/ux설계가 없으므로)
* synchronized를 통해 동시성 제어!
* @param : UpdatePaymentRequestDto paymentRequestDto
*/
public synchronized void refundPayMoney(PaymentRequestDto paymentRequestDto) {
// memberNo로 보유 페이머니 조회
Payment payment = paymentRepository.findByMemberNo(paymentRequestDto.getMemberNo());
//보유 페이 머니보다 입력한 환불할 금액이 많으면 예외 처리
if (payment.getPayMoney()- paymentRequestDto.getPayMoney() < 0) {
throw new CustomException(ErrorCode.INSUFFICIENT_PAYMONEY);
}
payment.decreasePayMoney(paymentRequestDto.getPayMoney());
paymentRepository.saveAndFlush(payment);
}
/**
* 식구페이 머니 조회 메서드
* 조회용 메서드라 @Transactional(readOnly = true) 처리
* @param : Integer memberNo
*/
@Transactional(readOnly = true)
public PaymentResponseDto getPayMoney(Integer memberNo) {
Payment payment = paymentRepository.findByMemberNo(memberNo);
PaymentResponseDto paymentResponseDto = PaymentResponseDto.builder()
.payMoney(payment.getPayMoney())
.memberNo(payment.getMemberNo())
.build();
return paymentResponseDto;
}
/**
*
* 식구페이 거래 메서드
* 판매자 상대 멤버 번호를 통해 해당 조회 후
* 판매자 paymoney += 거래할 금액
* 구매자 Paymoney -= 거래할 금액
* @param : PaymentRequestDto paymentRequestDto, Integer sellerNo
*/
@Transactional
public void tradePayMoney(PaymentRequestDto paymentRequestDto, Integer sellerNo) {
Payment sellerPayment = paymentRepository.findByMemberNo(sellerNo);
Payment buyerPayment = paymentRepository.findByMemberNo(paymentRequestDto.getMemberNo());
//거레할 금액보다 구매자 보유 payMoney가 적으면 예외 처리
if (buyerPayment.getPayMoney()< paymentRequestDto.getPayMoney()) {
throw new CustomException(ErrorCode.INSUFFICIENT_PAYMONEY);
}
paymentRepository.tradePayMoney(sellerPayment.getMemberNo(), buyerPayment.getMemberNo(), paymentRequestDto);
}
}
여기서 저는 페이머니 계좌 환불 메서드에서 사용자가 모르고 환불요청을 두번 이상 동시에 했을 경우를 대비해 페이머니가 공유 자원은 아니지만, 각각의 거래가 사용자의 잔액에 올바르게 반영되어야 하므로
하나의 요청만 실행 할 수있게 synchronized를 통해 동시성 제어를 해주었습니다.
throw new CustomException(ErrorCode.INSUFFICIENT_PAYMONEY);
@RequiredArgsConstructor
public class PaymentRepositoryImpl implements CustomPaymentRepository{
private final JPAQueryFactory jpaQueryFactory;
public void existsByMemberNoUpdatePayMoney(PaymentRequestDto paymentRequestDto) {
jpaQueryFactory.update(payment)
.set(payment.payMoney, payment.payMoney.add(paymentRequestDto.getPayMoney()))
.where(payment.memberNo.eq(paymentRequestDto.getMemberNo()))
.execute();
}
public void tradePayMoney(Integer sellerNo, Integer buyerNo, PaymentRequestDto paymentRequestDto) {
jpaQueryFactory.update(payment)
.set(payment.payMoney, payment.payMoney.add(paymentRequestDto.getPayMoney()))
.where(payment.memberNo.eq(sellerNo))
.execute();
jpaQueryFactory.update(payment)
.set(payment.payMoney, payment.payMoney.subtract(paymentRequestDto.getPayMoney()))
.where(payment.memberNo.eq(buyerNo))
.execute();
}
}
existsByMemberNoUpdatePayMoney 메서드: 이 메서드는 특정 회원의 payMoney를 증가시키는 업데이트 작업을 수행합니다. paymentRequestDto에서 제공된 금액만큼 payMoney를 증가시킵니다.
tradePayMoney 메서드: 두 회원 간의 금액 이동을 처리합니다. 판매자의 memberNo에 해당하는 회원의 payMoney를 증가시키고(add)
, 구매자의 memberNo에 해당하는 회원의 payMoney를 감소시킵니다(substract)
.