[식구하자_MSA] 결제 마이크로 서비스 아임포트 사용한 결제 구현 - 1

이민우·2024년 3월 28일
2

🍀 식구하자_MSA

목록 보기
8/21

지난 포스팅 채팅 마이크로 서비스 개발 완료 후, 이어서 결제 마이크로 서비스를 개발 과정을 포스팅 해보도록 하겠습니다.

클릭👉 github 주소 참고

요구사항 정리

  • 중고 식물 거래 시 , 페이 머니 충전을 통한 결제
    • 실제 계좌 이체 연동은 불가능하니, 아임포트 사용한 결제를 구현하여 페이 머니 충전
  • 페이 머니 환급시, 계좌 입력하여 환급(실제로 계좌로 이체x)
  • 페이 머니 조회

기능 흐름도를 보면 아래와 같습니다.

💰 아임포트 사용한 결제 구현

1. 계정 생성

https://portone.io/korea/ko

2. 결제 연동

좌측 메뉴에서 결제 연동 탭으로 이동

내 식별 코드를 클릭하면 결제창 연동 및 API 호출 시 필요한 연동 정보를 확인할 수 있습니다.

하단에서 결제대행사를 추가. 저는 카카오페이로 PG사를 선택하고 추가 해주었습니다.

다음과 같이 등록된걸 확인할 수 있습니다.

Spring 서버 코드

iamport 결제 연동만 필요하신분은 주석으로 처리된 iamport 연동 부분만 작성하시면 됩니다.

1. 의존성 추가

repositories {
    maven { url 'https://jitpack.io' }
    mavenCentral()
}
implementation 'com.github.iamport:iamport-rest-client-java:0.1.6'
IamportClient를 생성할 때 restApiKey와 restApiSecret이 필요합니다! (결제 연동 탭에서 확인했던 정보)
저는 application.yml에 해당 내용을 따로 작성해주고 @Value 애노테이션을 사용하여 값을 가져왔습니다!

2. Controller

@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);
    }


}

3. yml 작성

iamport:
  key: {본인 key}
  secret: {본인 정보에 맞게 넣으시면 됩니다}

4. service

@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);

    • custom exception을 따로 만들어, 보유 페이 머니보다 입력한 환불할 금액이 많으면 적절한 예외 처리를 해주었습니다.

Repository

  • querydsl을 이용하여 repository 코드를 작성했습니다!
@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).

profile
백엔드 공부중입니다!

0개의 댓글

관련 채용 정보