Article - 깊게 탐구하기/개인 프로젝트

토스 결제 API를 이용한 결제 모듈 구현 프로젝트 (FE v2/BE v1 기준)

조금씩 차근차근 2026. 3. 14. 19:00

최신 버전은 아래 구조와 다를 수 있습니다. 공식 문서를 참고하여, 버전이 맞는 지 확인해 주시기 바랍니다.

 

시작하기 | 토스페이먼츠 개발자센터

토스페이먼츠 결제 연동하는 클라이언트, 서버 개발자가 꼭 읽어야할 문서를 추천해드려요. 빠르게 개발을 시작해보세요.

docs.tosspayments.com

 

결제 이해하기

토스 결제 API를 사용하기 위한 준비

  1. 다음 링크에서 회원가입을 진행해주자.
  2. 회원가입 후, 우측 상단의 "내 개발정보"에서 API 키를 확인한다.

원래는 이 키를 사용해야 함이 맞지만, 여기에 존재하는 "테스트 키"는 사업자 등록을 완료해야 발급받을 수 있다.

따라서, 토스페이먼츠에서 따로 제공하는 테스트용 클라이언트 키와 시크릿 키를 확인하자.

출처는 이곳에서 확인할 수 있다.

 

회원가입, 사업자번호 없이 결제 테스트하기 | 토스페이먼츠 개발자센터

오늘은 계약 전에 토스페이먼츠의 테스트 환경에서 온라인 결제를 연동하고 시뮬레이션하는 방법을 쉽고 간단하게 소개할게요.

docs.tosspayments.com

 

Use Case

최상단에서 언급한 결제 흐름 이해하기 게시글을 참고하여, 결제 모듈의 핵심 Use Case를 도출해보자.

  1. 주문 생성
    a. 구매자가 상품을 선택하고, 주문서를 작성한다.
    b. 주문서에는 상품 정보, 결제 금액, 배송 주소 등이 포함된다.
    c. 주문 생성이 완료되면, 주문 번호(orderId) 및 최종적으로 검증해야 할 정보들이 발급된다.
  2. 결제 요청
    a. 구매자(우리 서비스의 고객)가 주문서의 상품 정보, 결제 금액을 확인하고 '결제하기' 버튼을 클릭한다.
    b. 프론트는 사용자의 '결제하기' 버튼의 클릭 이벤트로 토스페이먼츠 SDK의 결제 요청 메서드(requestPayment())를 호출한다. 결제 요청 메서드는 구매자가 선택한 결제수단의 결제창을 연다.
    c. 우리의 서비스는 해당 결제를 구분하기 위한 결제 요청 메서드의 파라미터로 주문번호(orderId), 성공 및 실패 URL(successUrl, failUrl)을 정의할 수 있다.
  3. 구매자 정보 인증
    a. 결제창에 카드 번호, 만료일, CVC 등 결제 정보를 직접 입력하거나 앱카드, 간편결제 앱으로 결제 정보를 불러온다.
    b. 카드사는 이 정보를 바탕으로, 구매자를 카드 소유자로 인증한다.
  4. 인증 결과 확인
    a. 토스 페이먼츠는 결제 기록을 생성하고, 결제 키(paymentKey)를 발급한다. 이는 각 결제에 고유한 값이며, 자동으로 발급한다.
    b. 토스 페이먼츠는 사전에 우리 서비스로부터 제공받은 성공 URL로 리다이렉트한다. 이때, 결제 키(paymentKey), 주문 번호(orderId) 등을 쿼리 파라미터로 전달한다.
  5. 결제 승인
    a. 백엔드는 성공 URL로 전달받은 쿼리 파라미터 값이 결제 요청에 보낸 값과 동일한지 확인한다.
    b. 백엔드는 결제 승인 API를 호출하여, 카드사로 결제 승인 요청을 보낸다.
    c. 카드사는 결제 금액을 구매자의 카드 한도 또는 은행 계좌에서 차감한다.
    d. 카드사는 최종 결제 결과를 백엔드로 전달한다. 결제 승인 API는 결제 승인 결과를 반환한다.
    e. paymentKey, orderId는 서버에 필수로 저장한다. 이는 결제 조회, 결제 취소에 사용되는 값이다.

왜 요청과 승인을 분리하는가?

사실 내가 기존에 알던 정보로는 웹훅을 이용해 결제 승인 결과를 전달받는 방식이었는데,

이제 토스페이먼츠는 결제 요청과 승인을 분리하여 처리하는 방식을 사용한다.

 

토스페이먼트에선 이렇게 요청과 승인을 두 번의 요청-응답 모델로 바꾼 이유를 설명한다.

  • 사용자가 결제를 중단하거나,
  • 서버에 트래픽이 몰려서 웹훅을 받지 못했다면

상점의 시스템과 토스페이먼츠 결제 결과와 불일치 데이터가 생길 수 있다.

즉, 하나의 트랜잭션 내 책임을 분리하여 요청을 단순화한다.
기존의 방식에서는 요청-응답 사이에 한번의 요청-응답이 깊게 들어가는 형태였는데, 이는 확실히 비직관적이고 복잡한 흐름이었다.

 

그래서 토스 페이먼츠는 결제 요청과 승인을 분리하여, 결제 승인 API를 통해 최종 결제 결과를 확인하는 방식을 사용한다.

인증 결과 확인 - 승인할 데이터 검증

위 Use Case에서 언급된 "인증 결과 확인" 단계에서, 백엔드는 성공 URL로 전달받은 쿼리 파라미터 값이 결제 요청에 보낸 값과 동일한지 확인해야 한다.

이때 검증해야 하는 과정 또한, 토스 페이먼츠 공식 문서에서 명확히 언급한다.

 

이 과정은 클라이언트에서 결제 금액을 조작해 승인하는 행위를 방지할 수 있다.

 

 

  1. 결제 인증이 완료되면 성공 URL에 들어온 값(paymentKey, orderId, amount)을 확인한다.
  2. orderId로 결제 요청 전에 저장해 둔 임시 정보를 불러온다.
  3. 적립금 및 쿠폰을 사용할 수 있는지, 적립금과 쿠폰을 적용한 최종 결제 금액이 토스페이먼츠에서 돌아온 성공 리다이렉트 URL을 통해 받은 금액과 같은지 확인해본다.
  4. 문제가 없다면 돌아온 데이터를 사용해서 결제 승인을 요청한다.

 

분리된 주문 모듈과 결제 모듈 - 어떻게 검증할 것인가?

현재 주문 모듈과 결제 모듈은 분리되어 있다고 가정했다.

그렇다면, 그 둘이 한 트랜잭션으로 묶이지 않는 상황이라면, 락을 통해서도 주문의 변조를 막을 수 없다.

이를 시퀀스 다이어그램으로 표현해보자.

  1. 악성 프로그램이 사용자가 결제 완료 버튼을 누르는 순간
  2. 주문 모듈에 추가적으로 요청을 보내서 주문의 상태를 변경하고
  3. 결제 모듈은 사용자가 보았던 주문 금액이 아닌, 해당 변경된 주문 데이터를 바탕으로 결제를 진행할 수 있다.
  4. 악성 프로그램은 그렇게 결제된 응답 데이터를 다시 기존 데이터로 사용자에게 바꿔 보여주는 것 또한 가능하다.

확정된 주문은 더이상 수정되지 않아야 한다.

즉, 검증 시점에는 "결제 완료 시점의 스냅샷"을 보아야 한다.

이를 어떻게 구현할 수 있을까?

  1. 결제 모듈은 requestPayment()으로 주문 정보를 받을 때, 자신의 DB에 따로 주문 스냅샷을 저장한다. (예: orderId, amount, version)
    • 결제 모듈에 주문 데이터가 중복 저장되는 형태이다.
    • 대신, 주문 모듈과의 통신 횟수는 줄어드는 효과를 가질 수 있다.
  2. requestPayment()호출 이전에, 더이상 주문 모듈에서 주문이 변경되지 않도록, 주문 모듈의 주문 데이터를 잠근다. (예: status=PENDING_PAYMENT)
    • 결제 모듈이 주문 데이터를 따로 저장할 필요가 없어진다.
    • 대신, 주문 모듈과 결제 모듈 간의 통신이 결제 승인 요청 시점에 1회 더 발생하는 형태가 된다.

여기서 현재 주문과 결제 데이터는 1:1 관계로 되어 있다.

따라서, 주문 데이터를 상태 전이로 잠그게 되면, 결제 모듈도 자연스럽게 잠긴 주문의 상태를 보게 된다.

이 경우, 불필요하게 결제 모듈에 주문 데이터의 스냅샷을 저장하지 않아도 된다.

그런데, 이 방식 괜찮나?

이렇게 글을 쓰면서, AI와 설계 모델을 토론했는데, AI는 "주문 모듈의 주문 데이터를 1:1 관계로 잠그는 방식은, 잠재적으로 주문 모듈이 결제 모듈에 강하게 의존하게 되는 형태가 될 수 있다"고 지적했다.

그 근거는 다음과 같았다.

  • 주문은 "비즈니스 모델"이고, 결제는 "트랜잭션 모델"이다.
  • 만약 사용자가 실제로 주문한 금액을 자주 확인해야 하는 상황이라면, 매 결제 시마다 주문이 새로 생성되는 형태는 주문 애그리거트의 존재 의미와 충돌할 수 있다.

그리하여 AI가 제안한 도메인 모델은 위 선택지의 1번 형태로, 다음과 같이 주문 스냅샷을 결제 모듈에 저장하는 형태였다.

하지만, 내가 생각한 주문 애그리거트의 존재 의미는 다음과 같았다.

  • 사용할 쿠폰 지정
  • 배송지 지정
  • 적립금 사용 여부 지정

즉, Pending Payment 이전 상태에서, 주문의 변경이 가능한 상태의 상태 전이 모델이 존재하는 것이 주문 애그리거트의 존재 의미라고 생각했다.

Pending Payment 이후, 주문이 결제가 실패했을 시, 다시 이전의 주문 정보를 수정하는 형태는 Order 애그리거트의 존재 의미와 충돌하지 않는다고 생각했다.

과연, 어떤 방식이 더 나은가?

  1. 주문 모듈의 요구사항
    • 상품 재고를 잠근 상태로 유지
    • 결제 승인 시점에 주문 금액이 변경되지 않았는지 검증
  2. 결제 모듈의 요구사항
    • 주문서에 적힌 금액으로 결제 진행

주문, 결제, 상품 간의 핵심적인 데이터 일관성 요구사항은 다음과 같다.

  • 재고 선점을 어떻게 할 것인가?
  • 상품 강제 품절 처리는 처리되지 않은 주문을 어떻게 처리할 것인가?
  • 주문 데이터는 어떻게 수정할 것인가?
  • 결제 모듈은 주문 데이터를 어떻게 검증할 것인가?

결국 선택지는 다음과 같이 요약될 수 있다.

  • 주문 모듈 결제 실패 시 상태 롤백 가능(기존 주문 사용) + 재고 선점 유지 + 결제 모듈에서 주문 스냅샷 저장
    • 재고 반환 후 재선점이 필요 없어진다.
    • 주문 모듈에서 결제 실패 시, 기존 주문을 재사용하는 형태가 된다.
    • 결제 모듈에서 주문 데이터의 스냅샷을 저장하므로, 자체적인 결제+주문 정보를 관리할 수 있다.
    • 강제 상품 품절 처리 시, 결제 모듈에 있는 결제를 막을 수 없다.
  • 주문 모듈 결제 실패 시 롤백 불가능(새 주문 생성) + 재고 반환 후 새 주문이 선점 + 결제 모듈에서 주문 모듈 데이터 확인
    • 재고 반환 후 재선점을 수행한다.
    • 주문 모듈에서 결제 실패 시, 주문 재생성 로직이 좀 더 복잡하다.
    • 결제 모듈에서 주문 데이터의 스냅샷을 저장하지 않으므로, 주문 모듈의 주문 데이터 누적이 결제 모듈에 의존한다.
    • 강제 상품 품절 처리 시, 결제 모듈이 최신 주문 상태를 확인하므로, 결제 모듈에 있는 결제를 막을 수 있다.

사용자가 결제 실패 시 주문을 재생성하게 되면, 재고 선점이 풀렸다가 다시 선점되는 형태가 될 수 있다.

이 경우, 사용자는 한번에 결제를 성공하지 못해서 원하던 상품의 재고를 놓치는 불쾌한 경험을 할 수 있다.

UX적인 측면에서, 이는 분명히 사용자에게 좋지 않은 경험이 될 수 있다.

그래서, 주문 모듈에서 결제 실패 시, 기존 주문을 재사용하는 형태를 구현하기로 결정했다.


구현 결과는 다음과 같다.

 

GitHub - GoGradually/toss-payments-sample: 토스 결제 API 체험

토스 결제 API 체험. Contribute to GoGradually/toss-payments-sample development by creating an account on GitHub.

github.com

 

주문 모듈은 배제하고, 스냅샷을 이용해 모듈화를 진행한 형태로 토스 페이먼츠 결제 모듈을 연동한 프로젝트이다.