티스토리 뷰

이전 포스트에서 정리했던 내용을 바탕으로 진행해보도록 한다.

 

필자는 백엔드 파트를 담당하므로 결제 API만 개발하면 되지만 좀더 직관적인 개발 및 테스트를 위해 간단히 화면도 같이 개발하고자 한다. 그럼 소비자들이 결제 시 가장 먼저 만나게 될 결제창을 구현해보겠다.

결제창 연동

이부분은 바로 전 포스트에서 정리한 것과 거의 동일하다.

일반 결제 스크립트 추가

<head>
    <title>결제하기</title>
    <script src="https://js.tosspayments.com/v1"></script>
    <meta charset="utf-8">
</head>
  • 토스페이먼츠에서 결제 시스템을 호출하기 위해 제공하는 라이브러리를 추가한다.
// * 테스트용 클라이언트 키로 시작하세요
var clientKey = 'test_ck_클라이언트_키'
var tossPayments = TossPayments(clientKey)
  • 테스트를 위해 제공받은 {테스트_클라이언트_키}를 이용해서 TossPayments객체를 생성한다. 이제 tossPayments 객체를 가지고 requestPayement(), requestBillingAuth() 등을 호출하여 사용할 수 있다.

메서드 설명

requestPayement(결제수단, {결제정보 ...})

tossPayments.requestPayment('카드', {
  amount: 15000,
  orderId: 'VQLxKu8uj_RNhx_6gZPWk',
  orderName: '토스 티셔츠 외 2건',
  customerName: '박토스',
  successUrl: 'http://localhost:8080/success',
  failUrl: 'http://localhost:8080/fail',
})

: 결제창을 호출하는 메서드이다. 첫번째 파라미터는 결제수단 (카드, 토스결제, 가상계좌 등등), 두번째 파라미터에는 결제 관련 정보가 포함된다.

 

결제수단

  • 카드/CARD
  • 토스결제/TOSSPAY
  • 가상계좌/VIRTUAL_ACCOUNT
  • 계좌이체/TRANSFER
  • 휴대폰/MOBILE_PHONE
  • 문화상품권/CULTURE_GIFT_CERTIFICATE
  • 도서문화상품권/BOOK_GIFT_CERTIFICATE
  • 게임문화상품권/GAME_GIFT_CERTIFICATE

결제정보

  • amount (필수) : 결제금액
  • orderId (필수) : 서비스제공자에서 사용하는 주문에 대한 고유번호이다. ( 모든 주문마다 달라야한다!! )
  • oderName (필수) : 주문 명 (ex, 아이패드 외 2건)
  • successUrl (필수) : 성공 시 콜백 URL로 결제 성공 시 해당 주소로 리다이렉트 된다. (ex, http://www.test.com/success). 이때 최종 결제 승인에 필요한 값들이 같이 파라미터로 포함되어 온다.
  • failUrl (필수) : 실패 시 콜백 URL로 결제 실패 시 해당 주소로 리다이렉트 된다. 이때 에러코드 및 에러메시지가 파라미터로 포함되어서 간다.
  • customerName : 고객명 (1~10자)
  • customerEmail : 고객 이메일
  • taxFreeAmount (숫자) : 면세금액
  • cultureExpense (불린) : 문화비 지출 여부
  • 이 외에는 결제수단에 따라 다른 매개변수가 있다. 결제수단에 따라 다른 매개변수들은 토스페이먼츠에서 확인해보도록 하자.
    카드 : https://docs.tosspayments.com/guides/windows/card 등 그외에 확인 필요!

requestBillingAuth(결제수단, {결제정보})

tossPayments.requestBillingAuth('카드', {
  customerKey: 'aENcQAtPdYbTjGhtQnNVj',
  successUrl: 'http://localhost:8080/success',
  failUrl: 'http://localhost:8080/fail',
})

: 자동 결제 등록창을 호출하는 메서드이다. 결제 정보를 등록해놓으면 가맹점에서 원하는 시점에 자동으로 결제하는 자동 결제 연동에 사용할 수 있다.

 

결제수단

  • 카드/CARD : 자동결제를 지원하는건 카드밖에 없다.

결제정보

  • customerKey (필수) : 서비스제공자에서 사용하는 고객을 구분하는 고유 번호이다. 영문대소문자, 숫자, 특수문자를 포함해서 2~255자 이하여야 한다.
  • successUrl (필수) : 앞에서 말한 성공 시 콜백 주소로 역활은 같다.
  • failUrl (필수) : 앞에서 말한 실패 시 콜백 주소로 역활은 같다.

인증

서버에서 토스페이먼츠로 요청을 보낼 때 해줘야 하는 인증 관련 규약이 존재한다.

 

HTTP 요청헤더에 Authoriztion을 꼭꼭꼭 넣어줘야 한다. 시크릿 키를 base64로 인코딩한 값으로 넣고 이때 "{Basic }"을 (공백포함)을 같이 인코딩 값 앞에 붙여서 넣어줘야 한다.

 

Basic Authorization은 {username:password} 조합으로 이뤄지는데 토스페이먼츠는 password는 사용하지 않고 username에 시크릿키를 사용하게 된다.

 

이때 요청 시 HTTPS 프로토콜로 요청해야 한다. 인코딩은 암호화가 아니라 인코딩 방식만 바꾼것이므로 보안은 없는 상태이다. 따라서 TLS를 적용해줘야한다. 이때 1.2 버전 이상만 가능하다고 하니 참고하도록 하자.

 

HTTPS는 포트가 80이 아닌 443이므로 해당 포트를 열어두도록 하자. AWS EC2 사용시 인바운드/아웃바운드 규칙에 포트를 넣는걸 깜박하는 경우가 잦은데 잊지말자!


이제 실제 코드를 통해 토스페이먼츠의 카드 결제를 연동하여 구현해보도록 한다.

카드 결제 구현

카드결제 시 흐름도

출처 : https://docs.tosspayments.com/guides/windows/card

결제 시 위 구조로 요청과 응답이 이뤄진다.

이때 화면 역활을 하는 곳에서는 토스페이먼츠에서 발급해준 CLIENT_KEY를 가지고 TossPayments 객체를 초기화해줘야 한다. 이렇게 생성한 tossPayments를 가지고 결제창 호출을하게 된다.

결제 총 과정 정리

프론트, 백엔드, 토스페이먼츠 간 결제 과정을 다시 정리해보자.

 

프론트 \ 백엔드 \ 토스페이먼츠

 

1.  프론트는 사용자가 입력한 정보를 가지고 [결제하기] 버튼을 통해 백엔드의 결제요청 API를 호출한다.

2.  백엔드는 해당 [요청 객체]를 가지고 검증 후 필요한 값들을 채워주고 DB에 결제 정보를 저장한 후 반환한다.

3.  프론트는 반환받은 값을 가지고 이어서 tossPayments.requestPayment('카드', {결제 정보 파라미터}) 를 통해 토스 페이먼츠에 [결제창 호출]을 하게 된다.

4.  결제창에서 구매자는 일련의 절차 후 최종적으로 결제를 완료한다. 이후 토스페이먼츠는 결제의 성공여부 및 관련 파라미터를 콜백 주소로 리다이렉트한다.

5.  백엔드 서버는 성공이냐 실패냐에 따라 적절한 Controller로 응답을 받는다. 성공적으로 결제가 이뤄졌다면 백엔드에서 토스페이먼츠에게 [최종 결제 승인 요청]을 보낸다. (이때, 실패면 프론트에게 실패한 정보를 함께 담아서 반환하고 끝난다)

6.  토스페이먼츠는 해당 요청을 검증하고 정상적이라면 백엔드에 반환한다. 백엔드도 해당 데이터를 확인 후 잘 포장해서 프론트에 반환한다.

7.  프론트가 응답 데이터를 가지고 구매자에게 결제 결과 및 정보를 보여주게 된다.

백엔드 카드 결제 파트 구현

이번 포스트에서는 위에서 보여준 일련의 과정 중 1번과 2번에 해당하는 부분을 구현하도록 하겠다.

 

서버에서 결제에 필요한 정보를 입력받을 수 있도록 Controller, 전달받은 정보를 가지고 검증 및 필요한 값들을 생성하는 Service, 요청 값들을 갖고있는 DTO, 결제요청내역으로서 DB에 저장될 EntityRepository를 구현해본다.

 

결제 호출 요청 DTO : PaymentReq / 응답 DTO : PaymentRes

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class PaymentReq {
	@ApiModelProperty("지불방법")
	private PayType payType;
	@ApiModelProperty("지불금액")
	private Long amount;
	@ApiModelProperty("주문 상품 이름")
	private OrderNameType orderName;
	@ApiModelProperty("구매자 이메일")
	private String customerEmail;
	@ApiModelProperty("구매자 이름")
	private String customerName;

	public Payment toEntity() {
		return Payment.builder()
				.orderId(UUID.randomUUID().toString())
				.payType(payType)
				.amount(amount)
				.orderName(orderName)
				.customerEmail(customerEmail)
				.customerName(customerName)
				.paySuccessYn("Y")
				.createDate(new DateConfig().getNowDate())
				.build();
	}
}

---------------------------------------------------------------
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PaymentRes {
	private String payType;			// 지불방법
	private Long amount;			// 지불금액
	private String orderId;			// 주문 고유 ID
	private String orderName;		// 주문 상품 이름
	private String customerEmail;		// 구매자 이메일
	private String customerName;		// 구매자 이름
	private String successUrl;		// 성공시 콜백 주소
	private String failUrl;			// 실패시 콜백 주소
	private String createDate;		// 결제 날짜
	private String paySuccessYn;		// 결제 성공 여부
}
  • 위 값들을 프론트에서 입력받게 된다.
  • 주문 고유 번호는 유니크해야하므로 UUID로 생성했다.
  • paySuccessYn는 추후 토스페이먼츠의 결과에 따라 유지하거나 "N"으로 하는 용도이다.
  • 해당 값들 PaymentReq 객체로 받아서 서버단에서 유효성 검사를 진행하고 실제 토스페이먼츠에 결제를 요청하기 위해 필요한 값들을 포함하여 PaymentRes 객체로 반환하게 된다.

결제 Entity : Payment

@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Payment {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name = "seq", nullable = false, unique = true)
	private Long seq;

	@Column(nullable = false)
	private PayType payType;

	@Column(nullable = false)
	private Long amount;

	@Column(nullable = false)
	private String orderId;

	@Column(nullable = false)
	private OrderNameType orderName;

	@Column(nullable = false)
	private String customerEmail;

	@Column(nullable = false)
	private String customerName;

	@Column(nullable = false)
	private String createDate;

	@Setter
	@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
	private Member customer;
    
	public PaymentRes toDto() {
		return PaymentRes.builder()
				.payType(payType.name())
				.amount(amount)
				.orderId(orderId)
				.orderName(orderName.name())
				.customerEmail(customerEmail)
				.customerName(customerName)
				.createDate(createDate)
				.build();
	}
}
  • 실제 DB에 저장하게 될 결제 관련 정보들이다.
  • 회원이 여러 결제를 갖게 된다.
  • DB에 저장하고 응답결과로는 엔티티가아닌 PaymentRes객체(DTO)로 반환한다.

결제 JpaRepository

@Repository
public interface PaymentRepository extends JpaRepository<Payment, Long> {
}

결제 Controller

@PostMapping
@ApiOperation(value = "결제 요청", notes = "결제 요청에 필요한 값들을 반환합니다.")
public SingleResult<PaymentRes> requestPayments(
        @ApiParam(value = "요청 객체", required = true) @ModelAttribute PaymentReq paymentReq
) {
    try {
        return responseService.getSingleResult(
                paymentService.requestPayments(paymentReq)
        );
    } catch (Exception e) {
        e.printStackTrace();
        throw new BussinessException(e.getMessage());
    }
}
  • 프론트에서 결제를 요청하기 위해 1차적으로 요청하는 API이다.
  • 이곳에서 먼저 요청값들에 대한 검증이 이뤄지고 성공적이라면 -> 토스페이먼츠로 실제 결제 요청을 보내게 된다.

결제 Service

@Slf4j
@Service
@RequiredArgsConstructor
public class PaymentService {

	private final PaymentRepository paymentRepository;
	private final MemberRepository memberRepository;

	@Value("${payments.toss.test_client_api_key}")
	private String testClientApiKey;

	@Value("${payments.toss.test_secret_api_key}")
	private String testSecretApiKey;

	@Value("${payments.toss.live_client_api_key}")
	private String liveClientApiKey;

	@Value("${payments.toss.live_secret_api_key}")
	private String liveSecretApiKey;

	@Value("${payments.toss.success_url}")
	private String successCallBackUrl;

	@Value("${payments.toss.fail_url}")
	private String failCallBackUrl;

	@Transactional
	public PaymentRes requestPayments(PaymentReq paymentReq) {
		Long amount = paymentReq.getAmount();
		String payType = paymentReq.getPayType().name();
		String customerEmail = paymentReq.getCustomerEmail();
		String orderName = paymentReq.getOrderName().name();

		if (amount == null || amount != 3000) {
			throw new BussinessException(ExMessage.PAYMENT_ERROR_ORDER_PRICE);
		}

		if (!payType.equals("CARD") && !payType.equals("카드")) {
			throw new BussinessException(ExMessage.PAYMENT_ERROR_ORDER_PAY_TYPE);
		}

		if (!orderName.equals(OrderNameType.상품명1.name()) &&
				!orderName.equals(OrderNameType.상품명2.name())) {
			throw new BussinessException(ExMessage.PAYMENT_ERROR_ORDER_NAME);
		}

		PaymentRes paymentRes;
		try {
			Payment payment = paymentReq.toEntity();
			memberRepository.findByEmailFJ(customerEmail)
					.ifPresentOrElse(
							M -> M.addPayment(payment)
							, () -> {
								throw new BussinessException(ExMessage.MEMBER_ERROR_NOT_FOUND);
							}
					);
			paymentRes = payment.toDto();
			paymentRes.setSuccessUrl(successCallBackUrl);
			paymentRes.setFailUrl(failCallBackUrl);
			return paymentRes;
		} catch (Exception e) {
			throw new BussinessException(ExMessage.DB_ERROR_SAVE);
		}
	}
}
  • 결제를 위해 요청한 값들을 전달받아서 검증을 하게 된다.
  • 조건이 맞는지 확인하고, 모든 조건들이 맞다면 Entity 객체로 만든 후 필요한 값들을 세팅해주고 반환해준다.

테스트

요청 및 응답

이런식으로 적절한 값을 받고 요청을 보내고 응답과 DB에도 값이 잘 들어간것 까지 확인할 수 있다.

 

이렇게 1차적으로 프론트에서 결제를 요청하고 백엔드에서 해당 데이터를 검증 후 결제 요청 객체를 반환해주는 파트가완성됐다. 이제 남은 부분은

  • 프론트에서 해당 응답을 가지고 토스페이먼츠에서 제공하는 메서드를 이용해서 결제창을 띄우는 부분
  • 해당 결제창 과정 후 토스페이먼츠에서 전달하는 콜백 요청
  • 콜백 요청에 맞게 백엔드에서 적절한 조치
  • 정상적인 결제가 되었다면 [최종 결제 요청] 후 [최종 결제 정보]를 프론트에 반환

이 남았다.

 

다음 포스트에서 이어서 해보도록 하겠다.

 

 


Reference

https://docs.tosspayments.com/guides/windows/card

반응형
Comments
반응형
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday