홈으로 돌아가기

Django SaaS boilerplate Shipyard: 루틴 없는 프로덕션 템플릿

Shipyard 분석 — SaaS 애플리케이션을 위한 프로덕션 레디 Django boilerplate. 멀티테넌시 아키텍처 분해, Stripe 통합 및 개발 2주 절약 기술 솔루션. 코드 예제와 모범 사례.

Shipyard: SaaS용 프로덕션 레디 Django 템플릿
Advertisement 728x90

## Shipyard: 번거로운 초기 설정 없이 프로덕션 레디 Django SaaS 보일러플레이트 만드는 법

SaaS 제품을 위한 새로운 Django 프로젝트를 시작하면 기본 구성 요소 설정만으로도 보통 2~3주가 소요됩니다. Shipyard는 사전 구성된 모듈이 포함된 프로덕션 레디 보일러플레이트를 제공하여 이 문제를 해결합니다. 개발 시간을 절약하는 아키텍처와 기술 솔루션을 자세히 살펴보겠습니다.

Stack and Project Structure

Shipyard는 프로덕션 워크로드에 최적화된 현대적인 스택으로 구축되었습니다. 주요 구성 요소:

  • Django 5 + DRF 3.15: HTML 템플릿 없이 API 우선 접근
  • PostgreSQL 16 + Redis 7: 데이터베이스와 캐시를 위한 별도 컨테이너
  • Celery 5 + Beat + Flower: 비동기 작업 및 모니터링
  • Docker Compose: 개발용과 프로덕션용 두 가지 구성
  • Stripe Webhooks: 멱등성(idempotency)을 지원한 이벤트 처리
  • RBAC를 사용한 멀티 테넌시: User → TeamMembership → Team 체인

프로젝트 구조는 엄격하게 모듈화되어 있습니다:

Google AdInline article slot
shipyard/
├── apps/
│   ├── core/
│   ├── users/
│   ├── teams/
│   ├── billing/
│   ├── notifications/
│   └── api/
├── config/
├── docker/
└── docker-compose.yml

각 앱은 자신의 책임 영역을 담당합니다. 예를 들어, billing은 구독 모델을, api는 비즈니스 로직 없이 DRF 인프라를 처리합니다.

Base Models Architecture

핵심 요소는 core/models.py의 추상 클래스입니다:

python
# apps/core/models.py
import uuid
from django.db import models

class TimestampedModel(models.Model):
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        abstract = True

class UUIDModel(models.Model):
    id = models.UUIDField(
        primary_key=True,
        default=uuid.uuid4,
        editable=False,
    )

    class Meta:
        abstract = True

이 믹스인은 모든 모델에서 사용됩니다. URL에서 예측 가능한 ID는 보안상 취약하고, 운영 중인 데이터베이스에서 정수에서 UUID로 마이그레이션하는 것은 고통스러운 작업입니다. TimestampedModel은 코드 중복 없이 변경 이력을 추적합니다.

Google AdInline article slot

users 앱은 이메일을 기본 식별자로 사용하는 커스텀 User 모델을 구현합니다:

python
# apps/users/models.py
class User(UUIDModel, AbstractBaseUser, PermissionsMixin):
    email = models.EmailField(unique=True)
    full_name = models.CharField(max_length=255, blank=True)
    is_email_verified = models.BooleanField(default=False)
    stripe_customer_id = models.CharField(max_length=255, blank=True, null=True)

    USERNAME_FIELD = "email"
    REQUIRED_FIELDS = ["full_name"]

stripe_customer_id 필드는 청구가 개인에 연결된 경우를 위해 User 모델에 추가됩니다. 이메일 검증 및 비밀번호 재설정 토큰은 EmailVerificationTokenPasswordResetToken 모델로 별도 처리되며, 캐시 없이 데이터베이스를 사용해 감사(auditing)를 지원합니다.

Multi-Tenancy via RBAC

Shipyard는 실용적인 멀티 테넌시 접근 방식을 사용합니다. 스키마 분리(django-tenants)나 전역 tenant_id 대신 소유권 체인을 활용합니다:

Google AdInline article slot
User → TeamMembership → Team

TeamMembershiprole 필드를 가진 중간 테이블 역할을 합니다. TeamPlan에서 비정규화된 제한값(max_members, max_projects)을 저장해 JOIN 없이 빠른 검사 가능합니다. 이로 인해 제한 확인 시 쿼리 복잡도가 줄어듭니다.

DRF 권한을 통해 세 가지 역할을 구현합니다:

python
# apps/teams/permissions.py
class IsTeamMember(BasePermission):
    def has_permission(self, request, view):
        team_id = view.kwargs.get('team_id')
        return TeamMembership.objects.filter(
            user=request.user,
            team_id=team_id
        ).exists()

패턴은 기계적입니다: 모든 ViewSet은 URL의 team_id로 쿼리셋을 필터링합니다. 이는 모든 쿼리에 tenant_id를 수동 추가하는 방식보다 테넌트 간 데이터 유출 위험을 줄입니다.

Billing System and Webhooks

Plan 모델은 Stripe Product + Prices를 반영합니다:

python
class Plan(models.Model):
    stripe_product_id = models.CharField(max_length=255)
    stripe_price_id_monthly = models.CharField(max_length=255)
    stripe_price_id_yearly = models.CharField(max_length=255)
    max_members = models.PositiveIntegerField()
    max_projects = models.PositiveIntegerField()

Plan의 비정규화 제한값으로 Stripe 호출 없이 즉시 쿼터 확인 가능합니다. 팀 생성 시 post_save 신호로 Stripe 고객을 프로비저닝해 바로 결제 준비가 됩니다.

웹훅 처리는 멱등성을 기반으로 합니다. WebhookEvent 모델이 처리된 이벤트를 추적합니다:

python
class WebhookEvent(models.Model):
    event_id = models.CharField(max_length=255, unique=True)
    event_type = models.CharField(max_length=255)
    payload = models.JSONField()
    processed_at = models.DateTimeField(null=True)

이로 인해 Stripe 재시도 시 중복 이벤트 처리를 방지합니다.

Key Highlights

  • 정수 PK 대신 UUID — 한 번 결정으로 끝; 운영 DB 마이그레이션 불가능
  • 중앙화된 헬스 체크 — Docker와 Kubernetes 통합을 위한 /health/ 및 /ready/ 엔드포인트
  • 별도 앱의 DRF 인프라 — 버저닝, 페이지네이션, 오류 처리 비즈니스 로직과 분리
  • EmailLog를 통한 이메일 감사 — 지원을 위한 모든 발송 이메일 로깅
  • 두 가지 Docker Compose 설정 — 볼륨 포함 개발 구성, 멀티스테이지 빌드 프로덕션 구성

Stripe 통합은 멱등성에 특별 주의가 필요합니다. 모든 웹훅 이벤트는 event_id로 고유성을 확인합니다. 이는 청구 같은 작업에서 재처리로 인한 이중 청구를 방지하는 데 핵심입니다. Shipyard는 WebhookEvent 모델을 자연스러운 락킹 메커니즘으로 사용합니다.

알림 기능은 기본 레이아웃을 가진 템플릿 시스템을 갖췄습니다. 모든 이메일(welcome, verify_email, invitation)은 유지보수가 쉬운 공통 구조를 상속합니다. 템플릿은 apps/notifications/templates/notifications/에 HTML과 텍스트 버전으로 별도 저장됩니다.

프로덕션 Docker Compose 구성은 멀티스테이지 빌드를 사용합니다. 첫 번째 단계에서 종속성 설치, 두 번째에서 코드 복사로 최종 이미지 크기를 줄이고 배포를 가속화합니다. CI/CD를 위해 GitHub Actions 워크플로 세 가지가 설정되어 있습니다: PR 시 린트 테스트, main 병합 시 이미지 빌드, 릴리스 태그 시 배포.

— Editorial Team

Advertisement 728x90

다음 읽기