Saleor Core: Payment & Checkout 모듈 분석
한국 PG 연동(토스페이먼츠, 나이스페이 등)을 위한 포크 기반 통합 가이드
1. 아키텍처 개요
Saleor는 두 가지 결제 흐름을 동시에 지원한다:
| 흐름 | 모델 | 용도 | 한국 PG 권장 |
|---|---|---|---|
| Payment Flow (레거시) | Payment + Transaction | 빌트인 게이트웨이 플러그인 (Stripe, Dummy 등) | X |
| Transaction Flow (신규) | TransactionItem + TransactionEvent | 외부 앱 기반 결제 처리, Saleor App으로 연동 | O |
채널 설정의 order_mark_as_paid_strategy가 TRANSACTION_FLOW이면 Transaction Flow, 아니면 Payment Flow를 사용한다.
권장: 한국 PG 연동은 Transaction Flow를 사용하는 것이 적합하다. Payment Flow는 deprecated 경향이 있고, Transaction Flow가 비동기 webhook 기반 처리에 더 적합하다.
2. Payment 모듈 상세 분석
2.1 핵심 상수/열거형 (saleor/payment/__init__.py)
# TransactionKind (Payment Flow에서 사용)
class TransactionKind:
EXTERNAL = "external" # 외부 참조
AUTH = "auth" # 인가 (금액 홀드)
CAPTURE = "capture" # 캡처 (실제 청구)
VOID = "void" # 인가 취소
REFUND = "refund" # 환불
CONFIRM = "confirm" # 확인 (3DS 등)
PENDING = "pending" # 대기
CANCEL = "cancel" # 취소
# TransactionAction (Transaction Flow에서 사용)
class TransactionAction:
CHARGE = "charge" # 청구
REFUND = "refund" # 환불
CANCEL = "cancel" # 취소
# ChargeStatus
class ChargeStatus:
NOT_CHARGED = "not-charged"
PENDING = "pending"
PARTIALLY_CHARGED = "partially-charged"
FULLY_CHARGED = "fully-charged"
PARTIALLY_REFUNDED = "partially-refunded"
FULLY_REFUNDED = "fully-refunded"
REFUSED = "refused"
CANCELLED = "cancelled"
# TransactionEventType (Transaction Flow 이벤트)
class TransactionEventType:
AUTHORIZATION_SUCCESS / FAILURE / ADJUSTMENT / REQUEST
CHARGE_SUCCESS / FAILURE / BACK / REQUEST
REFUND_SUCCESS / FAILURE / REVERSE / REQUEST
CANCEL_SUCCESS / FAILURE / REQUEST
INFO2.2 모델: Payment (레거시) — saleor/payment/models.py:273-459
Payment Flow에서 사용하는 결제 정보 모델.
| 필드 | 타입 | 설명 |
|---|---|---|
gateway | CharField(255) | 게이트웨이 식별자 (예: mirumee.payments.stripe) |
is_active | BooleanField | 활성 여부 |
to_confirm | BooleanField | 3DS 등 추가 인증 필요 여부 |
charge_status | CharField(20) | ChargeStatus 값 |
token | CharField(512) | PG사 토큰 |
total | DecimalField | 결제 총액 |
captured_amount | DecimalField | 캡처된 금액 |
currency | CharField | 통화 코드 |
checkout | FK → Checkout | null 가능, SET_NULL |
order | FK → Order | null 가능, PROTECT |
store_payment_method | CharField(11) | ON_SESSION / OFF_SESSION / NONE |
billing_* | 다수 | 청구 주소 비정규화 필드들 |
cc_* | 다수 | 카드 정보 (first_digits, last_digits, brand, exp) |
psp_reference | CharField(512) | PG사 참조 ID |
return_url | URLField | 결제 완료 후 리다이렉트 URL |
extra_data | TextField | 추가 데이터 (JSON 문자열) |
주요 메서드:
can_authorize()— 인가 가능 여부 (is_active and not_charged)can_capture()— 캡처 가능 여부can_void()— 인가 취소 가능 여부can_refund()— 환불 가능 여부 (PARTIALLY/FULLY_CHARGED, PARTIALLY_REFUNDED)get_authorized_amount()— 인가 금액 계산 (트랜잭션 기반)
2.3 모델: Transaction (레거시) — saleor/payment/models.py:461-509
Payment의 개별 결제 작업을 나타내는 모델.
| 필드 | 타입 | 설명 |
|---|---|---|
payment | FK → Payment | PROTECT |
token | CharField(512) | 작업 토큰 |
kind | CharField(25) | TransactionKind 값 |
is_success | BooleanField | 성공 여부 |
action_required | BooleanField | 추가 액션 필요 (3DS 등) |
action_required_data | JSONField | 추가 액션에 필요한 데이터 |
amount | DecimalField | 금액 |
error | TextField | 에러 메시지 |
gateway_response | JSONField | PG사 원시 응답 (deprecated) |
2.4 모델: TransactionItem (신규) — saleor/payment/models.py:31-203
Transaction Flow의 핵심 모델. 결제 상태를 이벤트 기반으로 추적한다.
| 필드 | 타입 | 설명 |
|---|---|---|
token | UUIDField(unique) | 고유 토큰 |
psp_reference | CharField(512) | PG사 참조 ID — 한국 PG 연동 시 핵심 |
name | CharField(512) | 결제 수단 이름 |
message | CharField(512) | 상태 메시지 |
available_actions | ArrayField | 가능한 액션 목록 (charge, refund, cancel) |
currency | CharField | 통화 |
authorized_value | Decimal | 인가 금액 |
charged_value | Decimal | 청구 금액 |
refunded_value | Decimal | 환불 금액 |
canceled_value | Decimal | 취소 금액 |
authorize_pending_value | Decimal | 인가 보류 금액 |
charge_pending_value | Decimal | 청구 보류 금액 |
refund_pending_value | Decimal | 환불 보류 금액 |
cancel_pending_value | Decimal | 취소 보류 금액 |
checkout | FK → Checkout | SET_NULL |
order | FK → Order | PROTECT |
app | FK → App | 결제 앱 |
app_identifier | CharField(256) | 앱 식별자 (재설치 시 매칭용) |
last_refund_success | BooleanField | 마지막 환불 성공 여부 |
cc_* / payment_method_* | 다수 | 카드/결제수단 정보 |
external_url | URLField | PG사 관리 페이지 URL |
제약 조건:
(app_identifier, idempotency_key)UNIQUE — 멱등성 보장
2.5 모델: TransactionEvent — saleor/payment/models.py:206-270
TransactionItem의 이벤트 로그. 모든 결제 상태 변경이 이벤트로 기록된다.
| 필드 | 타입 | 설명 |
|---|---|---|
transaction | FK → TransactionItem | CASCADE |
type | CharField(128) | TransactionEventType 값 |
amount_value | Decimal | 이벤트 금액 |
psp_reference | CharField(512) | PG사 참조 ID (이벤트별) |
include_in_calculations | BooleanField | 금액 계산 포함 여부 |
related_granted_refund | FK → OrderGrantedRefund | 환불 승인 연관 |
app / app_identifier | 이벤트 생성 앱 | |
user | FK → User | 이벤트 트리거 사용자 |
제약 조건:
(transaction_id, idempotency_key)UNIQUE
2.6 모델 관계도
erDiagram Checkout ||--o{ Payment : "payments (레거시)" Checkout ||--o{ TransactionItem : "payment_transactions (신규)" Payment ||--o{ Transaction : "transactions" TransactionItem ||--o{ TransactionEvent : "events" Order ||--o{ Payment : "payments" Order ||--o{ TransactionItem : "payment_transactions" TransactionItem }o--|| App : "app" TransactionEvent }o--|| App : "app" TransactionEvent }o--o| OrderGrantedRefund : "related_granted_refund" Payment }o--o| Checkout : "checkout" Payment }o--o| Order : "order" TransactionItem }o--o| Checkout : "checkout" TransactionItem }o--o| Order : "order" Checkout }o--|| Channel : "channel" Checkout { UUID token PK FK user FK channel FK billing_address FK shipping_address Decimal total_gross_amount string authorize_status string charge_status } Payment { string gateway boolean is_active string charge_status Decimal total Decimal captured_amount string token } Transaction { FK payment string kind boolean is_success Decimal amount } TransactionItem { UUID token string psp_reference Decimal authorized_value Decimal charged_value Decimal refunded_value Decimal canceled_value FK app string app_identifier } TransactionEvent { FK transaction string type Decimal amount_value string psp_reference boolean include_in_calculations }
3. Gateway Interface — saleor/payment/interface.py
3.1 핵심 데이터 클래스
PaymentData (L377-417) — Payment Flow 게이트웨이에 전달되는 결제 정보
@dataclass
class PaymentData:
gateway: str # 게이트웨이 식별자
amount: Decimal # 결제 금액
currency: str # 통화 코드
billing: AddressData | None
shipping: AddressData | None
payment_id: int # Payment 모델 ID
order_id: str | None
customer_ip_address: str | None
customer_email: str
token: str | None # PG사 토큰
checkout_token: str | None
psp_reference: str | None
store_payment_method: StorePaymentMethodEnum
refund_data: RefundData | None
transactions: list[TransactionData]GatewayResponse (L303-326) — 게이트웨이 응답 통합 포맷
@dataclass
class GatewayResponse:
is_success: bool # 성공 여부
action_required: bool # 추가 인증 필요 (3DS, 리다이렉트 등)
kind: str # TransactionKind 값
amount: Decimal
currency: str
transaction_id: str # PG사 트랜잭션 ID
error: str | None
payment_method_info: PaymentMethodInfo | None
action_required_data: JSONType | None # 리다이렉트 URL 등
psp_reference: str | NoneGatewayConfig (L428-443) — 게이트웨이 설정
@dataclass
class GatewayConfig:
gateway_name: str
auto_capture: bool # 자동 캡처 여부
supported_currencies: str # 지원 통화 (쉼표 구분)
connection_params: dict # API 키 등 연결 파라미터
store_customer: bool
require_3d_secure: boolTransactionSessionData (L183-189) — Transaction Flow 세션 데이터
@dataclass
class TransactionSessionData:
transaction: TransactionItem
source_object: Checkout | Order # 체크아웃 또는 주문
action: TransactionProcessActionData # action_type, amount, currency
payment_gateway_data: PaymentGatewayData
customer_ip_address: str | None
idempotency_key: str | NoneTransactionActionData (L112-118) — Transaction 액션 요청 데이터
@dataclass
class TransactionActionData:
action_type: str # "charge", "refund", "cancel"
transaction: TransactionItem
event: TransactionEvent
transaction_app_owner: App | None
action_value: Decimal
granted_refund: OrderGrantedRefund | None4. 게이트웨이 구현 패턴
4.1 빌트인 게이트웨이 목록 (saleor/payment/gateways/)
| 디렉토리 | 설명 |
|---|---|
dummy/ | 테스트용 더미 게이트웨이 |
dummy_credit_card/ | 테스트용 신용카드 더미 |
stripe/ | Stripe 연동 (레거시 Payment Flow) |
braintree/ | Braintree 연동 |
razorpay/ | Razorpay 연동 |
authorize_net/ | Authorize.net 연동 |
4.2 Dummy 게이트웨이 구현 패턴 (saleor/payment/gateways/dummy/__init__.py)
각 게이트웨이는 표준 함수를 구현해야 한다:
# 필수 구현 함수 시그니처
def authorize(payment_information: PaymentData, config: GatewayConfig) -> GatewayResponse:
"""인가 — 금액을 홀드한다."""
return GatewayResponse(
is_success=True,
action_required=False,
kind=TransactionKind.AUTH,
amount=payment_information.amount,
currency=payment_information.currency,
transaction_id=payment_information.token,
error=None,
payment_method_info=PaymentMethodInfo(...),
)
def capture(payment_information: PaymentData, config: GatewayConfig) -> GatewayResponse:
"""캡처 — 홀드된 금액을 실제 청구한다."""
def void(payment_information: PaymentData, config: GatewayConfig) -> GatewayResponse:
"""취소 — 인가를 취소한다."""
def refund(payment_information: PaymentData, config: GatewayConfig) -> GatewayResponse:
"""환불 — 청구된 금액을 환불한다."""
def confirm(payment_information: PaymentData, config: GatewayConfig) -> GatewayResponse:
"""확인 — 3DS 등 추가 인증 후 결제를 확정한다."""
def process_payment(payment_information: PaymentData, config: GatewayConfig) -> GatewayResponse:
"""단일 호출로 결제 처리 (authorize → capture 일괄)."""
def get_client_token(**_) -> str:
"""클라이언트 사이드 토큰 생성."""4.3 플러그인 등록 (saleor/payment/gateways/dummy/plugin.py)
class DeprecatedDummyGatewayPlugin(BasePlugin):
PLUGIN_ID = "mirumee.payments.dummy"
PLUGIN_NAME = "Dummy"
DEFAULT_CONFIGURATION = [
{"name": "Store customers card", "value": False},
{"name": "Automatic payment capture", "value": True},
{"name": "Supported currencies", "value": "USD, PLN"},
]
def authorize_payment(self, payment_information, previous_value) -> GatewayResponse:
return authorize(payment_information, self._get_gateway_config())
def capture_payment(self, payment_information, previous_value) -> GatewayResponse:
return capture(payment_information, self._get_gateway_config())
# process_payment, refund_payment, void_payment, confirm_payment 등4.4 Stripe 웹훅 처리 (saleor/payment/gateways/stripe/webhooks.py)
Stripe 웹훅 핸들러는 다음 이벤트를 처리한다:
WEBHOOK_SUCCESS_EVENT→ charge 성공WEBHOOK_AUTHORIZED_EVENT→ 인가 성공WEBHOOK_CANCELED_EVENT→ 취소WEBHOOK_FAILED_EVENT→ 실패WEBHOOK_REFUND_EVENT→ 환불WEBHOOK_PROCESSING_EVENT→ 처리 중
5. Transaction 금액 재계산 (saleor/payment/transaction_item_calculations.py)
Transaction Flow에서 TransactionItem의 금액은 TransactionEvent 이벤트들을 기반으로 재계산된다.
5.1 계산 로직
def recalculate_transaction_amounts(transaction: TransactionItem, save=True):
"""이벤트 기반 금액 재계산.
1. 모든 이벤트를 created_at 순으로 조회
2. psp_reference 기준으로 그룹핑
3. 각 액션 타입별 (auth/charge/refund/cancel) 금액 계산
4. pending 금액: request만 있고 success/failure 없을 때
5. 확정 금액: success 이벤트 기준 (failure보다 최신일 때만)
"""5.2 이벤트-금액 매핑
AUTHORIZATION_REQUEST → authorize_pending_value ↑
AUTHORIZATION_SUCCESS → authorized_value ↑
CHARGE_REQUEST → charge_pending_value ↑, authorized_value ↓
CHARGE_SUCCESS → charged_value ↑, authorized_value ↓
CHARGE_BACK → charged_value ↓
REFUND_REQUEST → refund_pending_value ↑
REFUND_SUCCESS → refunded_value ↑, charged_value ↓
REFUND_REVERSE → refunded_value ↓, charged_value ↑
CANCEL_REQUEST → cancel_pending_value ↑
CANCEL_SUCCESS → canceled_value ↑, authorized_value ↓
6. Checkout 모듈 상세 분석
6.1 모델: Checkout — saleor/checkout/models.py:110-432
| 필드 | 타입 | 설명 |
|---|---|---|
token | UUIDField (PK) | 체크아웃 고유 식별자 |
user | FK → User | 로그인 사용자 |
channel | FK → Channel | 판매 채널 |
email | EmailField | 비회원 이메일 |
currency | CharField | 통화 |
country | CountryField | 국가 코드 |
billing_address | FK → Address | 청구 주소 |
shipping_address | FK → Address | 배송 주소 |
shipping_method | FK → ShippingMethod | 배송 방법 |
total_gross_amount | Decimal | 총액 (세금 포함) |
total_net_amount | Decimal | 총액 (세금 미포함) |
subtotal_* | Decimal | 소계 |
shipping_price_* | Decimal | 배송비 |
discount_amount | Decimal | 할인 금액 |
authorize_status | CharField(32) | NONE / PARTIAL / FULL |
charge_status | CharField(32) | NONE / PARTIAL / FULL / OVERCHARGED |
completing_started_at | DateTime | 완료 프로세스 시작 시간 (락 용도) |
voucher_code | CharField | 적용된 바우처 코드 |
gift_cards | M2M → GiftCard | 적용된 기프트카드 |
redirect_url | URLField | 완료 후 리다이렉트 URL |
language_code | CharField | 언어 코드 |
tax_exemption | BooleanField | 면세 여부 |
price_expiration | DateTime | 가격 캐시 만료 시간 |
last_transaction_modified_at | DateTime | 최신 TransactionItem 수정 시간 |
automatically_refundable | BooleanField | 자동 환불 가능 여부 |
핵심 메서드:
is_checkout_locked()— 완료 프로세스 진행 중인지 확인 (completing_started_at기반)get_last_active_payment()— 마지막 활성 Payment 반환get_total_gift_cards_balance()— 기프트카드 잔액 합산safe_update()—SELECT FOR UPDATE기반 안전한 업데이트
6.2 모델: CheckoutLine — saleor/checkout/models.py:434-531
| 필드 | 타입 | 설명 |
|---|---|---|
id | UUIDField (PK) | 라인 ID |
checkout | FK → Checkout | CASCADE |
variant | FK → ProductVariant | 상품 변형 |
quantity | PositiveIntegerField | 수량 |
price_override | DecimalField | 가격 오버라이드 |
undiscounted_unit_price_amount | Decimal | 할인 전 단가 |
total_price_gross_amount | Decimal | 총 가격 (세금 포함) |
tax_rate | Decimal(5,4) | 세율 |
6.3 모델: CheckoutDelivery — saleor/checkout/models.py:38-108
배송 방법 캐시 모델. 외부 배송 앱의 응답을 저장한다.
6.4 모델: CheckoutMetadata — saleor/checkout/models.py:535-538
체크아웃 메타데이터를 분리 저장. SELECT FOR UPDATE 중에도 메타데이터 접근 가능.
7. Checkout → Order 전환 흐름
7.1 진입점: complete_checkout() — saleor/checkout/complete_checkout.py:1695
def complete_checkout(manager, checkout_info, lines, payment_data, ...):
# 1. 체크아웃 데이터 fetch (세금 계산 포함)
fetch_checkout_data(checkout_info, manager, lines)
# 2. 흐름 결정
active_payment = checkout.get_last_active_payment()
is_fully_authorized = checkout.authorize_status == "full"
if is_fully_authorized or (transactions and not active_payment):
# Transaction Flow
return complete_checkout_with_transaction(...)
else:
# Payment Flow (레거시)
return complete_checkout_with_payment(...)7.2 시퀀스 다이어그램: Transaction Flow
sequenceDiagram participant Client as 프론트엔드 participant GQL as GraphQL API participant CC as complete_checkout participant TF as Transaction Flow participant PG as 결제 앱 (한국 PG App) participant DB as Database Client->>GQL: checkoutPaymentCreate / transactionInitialize (GraphQL) GQL->>PG: 결제 세션 생성 요청 PG-->>GQL: 결제 URL / 세션 토큰 Note over Client,PG: 고객이 PG 결제 페이지에서 결제 완료 PG->>GQL: Webhook: TRANSACTION_CHARGE_REQUESTED 또는 직접 보고 GQL->>DB: TransactionEvent(AUTHORIZATION_SUCCESS) 생성 GQL->>DB: TransactionItem 금액 재계산 GQL->>DB: Checkout authorize_status = FULL 업데이트 Client->>GQL: checkoutComplete GQL->>CC: complete_checkout() CC->>CC: authorize_status == FULL 확인 CC->>TF: complete_checkout_with_transaction() TF->>TF: _prepare_checkout_with_transactions() Note over TF: billing_address 검증<br/>authorize_status == FULL 확인<br/>voucher 유효성 확인 TF->>TF: create_order_from_checkout() TF->>DB: Order 생성 TF->>DB: OrderLine 생성 (bulk_create) TF->>DB: 재고 할당 (allocate_stocks) TF->>DB: Payment → Order 연결 (checkout.payments.update) TF->>DB: Checkout 삭제 TF-->>Client: Order 반환
7.3 시퀀스 다이어그램: Payment Flow (레거시)
sequenceDiagram participant Client as 프론트엔드 participant GQL as GraphQL API participant CC as complete_checkout participant PF as Payment Flow participant GW as Gateway Plugin participant PG as PG사 API participant DB as Database Client->>GQL: checkoutPaymentCreate GQL->>DB: Payment 생성 Client->>GQL: checkoutComplete GQL->>CC: complete_checkout() CC->>PF: complete_checkout_with_payment() PF->>DB: Checkout SELECT FOR UPDATE (동시성 제어) PF->>PF: complete_checkout_pre_payment_part() Note over PF: 배송/주소 검증<br/>주문 데이터 준비<br/>재고 확인 PF->>PF: _process_payment() PF->>GW: gateway.process_payment() 또는 gateway.confirm() GW->>PG: PG사 API 호출 PG-->>GW: 응답 GW-->>PF: GatewayResponse alt 추가 인증 필요 (3DS) PF-->>Client: action_required=True, action_data (리다이렉트 URL) Client->>PG: 3DS 인증 수행 PG-->>GQL: Webhook 콜백 else 결제 성공 PF->>PF: complete_checkout_post_payment_part() PF->>DB: _create_order() — Order 생성 PF->>DB: Checkout 삭제 PF-->>Client: Order 반환 end
7.4 주문 생성 상세 (_create_order / _create_order_from_checkout)
- 중복 방지:
Order.objects.filter(checkout_token=checkout.token)— 이미 주문이 있으면 반환 - 주문 상태:
channel.automatically_confirm_all_new_orders에 따라UNFULFILLED또는UNCONFIRMED - 주문 라인 생성: checkout 라인 → order 라인 변환 (가격, 할인, 세금 계산 반영)
- 재고 할당:
allocate_stocks(),allocate_preorders() - 기프트카드 적용:
add_gift_cards_to_order() - 결제 연결:
checkout.payments.update(order=order) - 메타데이터 복사: checkout → order
- 이벤트 발행:
order_created()(on_commit) - 확인 이메일:
send_order_confirmation()(on_commit) - Checkout 삭제:
delete_checkouts()
8. 세금 및 배송비 계산
8.1 세금 계산 (saleor/checkout/calculations.py)
def fetch_checkout_data(checkout_info, manager, lines, ...):
"""체크아웃 가격 데이터를 가져오고 캐싱한다.
1. 할인 적용 (create_or_update_discount_objects_from_promotion_for_checkout)
2. 세금 전략 결정 (TaxCalculationStrategy)
- FLAT_RATES: 내장 세율 테이블 사용
- TAX_APP: 외부 세금 앱 (webhook) 사용
3. 결제 상태 업데이트 (update_checkout_payment_statuses)
"""세금 계산 진입점:
checkout_shipping_price()— 배송비 (세금 포함)checkout_line_unit_price()— 라인 단가checkout_line_total()— 라인 합계checkout_line_tax_rate()— 라인 세율calculate_checkout_total_with_gift_cards()— 기프트카드 차감 후 총액 (가격 계산 참조)
8.2 배송비 계산
base_checkout_delivery_price() → checkout_shipping_price() (세금 적용)
배송비는 CheckoutDelivery 모델에 캐시되며, 내장/외부 배송 앱 양쪽 지원.
9. Checkout 결제 상태 관리 (saleor/checkout/payment_utils.py)
9.1 authorize_status 계산
def _update_authorize_status(checkout, checkout_total_gross,
total_authorized, total_charged, checkout_has_lines):
total_covered = total_authorized + total_charged
if checkout_with_only_zero_price_lines:
authorize_status = FULL
elif total_covered >= checkout_total_gross:
authorize_status = FULL
elif total_covered > 0:
authorize_status = PARTIAL
else:
authorize_status = NONE9.2 charge_status 계산
def _update_charge_status(checkout, checkout_total_gross, total_charged, ...):
if total_charged >= checkout_total_gross:
charge_status = FULL
elif total_charged > 0:
charge_status = PARTIAL
else:
charge_status = NONE9.3 금액 집계
def _get_payment_amount_for_checkout(checkout_transactions, currency):
for transaction in checkout_transactions:
total_authorized += transaction.amount_authorized + transaction.amount_authorize_pending
total_charged += transaction.amount_charged + transaction.amount_charge_pending
return total_authorized, total_charged10. Gateway 모듈 (saleor/payment/gateway.py)
10.1 Transaction Flow 액션 요청
def request_charge_action(transaction, manager, charge_value, request_event, ...):
"""TransactionItem에 대한 charge 요청.
→ WebhookEventSyncType.TRANSACTION_CHARGE_REQUESTED 발행
→ manager.transaction_charge_requested() 호출"""
def request_refund_action(transaction, manager, refund_value, request_event, ...):
"""TransactionItem에 대한 refund 요청.
→ WebhookEventSyncType.TRANSACTION_REFUND_REQUESTED 발행"""
def request_cancelation_action(transaction, manager, cancel_value, ...):
"""TransactionItem에 대한 cancel 요청.
→ WebhookEventSyncType.TRANSACTION_CANCELATION_REQUESTED 발행"""10.2 Payment Flow 게이트웨이 호출
# gateway.py의 데코레이터 패턴
@raise_payment_error # 실패 시 PaymentError 발생
@payment_postprocess # 결제 후처리 (charge_status 업데이트 등)
@require_active_payment # 비활성 결제 차단
@with_locked_payment # SELECT FOR UPDATE 락
def process_payment(payment, token, manager, ...):
"""결제 처리 — 플러그인 매니저를 통해 게이트웨이 호출"""11. 한국 PG 연동 포인트
11.1 연동 전략 비교
| 방식 | 장점 | 단점 |
|---|---|---|
| A. Saleor App (Transaction Flow) | 코어 코드 수정 불필요, 업그레이드 용이 | Saleor App 인프라 필요 |
| B. Payment Plugin (Payment Flow) | 빌트인, 코드 내 직접 제어 | deprecated 경향, 코어 포크 필요 |
| C. Hybrid | 유연성 | 복잡도 증가 |
권장: A. Saleor App 방식 (Transaction Flow)
11.2 Transaction Flow 기반 한국 PG 앱 구현 포인트
1) 결제 세션 초기화 — PAYMENT_GATEWAY_INITIALIZE_SESSION sync 웹훅
프론트엔드 → paymentGatewayInitialize GraphQL mutation
→ Saleor → PAYMENT_GATEWAY_INITIALIZE_SESSION 웹훅 → PG 앱
→ PG 앱: 토스페이먼츠 /payments 생성, clientKey 반환
→ 프론트엔드: TossPayments SDK로 결제 UI 렌더링
2) 결제 처리 — TRANSACTION_INITIALIZE_SESSION / TRANSACTION_PROCESS_SESSION 웹훅
프론트엔드 → transactionInitialize mutation (amount, paymentGateway, data)
→ Saleor → TransactionItem 생성
→ Saleor → TRANSACTION_INITIALIZE_SESSION 웹훅 → PG 앱
→ PG 앱:
토스: /confirm API 호출 (paymentKey, orderId, amount)
나이스: /approve API 호출
→ PG 앱 → 응답:
{
"pspReference": "pg_transaction_id",
"result": "CHARGE_SUCCESS",
"amount": 50000,
"data": { ... } // PG 원시 응답
}
→ Saleor: TransactionEvent(CHARGE_SUCCESS) 생성
→ Saleor: TransactionItem 금액 재계산
→ Saleor: Checkout authorize/charge 상태 업데이트
3) 웹훅 수신 — TRANSACTION_CHARGE_REQUESTED / TRANSACTION_REFUND_REQUESTED
관리자 → Order에서 환불 요청
→ Saleor → TRANSACTION_REFUND_REQUESTED 웹훅 → PG 앱
→ PG 앱: 토스 /cancel API 또는 나이스 /cancel API 호출
→ PG 앱 → transactionEventReport mutation 호출
(type: REFUND_SUCCESS, pspReference, amount)
→ Saleor: TransactionEvent 기록, 금액 재계산
4) 결과 보고 — transactionEventReport mutation
PG 앱은 비동기 결과를 transactionEventReport로 Saleor에 보고한다:
mutation {
transactionEventReport(
id: "transaction-item-id"
type: CHARGE_SUCCESS
amount: 50000
pspReference: "toss_xxxx_yyyy"
message: "결제 완료"
) {
alreadyProcessed
transactionEvent { ... }
}
}11.3 한국 PG 특화 고려사항
가상계좌 (Virtual Account)
토스페이먼츠: method === "가상계좌"
→ AUTHORIZATION_SUCCESS (입금 대기)
→ 웹훅으로 입금 확인 시 CHARGE_SUCCESS
→ TransactionItem.available_actions에서 "charge" 제거
한국 PG에서 가상계좌는 2단계 플로우:
- 결제 요청 → 계좌 발급 (AUTHORIZATION)
- 고객 입금 → 입금 확인 webhook → CHARGE_SUCCESS
이 패턴은 Saleor의 authorize → charge 패턴과 자연스럽게 매핑된다.
간편결제 (카카오페이, 네이버페이 등)
→ CHARGE_SUCCESS 직접 (authorize 없이 즉시 결제)
→ available_actions = ["refund"]
→ payment_method_type = "other" (카드가 아닌 경우)
부분 취소
토스페이먼츠, 나이스페이 모두 부분 취소를 지원한다. Saleor의 REFUND_REQUEST → REFUND_SUCCESS 패턴으로 처리.
통화
TransactionItem.currency와 Checkout.currency가 KRW여야 한다. KRW는 소수점이 없으므로 DECIMAL_PLACES=0 설정 확인 필요. Saleor 설정에서:
# settings.py
DEFAULT_CURRENCY = "KRW"
DEFAULT_DECIMAL_PLACES = 0 # 한국 원화는 소수점 없음11.4 포크 시 수정이 필요한 파일들
Transaction Flow (Saleor App) 방식을 사용하면 코어 수정은 최소화된다. 필요한 경우:
| 파일 | 수정 목적 |
|---|---|
saleor/payment/__init__.py | PaymentMethodType에 한국 PG 결제수단 추가 (선택) |
saleor/payment/models.py | TransactionItem에 한국 PG 전용 필드 추가 (선택) |
saleor/checkout/complete_checkout.py | 가상계좌 등 특수 플로우 지원 (선택) |
settings.py | KRW 통화 설정, 소수점 설정 |
11.5 PG 앱 (별도 서비스) 구현 체크리스트
□ PAYMENT_GATEWAY_INITIALIZE_SESSION 웹훅 핸들러
□ TRANSACTION_INITIALIZE_SESSION 웹훅 핸들러
□ TRANSACTION_PROCESS_SESSION 웹훅 핸들러
□ TRANSACTION_CHARGE_REQUESTED 웹훅 핸들러
□ TRANSACTION_REFUND_REQUESTED 웹훅 핸들러
□ TRANSACTION_CANCELATION_REQUESTED 웹훅 핸들러
□ PG사 웹훅 수신 엔드포인트 (입금 확인, 결제 상태 변경 등)
□ transactionEventReport mutation 호출 로직
□ 멱등성 처리 (idempotency_key 활용)
□ 에러 핸들링 및 재시도 로직
12. 주요 파일 인덱스
| 파일 | 설명 |
|---|---|
saleor/payment/__init__.py | 결제 상수, 열거형 정의 |
saleor/payment/models.py | Payment, Transaction, TransactionItem, TransactionEvent 모델 |
saleor/payment/interface.py | 게이트웨이 인터페이스 데이터 클래스 |
saleor/payment/gateway.py | 결제 요청 엔트리포인트, 액션 요청 함수 |
saleor/payment/utils.py | 결제 유틸리티 (PaymentData 생성, 트랜잭션 관리 등) |
saleor/payment/transaction_item_calculations.py | TransactionItem 이벤트 기반 금액 재계산 |
saleor/payment/gateways/dummy/__init__.py | 더미 게이트웨이 참조 구현 |
saleor/payment/gateways/dummy/plugin.py | 게이트웨이 플러그인 등록 패턴 |
saleor/payment/gateways/stripe/plugin.py | Stripe 플러그인 (실제 PG 연동 참조) |
saleor/payment/gateways/stripe/webhooks.py | Stripe 웹훅 핸들러 |
saleor/checkout/__init__.py | Checkout 상태 열거형 (ChargeStatus, AuthorizeStatus) |
saleor/checkout/models.py | Checkout, CheckoutLine, CheckoutDelivery 모델 |
saleor/checkout/complete_checkout.py | 체크아웃 완료 전체 로직 (핵심) |
saleor/checkout/calculations.py | 가격/세금 계산 |
saleor/checkout/fetch.py | CheckoutInfo, CheckoutLineInfo 데이터 클래스 |
saleor/checkout/payment_utils.py | 체크아웃 결제 상태 업데이트 |
saleor/checkout/actions.py | 체크아웃 이벤트 트리거 (webhook 발행) |
관련 문서
핵심 개념
- 📒 Saleor Payments / 📒 Saleor Payments Detail — 결제 개요 및 상세
- 📒 Saleor Checkout / 📒 Saleor Checkout Detail — 체크아웃 개요 및 상세
- 📒 Saleor Orders / 📒 Saleor Order Detail — 주문 개요 및 상세
- 📒 Saleor Channels / 📒 Saleor Channels Detail — 채널 설정
앱 및 확장
- 📒 Saleor Apps — Saleor App 개요
- 📒 Saleor App Architecture — 앱 아키텍처
- 📒 Saleor Payment App Development — 결제 앱 개발 가이드
- 📒 Saleor Webhooks — 웹훅 개요
- 📒 Saleor Sync Events Overview / 📒 Saleor Sync Events Payment — 동기 이벤트 및 결제 이벤트
- 📒 Saleor Async Events — 비동기 이벤트
도메인
- 📒 Saleor Stock — 재고 관리
- 📒 Saleor Taxes / 📒 Saleor Price Calculation — 세금 및 가격 계산
- 📒 Saleor Discounts — 할인/프로모션
- 📒 Saleor Shipping Detail — 배송
- 📒 Saleor Address — 주소
- 📒 Saleor Metadata — 메타데이터
- 📒 Saleor Products Detail — 상품
아키텍처 및 참조
- 📒 Saleor Architecture — 전체 아키텍처
- 📒 Saleor GraphQL — GraphQL API
- 📒 Saleor Core Fork Analysis — 포크 분석
- 📚 227 Saleor — Saleor 인덱스