Saleor Core Codemap: Channel / Order / Warehouse

한국형 멀티테넌트 이커머스 플랫폼 포크를 위한 아키텍처 분석 문서 분석 대상: saleor-core (Django + GraphQL)


1. Channel 모듈 (멀티테넌시 핵심)

1.1 Channel 모델 필드 해설

필드타입용도
nameCharField(250)채널 표시명 (예: “Korea Store”)
slugSlugField(255, unique)URL/API 식별자, 전역 고유
is_activeBooleanField채널 활성화 여부
currency_codeCharField채널 기본 통화 (예: KRW)
default_countryCountryField채널 기본 국가 (예: KR)
allocation_strategyCharField재고 할당 전략 (정렬순 우선 / 재고량 우선)
order_mark_as_paid_strategyCharField수동 결제 처리 방식 (Transaction / Payment flow)
default_transaction_flow_strategyCharField결제 흐름 (인증만 / 즉시 청구)
automatically_confirm_all_new_ordersBooleanField주문 자동 확인 여부
allow_unpaid_ordersBooleanField미결제 주문 허용 여부
automatically_fulfill_non_shippable_gift_cardBooleanField디지털 기프트카드 자동 이행
expire_orders_afterIntegerField(nullable)주문 자동 만료 시간 (분)
delete_expired_orders_afterDurationField만료 주문 삭제 대기 기간 (기본 60일)
include_draft_order_in_voucher_usageBooleanField바우처 사용량에 초안주문 포함 여부
use_legacy_error_flow_for_checkoutBooleanField레거시 체크아웃 에러 흐름 사용
automatically_complete_fully_paid_checkoutsBooleanField완불 체크아웃 자동 완료
draft_order_line_price_freeze_periodPositiveIntegerField초안 주문 라인 가격 갱신 주기 (시간)

상속: ModelWithMetadataprivate_metadata JSON 필드 포함

1.2 Channel 전략 Enum 정리

AllocationStrategy:
  PRIORITIZE_SORTING_ORDER  -- 채널 내 warehouse 순서대로 할당
  PRIORITIZE_HIGH_STOCK     -- 재고가 가장 많은 warehouse부터 할당

MarkAsPaidStrategy:
  TRANSACTION_FLOW  -- TransactionItem으로 결제 처리
  PAYMENT_FLOW      -- Payment 객체로 결제 처리

TransactionFlowStrategy:
  AUTHORIZATION  -- 인증만 (이후 수동 캡처)
  CHARGE         -- 즉시 청구

1.3 Channel Penetration Map (채널 참조 모델 전체)

Saleor에서 Channel은 거의 모든 비즈니스 도메인에 FK/M2M으로 침투해 있다 (Channel 상세). 아래는 models.py 기준 직접 참조 목록:

graph TD
    CH[Channel]

    subgraph "직접 FK/M2M 참조"
        O[Order] -->|FK, PROTECT| CH
        CK[Checkout] -->|FK, PROTECT| CH
        PCL[ProductChannelListing] -->|FK, CASCADE| CH
        PVCL[ProductVariantChannelListing] -->|FK, CASCADE| CH
        CCL[CollectionChannelListing] -->|FK, CASCADE| CH
        VCL[VoucherChannelListing] -->|FK, CASCADE| CH
        SMCL[ShippingMethodChannelListing] -->|FK, CASCADE| CH
        CW[ChannelWarehouse] -->|FK, CASCADE| CH
        WH[Warehouse] -->|M2M through CW| CH
        GRP[Group] -->|M2M| CH
    end

    subgraph "간접 참조 (ChannelListing 패턴)"
        P[Product] -.->|via PCL| CH
        PV[ProductVariant] -.->|via PVCL| CH
        COL[Collection] -.->|via CCL| CH
        V[Voucher] -.->|via VCL| CH
        SM[ShippingMethod] -.->|via SMCL| CH
    end

ChannelListing 모델 상세

ChannelListing 모델소속 모듈주요 필드역할
ProductChannelListingproductvisible_in_listings, available_for_purchase_at, discounted_price상품의 채널별 게시 상태/가격
ProductVariantChannelListingproductprice, cost_price, prior_price, discounted_price, preorder_quantity_threshold옵션(SKU)별 채널 가격
CollectionChannelListingproduct(PublishableModel 상속)컬렉션의 채널별 게시 상태
VoucherChannelListingdiscountdiscount_value, min_spent바우처의 채널별 할인액/최소주문
ShippingMethodChannelListingshippingprice, minimum_order_price, maximum_order_price배송방법의 채널별 요금
VariantChannelListingPromotionRuleproductdiscount_amount프로모션 룰 적용 결과 (variant+channel 조합)

1.4 채널 기반 필터링 메커니즘

Query 레벨: GraphQL resolver에서 get_user_accessible_channels()로 사용자가 접근 가능한 채널 목록을 조회한 뒤, channel_id__in= 조건으로 필터링.

# saleor/graphql/order/resolvers.py
accessible_channels = get_user_accessible_channels(info, info.context.user)
channel_ids = [channel.id for channel in accessible_channels]
qs = qs.filter(channel_id__in=channel_ids)

Warehouse 레벨: WarehouseQueryset.for_channel(channel_id) — Channel-Warehouse M2M through 테이블 기준으로 필터.

Stock 레벨: StockQuerySet.for_channel_and_country() — Channel → Warehouse → ShippingZone 3중 조인으로 특정 채널+국가에 유효한 재고만 반환.

1.5 Permission Scoping by Channel

graph LR
    U[User] -->|belongs_to| G[Group]
    G -->|has| P[Permissions]
    G -->|M2M| CH[Channels]
    G -->|flag| R[restricted_access_to_channels]

    R -->|true| RESTRICTED["특정 채널만 접근"]
    R -->|false| FULL["전체 채널 접근"]
  • Group.restricted_access_to_channels = True이면 Group.channels M2M에 연결된 채널만 접근 가능
  • False이면 모든 채널에 접근 가능
  • **App (API 클라이언트)**는 항상 전체 채널 접근 (check_channel_permissions에서 App이면 바로 return)
  • Mutation 레벨에서 check_channel_permissions(info, [channel_id])로 채널 접근 권한 검증

2. Order 모듈

2.1 핵심 모델 관계도

erDiagram
    Order ||--o{ OrderLine : "lines (CASCADE)"
    Order ||--o{ Fulfillment : "fulfillments (CASCADE)"
    Order ||--o{ OrderEvent : "events (CASCADE)"
    Order ||--o{ OrderGrantedRefund : "granted_refunds (CASCADE)"
    Order }o--|| Channel : "channel (PROTECT)"
    Order }o--o| User : "user (SET_NULL)"
    Order }o--o| ShippingMethod : "shipping_method (SET_NULL)"
    Order }o--o| Warehouse : "collection_point (SET_NULL)"
    Order }o--o| Voucher : "voucher (SET_NULL)"
    Order }o--o{ GiftCard : "gift_cards (M2M)"

    OrderLine }o--o| ProductVariant : "variant (SET_NULL)"
    OrderLine ||--o{ FulfillmentLine : "fulfillment_lines (CASCADE)"
    OrderLine ||--o{ Allocation : "allocations (CASCADE)"
    OrderLine ||--o{ OrderGrantedRefundLine : "granted_refund_lines (CASCADE)"

    Fulfillment ||--o{ FulfillmentLine : "lines (CASCADE)"
    FulfillmentLine }o--o| Stock : "stock (SET_NULL)"

    OrderGrantedRefund ||--o{ OrderGrantedRefundLine : "lines (CASCADE)"
    OrderGrantedRefund }o--o| TransactionItem : "transaction_item (SET_NULL)"

2.2 Order 상태 머신

stateDiagram-v2
    [*] --> DRAFT: staff가 수동 생성
    [*] --> UNCONFIRMED: checkout 완료 (자동확인 꺼짐)
    [*] --> UNFULFILLED: checkout 완료 (자동확인 켜짐)

    DRAFT --> UNFULFILLED: place order
    DRAFT --> EXPIRED: expire_orders_after 초과

    UNCONFIRMED --> UNFULFILLED: confirm order
    UNCONFIRMED --> CANCELED: cancel
    UNCONFIRMED --> EXPIRED: expire_orders_after 초과

    UNFULFILLED --> PARTIALLY_FULFILLED: 일부 라인 fulfill
    UNFULFILLED --> FULFILLED: 전체 라인 fulfill
    UNFULFILLED --> CANCELED: cancel (fulfillment 없을 때만)

    PARTIALLY_FULFILLED --> FULFILLED: 나머지 라인 fulfill
    PARTIALLY_FULFILLED --> PARTIALLY_RETURNED: 일부 반품
    PARTIALLY_FULFILLED --> CANCELED: cancel (active fulfillment 없을 때)

    FULFILLED --> PARTIALLY_RETURNED: 일부 반품
    FULFILLED --> RETURNED: 전체 반품

    PARTIALLY_RETURNED --> RETURNED: 나머지 반품

    CANCELED --> [*]
    EXPIRED --> [*]
    RETURNED --> [*]

상태 결정 로직 (determine_order_status)

if quantity_fulfilled - quantity_awaiting_approval <= 0:
    status = UNFULFILLED
elif 0 < quantity_returned < total_quantity:
    status = PARTIALLY_RETURNED
elif quantity_returned == total_quantity:
    status = RETURNED
elif quantity_fulfilled - quantity_awaiting_approval < total_quantity:
    status = PARTIALLY_FULFILLED
else:
    status = FULFILLED

2.3 Fulfillment 상태

상태의미
FULFILLED배송 완료 (또는 발송됨)
REFUNDED환불 처리됨
RETURNED반품 처리됨
REFUNDED_AND_RETURNED환불 + 반품
REPLACED교환 처리됨
CANCELED이행 취소됨
WAITING_FOR_APPROVAL승인 대기

2.4 주문 생성 흐름 (Checkout → Order)

1. Checkout 완료 요청
   └─ _create_order_from_checkout() [complete_checkout.py:1370]
       ├─ _create_order() -- Order 객체 생성 (channel FK 복사)
       ├─ _create_order_lines_from_checkout_lines() -- CheckoutLine → OrderLine 변환
       ├─ _create_order_discount() -- 바우처/프로모션 할인 복사
       └─ order_created() [actions.py:286]
           ├─ user.number_of_orders += 1
           ├─ order_created_event 기록
           ├─ [[📒 Saleor Webhooks|webhook]]: ORDER_CREATED 발송
           ├─ 결제 상태에 따라 order_authorized / order_charged 호출
           └─ channel.automatically_confirm_all_new_orders이면 order_confirmed 호출

2.5 주문 취소 흐름

cancel_order() [actions.py:421]
├─ order_canceled_event 기록
├─ deallocate_stock_for_orders() -- 미이행 라인의 재고 할당 해제
├─ order.status = CANCELED
├─ webhook: ORDER_CANCELLED, ORDER_UPDATED
└─ send_order_canceled_confirmation -- 고객 알림

취소 가능 조건 (Order.can_cancel()):

  • active fulfillment이 없어야 함 (CANCELED, REFUNDED, RETURNED, REPLACED만 허용)
  • 주문 상태가 CANCELED, DRAFT, EXPIRED가 아니어야 함

2.6 환불 흐름

order_refunded() [actions.py:455]
├─ payment_refunded_event 기록
├─ send_order_refunded_confirmation
├─ 총 환불액 계산 (Payment.transactions + TransactionItem.refunded_value)
├─ total_refunded >= order.total.gross이면 ORDER_FULLY_REFUNDED webhook
└─ webhook: ORDER_REFUNDED, ORDER_UPDATED

관련 모델:
  OrderGrantedRefund -- 승인된 환불 (NONE → PENDING → SUCCESS/FAILURE)
  OrderGrantedRefundLine -- 환불 대상 라인별 수량

2.7 결제 상태 시스템

Order는 fulfillment 상태와 별도로 **결제 상태**를 독립 추적:

필드의미
authorize_statusNONE / PARTIAL / FULL인증(사전승인) 상태
charge_statusNONE / PARTIAL / FULL / OVERCHARGED실제 청구 상태

3. Warehouse / Stock 모듈

3.1 모델 관계도

erDiagram
    Warehouse ||--o{ Stock : "stock_set (CASCADE)"
    Warehouse }o--o{ Channel : "M2M through ChannelWarehouse"
    Warehouse }o--o{ ShippingZone : "M2M"
    Warehouse }o--|| Address : "address (PROTECT)"

    ChannelWarehouse }o--|| Channel : "channel (CASCADE)"
    ChannelWarehouse }o--|| Warehouse : "warehouse (CASCADE)"

    Stock }o--|| ProductVariant : "product_variant (CASCADE)"
    Stock ||--o{ Allocation : "allocations (CASCADE)"
    Stock ||--o{ Reservation : "reservations (CASCADE)"

    Allocation }o--|| OrderLine : "order_line (CASCADE)"

    Reservation }o--|| CheckoutLine : "checkout_line (CASCADE)"

    PreorderAllocation }o--|| OrderLine : "order_line (CASCADE)"
    PreorderAllocation }o--|| ProductVariantChannelListing : "(CASCADE)"

    PreorderReservation }o--|| CheckoutLine : "checkout_line (CASCADE)"
    PreorderReservation }o--|| ProductVariantChannelListing : "(CASCADE)"

3.2 재고 수량 계산

available_quantity = Stock.quantity - SUM(Allocation.quantity_allocated)

예약 포함:
available_quantity = Stock.quantity
    - SUM(Allocation.quantity_allocated)
    - SUM(Reservation.quantity_reserved WHERE reserved_until > now)
  • Stock.quantity: 물리적 재고량
  • Stock.quantity_allocated: 비정규화된 할당 총량 (성능 최적화)
  • Allocation: 주문 라인별 실제 할당 (주문 확정 시 생성)
  • Reservation: 체크아웃 중 임시 예약 (만료 시간 있음)

3.3 재고 할당 흐름

allocate_stocks() [management.py:80]
├─ 재고추적 대상 라인만 필터링
├─ channel.slug 기준으로 Stock 조회
│   ├─ collection_point이면: for_channel_and_click_and_collect()
│   └─ 일반 배송이면: for_channel_and_country(channel_slug, country_code)
├─ 기존 allocation 수량 집계
├─ sort_stocks(channel.allocation_strategy, ...)
│   ├─ PRIORITIZE_SORTING_ORDER: ChannelWarehouse.sort_order 기준
│   └─ PRIORITIZE_HIGH_STOCK: 가용 재고량 내림차순
├─ variant별 stocks 매핑
├─ 라인별 할당 생성 (_create_allocations)
│   └─ 부족하면 InsufficientStock 예외
└─ Allocation bulk_create + Stock.quantity_allocated 갱신

3.4 Warehouse 모델 주요 필드

필드타입용도
idUUIDPK
nameCharField(250)창고명
slugSlugField(255, unique)URL 식별자
channelsM2M(Channel) through ChannelWarehouse채널 연결
shipping_zonesM2M(ShippingZone)배송 가능 지역
addressFK(Address, PROTECT)창고 물리 주소
click_and_collect_optionCharFieldDISABLED / LOCAL_STOCK / ALL_WAREHOUSES
is_privateBooleanField비공개 창고 여부

4. 멀티테넌트 격리 평가

4.1 현재 Saleor의 Channel = “Soft Tenant”

Saleor의 Channel은 **논리적 격리(logical isolation)**를 제공하지만, 물리적 테넌트 격리는 아니다 (포크 분석 참고):

측면현재 상태포크 시 고려사항
DB 격리단일 DB, channel_id FK로 필터링테넌트 간 데이터 누출 위험 — 모든 쿼리에 channel 필터 필수
상품 카탈로그ChannelListing 패턴으로 채널별 가격/게시상품 자체는 공유 — 테넌트별 완전 분리 시 Product에도 tenant FK 필요
사용자User는 channel과 무관 (전역)멀티테넌트면 tenant-user 매핑 테이블 추가 필요
재고Warehouse-Channel M2M으로 채널별 창고 할당이미 채널별 분리 가능, 물류 공유도 지원
권한Group.restricted_access_to_channels로 채널별 접근 제한App은 전체 접근 — 테넌트별 App 분리 필요
결제Order.channel FK로 채널별 결제 설정 분리한국 PG(토스, KG이니시스 등) 연동 시 채널별 PG 설정 필요 (Payment App 개발)
URL 라우팅channel slug 기반도메인별 테넌트 라우팅 레이어 추가 필요

4.2 포크 시 멀티테넌시 강화 방안

방안 1: Channel = Tenant (현재 구조 활용)
  - 장점: 기존 코드 변경 최소화
  - 단점: 상품/사용자 공유 문제, 누출 위험
  - 적합: B2B SaaS가 아닌 자사 멀티브랜드 운영

방안 2: Tenant 모델 신설 + Channel 하위 배치
  - Tenant → Channel (1:N)
  - 모든 root 모델에 tenant_id FK 추가
  - Row-Level Security (PostgreSQL RLS) 활용
  - 적합: 완전한 SaaS 멀티테넌시

방안 3: Schema-per-Tenant
  - django-tenants 패키지 활용
  - 가장 강력한 격리, 가장 큰 마이그레이션 비용

4.3 누락 포인트 (현재 Saleor에 없는 것)

  • Row-Level Security: 없음 — 모든 격리가 애플리케이션 레벨
  • Tenant-aware 캐시: 없음 — 채널 slug를 캐시 키에 수동 포함 필요
  • Cross-tenant 방지 인덱스: unique_together에 channel이 빠진 모델들 존재 (예: Order.number는 전역 unique)
  • 감사 로그 격리: OrderEvent에 channel FK 없음 (order를 통해 간접 추적)

5. 한국 배송 연동 포인트

5.1 배송사 통합 아키텍처

graph TB
    subgraph "Saleor 기존 구조"
        SM[ShippingMethod]
        SZ[ShippingZone]
        SMCL[ShippingMethodChannelListing]
        F[Fulfillment]
        FL[FulfillmentLine]
    end

    subgraph "한국 배송 확장 필요"
        KSP[KR Shipping Provider Plugin]
        TN[tracking_number 활용]
        WH_API[Webhook: FULFILLMENT_CREATED]
    end

    SM --> SZ
    SM --> SMCL
    F --> FL
    F -->|tracking_number| TN
    TN -->|CJ대한통운, 한진, 로젠 등| KSP
    WH_API -->|비동기 배송 접수| KSP

5.2 주요 커스터마이징 포인트

영역현재 Saleor한국 커스텀 필요사항
배송비 계산ShippingMethodChannelListing.price (고정/무게)도서산간 추가금, 제주 별도 요금, 무료배송 임계값
배송 추적Fulfillment.tracking_number (단순 문자열)택배사 API 연동, 배송 상태 webhook 수신
반품/교환Fulfillment RETURNED/REPLACED 상태반품 택배 접수, 수거 예약 API
송장 출력Invoice 모델 존재한국 세금계산서 형식, 현금영수증 발행
주문번호전역 sequence (order_order_number_seq)테넌트별 주문번호 체계 필요 시 sequence 분리
우편번호ShippingZone.countries + PostalCodeRule한국 5자리 우편번호 체계, 행정구역 기반 배송권역 (Address 모델)
배송 상태FulfillmentStatus 7개”집하”, “간선상차”, “배송출발”, “배달완료” 등 세분화 필요

5.3 Fulfillment 확장 제안

# 한국 배송사 연동을 위한 Fulfillment 모델 확장
class Fulfillment(ModelWithMetadata):
    # 기존 필드
    tracking_number = CharField(max_length=255)
    status = CharField(choices=FulfillmentStatus.CHOICES)
 
    # 한국 배송 확장 필드 (제안)
    shipping_carrier = CharField()          # CJ, HANJIN, LOGEN, LOTTE 등
    shipping_carrier_code = CharField()     # 택배사 코드
    estimated_delivery_date = DateField()   # 예상 배송일
    actual_delivery_date = DateField()      # 실제 배송일
    delivery_message = TextField()          # 배송 메모 (부재 시 문앞 등)

5.4 한국 PG 연동 포인트

Order의 결제 흐름은 Channel의 default_transaction_flow_strategy로 결정:

  • AUTHORIZATION: 카드 인증 → 수동 캡처 (일반적이지 않음)
  • CHARGE: 즉시 결제 (토스페이먼츠, KG이니시스 등 한국 PG 표준 흐름)

Payment 플러그인 시스템을 통해 한국 PG를 연동하되, Channel 설정에서 CHARGE 전략을 기본으로 설정하면 된다.


6. 주요 파일 경로 요약

파일역할
saleor/channel/models.pyChannel 모델 정의
saleor/channel/__init__.pyAllocationStrategy, MarkAsPaidStrategy, TransactionFlowStrategy
saleor/channel/utils.pyget_default_channel (레거시 단일채널 호환)
saleor/order/models.pyOrder, OrderLine, Fulfillment, FulfillmentLine, OrderEvent, OrderGrantedRefund
saleor/order/__init__.pyOrderStatus, FulfillmentStatus, OrderEvents, OrderChargeStatus 등 enum
saleor/order/actions.py주문 생명주기 액션 (created, confirmed, canceled, refunded, fulfilled 등)
saleor/order/utils.pyupdate_order_status, determine_order_status, create_order_line
saleor/checkout/complete_checkout.pyCheckout → Order 변환 로직
saleor/warehouse/models.pyWarehouse, Stock, Allocation, Reservation, ChannelWarehouse
saleor/warehouse/management.pyallocate_stocks, deallocate_stock, decrease_stock
saleor/product/models.pyProductChannelListing, ProductVariantChannelListing, CollectionChannelListing
saleor/discount/models.pyVoucherChannelListing
saleor/shipping/models.pyShippingMethodChannelListing
saleor/account/models.pyGroup (restricted_access_to_channels, channels M2M)
saleor/graphql/core/mutations.pycheck_channel_permissions — 채널별 권한 검증
saleor/graphql/order/resolvers.py주문 쿼리에서 accessible_channels 필터링

관련 문서

핵심 개념

Channel / Order / Warehouse

결제 / 배송 / 할인

상품 / 사용자 / 권한

확장 / 커스터마이징