결제 시스템 (Spring, Import)

박찬섭·2024년 10월 22일

스프링

목록 보기
10/14

Import
구현 내용
리뷰

Import

결제대행사이트 Import를 사용하여 결제 시스템을 구축할 수 있다.

실거래에서는 사업자번호 등록, 계좌 연결 등을 추가로 넣어야 한다.

아직 코드상에서는 Import라는 이름으로 사용중이지만 사이트는 PortOne으로 명칭이 바뀌어 있다.
사이트에서 UI나 기타 설정명이 좀 자주 바뀌는 것 같다.

결제가 이루어 지는 방법은 Oauth로그인과 유사한 것 같다.

1. 프론트에서 백엔드(Spring)에 결제API 요청
2. 백엔드는 외부 서버(Import)에 결제될 내용 사전 등록
3. 백엔드는 외부 서버와 통신 후 성공 여부 및 결제될 데이터를 프론트에게 응답
4. 백엔드 서버로부터 성공 여부 및 데이터를 확인하고 Import에 실제 거래 요청

위 진행순서대로 진행하지 않고 백엔드 서버가 바로 Import에 실제 결제 요청을 보내는 방법도 있다.

구현 내용

거래가 이루어지는 방법은 아래와 같다

  1. 프론트에서 결제할 내용(상품 ID), 로그인 정보를 결제 API에 POST 요청을 한다.

  2. 서버는 프론트에서 보낸 상품 ID, 로그인 정보를 기반으로 외부 서버(PortOne)에 사전 결제 내용을 요청, 따로 DB에 결제내용을 등록하고 프론트에게 외부 서버와의 요청 결과 및 결제될 내용을 응답한다.

  3. 프론트에서는 서버에게 응답받은 결과를 통해 외부 서버에게 직접 결제 요청을 하고 외부 서버와의 통신 결과와 고유 결제 ID를 다시 백엔드 서버에게 요청한다.

  4. 서버는 다시 결제 고유 결제 ID를 외부 서버를 통한 조회하고 결제 결과를 DB에 저장한다.


2번 내용

//controller
public ResponseDto<PreOrderResponseDto> prepareOrder(
            @RequestBody @Valid PreOrderRequestDto preOrderRequestDto,
            @AuthenticationPrincipal UserDetailsImpl userDetails) throws IamportResponseException, IOException {
        PreOrderResponseDto responseDto = paymentService.prepareOrder(preOrderRequestDto, userDetails.user());
        return ResponseDto.success("사전 결제 등록 성공", responseDto);
    }
//service
public PreOrderResponseDto prepareOrder(PreOrderRequestDto preOrderRequestDto, User user) throws IamportResponseException, IOException {
        List<DigitalProduct> orderedProducts = getProductList(preOrderRequestDto.getOrderItems());
        if (preOrderRequestDto.getOrderItems().size() != orderedProducts.size()) {
            throw new IllegalArgumentException("존재하지 않는 상품에 대한 주문입니다.");
        }

        BigDecimal totalAmount = orderedProducts.stream()
                .map(product -> new BigDecimal(product.getAmount()))
                .reduce(BigDecimal.ZERO, BigDecimal::add);

        PrepareData prepareData = new PrepareData(preOrderRequestDto.getMerchantUid(), totalAmount);
        IamportResponse<Prepare> iamportResponse = iamportClient.postPrepare(prepareData);
        if (iamportResponse.getCode() != 0) {
            throw new IllegalArgumentException(iamportResponse.getMessage());
        }
        log.info("사전 결제 아임포트 추가 성공");

        PaymentInfo prePaymentInfo = new PaymentInfo(preOrderRequestDto);
        paymentInfoRepository.save(prePaymentInfo);


        Orders preOrders = createOrder(totalAmount.longValue(), user, prePaymentInfo);
        log.info("사전 주문 테이블 생성 성공");

        createOrdersDetail(preOrders, orderedProducts);
        return new PreOrderResponseDto(totalAmount, "포인트 구매");
    }
public Orders createOrder(Long totalAmount, User user, PaymentInfo prePaymentInfo) {
        Orders preOrders = new Orders(totalAmount, user, prePaymentInfo);
        prePaymentInfo.updateOrders(preOrders);
        return ordersRepository.save(preOrders);
    }

4번 내용

//controller
public ResponseDto<Void> postOrder(
            @PathVariable String impUid,
            @AuthenticationPrincipal UserDetailsImpl userDetails) throws IamportResponseException, IOException {
        paymentService.postOrder(impUid, userDetails.user());
        return ResponseDto.success("결제 완료", null);
    }
public void postOrder(String impUid, User user) throws IamportResponseException, IOException {
        IamportResponse<Payment> payment = iamportClient.paymentByImpUid(impUid);
        Payment response = payment.getResponse();
        LocalDateTime responsePaidAt = changePaidAtLocalDateTime(response.getPaidAt());

        PaymentInfo paymentInfo = paymentInfoRepository.findPaymentInfoWithDetailsByMerchantUid(response.getMerchantUid()).orElseThrow(() ->
                new IllegalArgumentException("존재 하지 않는 주문 내용입니다.")
        );

        Orders orders = paymentInfo.getOrders();

        if (!Objects.equals(orders.getUser().getUserId(), user.getUserId())) {
            throw new IllegalArgumentException("주문한 유저와 결제한 유저가 일치하지 않습니다.");
        }

        List<OrdersDetail> ordersDetailList = orders.getOrdersDetailList();

        paymentInfo.updateImpUid(response.getImpUid());
        paymentInfo.updatePaymentDate(responsePaidAt);
        paymentInfo.updatePaymentMethod(response.getPayMethod());

        user.updatePoint(processPaymentAndUpdatePoint(responsePaidAt, ordersDetailList));
        userRepository.save(user);
    }
public LocalDateTime changePaidAtLocalDateTime(Date paidAt) {
        Instant instant = paidAt.toInstant();
        return LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
    }
public List<DigitalProduct> getProductList(List<Integer> productList) {
        List<Long> productIds = productList.stream()
                .map(Long::valueOf)
                .toList();
        return digitalProductRepository.findAllById(productIds);
    }

리뷰

구현 단계에서 그치고 실제 적용 및 실제 서비스에서 작동하지는 않아서
더 디테일하게 구현하고 준비하지는 않았지만
조금 더 효율적이고 안전한 방법으로 구현해야 될 것 같다.
또한 카드 결제, 계좌 이체는 가능하지만 휴대폰 소액결제로 결제하는 방법은 Import에서 지원하지 않는 것 같다.

profile
백엔드 개발자를 희망하는

0개의 댓글