Community코딩 & 개발github.com

Satcomx00-x00/skills-db

SaaS 구축을 위한 포괄적인 스킬: 멀티 테넌시, 사용량 기반 과금, PLG, AI 기능 통합, 현대적 관찰 가능성, 규정 준수 및 보안을 다룹니다.

skills-db란 무엇인가요?

이 스킬은 프로덕션급 Software-as-a-Service(SaaS) 제품을 구축하기 위한 최신 모범 사례를 제공합니다. 주요 영역으로는 멀티 테넌시(공유 스키마, 스키마-퍼-테넌트, DB-퍼-테넌트, 하이브리드 모델과 함께 PostgreSQL RLS 및 UUID 사용), 구독 및 사용량 기반 과금(Stripe, Paddle 등과 같은 결제 제공업체에 위임하고 웹훅을 통해 상태 동기화), 그리고 제품 주도 성장(PLG) 전략(무료 티어, 제품 적격 리드(PQL), 인앱 업그레이드 유도)을 다룹니다. 또한 AI 네이티브 기능을 통합하기 위한 패턴(RAG, 스트리밍 응답, 테넌트별 사용량 추적)과 기능 플래그를 사용한 요금제 기반 게이팅, 구조화된 로깅, 분산 추적(OpenTelemetry) 및 테넌트별 메트릭을 포함한 관찰 가능성, GDPR/SOC 2와 같은 데이터 규정 준수, 그리고 데이터 격리 및 보안 모범 사례를 포함합니다. 제공된 코드 예제는 테넌트 범위 저장소, AI 스트리밍 엔드포인트, 그리고 멱등성 Stripe 웹훅 처리를 보여줍니다.

지원 대상~Claude Code✓Codex CLI~Cursor
npx skills add Satcomx00-x00/skills-db

Installed? Explore more 코딩 & 개발 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

SaaS (서비스형 소프트웨어)

SaaS의 최신 기술 — 멀티 테넌시, 사용량 기반 과금, PLG, AI 기능 통합, 현대적 관찰 가능성, 규정 준수, 그리고 프로덕션 Software-as-a-Service 제품을 위한 보안.

지침

멀티 테넌시(Multi-Tenancy)

사용 사례에 적합한 격리 모델을 선택하세요:

모델설명사용 시기
공유 스키마모든 테넌트가 테이블을 공유하며, 행은 tenant_id로 태그됨초기 단계, 비용 효율적, 간단한 규정 준수
스키마-퍼-테넌트테넌트당 별도의 DB 스키마중간 수준의 격리, 테넌트별 마이그레이션 용이
DB-퍼-테넌트테넌트당 별도의 데이터베이스엔터프라이즈, 엄격한 데이터 상주, 높은 규정 준수(SOC 2, HIPAA)
하이브리드핵심 데이터는 공유, 민감 데이터는 격리혼합 등급 서비스를 제공하는 규제 산업

공유 스키마 테넌시 규칙:

  • 모든 쿼리는 반드시 tenant_id로 필터링해야 합니다. — 개발자 규율이 아닌 기본 리포지토리 또는 ORM 미들웨어를 통해 적용하세요.
  • PostgreSQL **행 수준 보안(RLS)**을 두 번째 방어 계층으로 사용하세요:
    ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
    CREATE POLICY tenant_isolation ON orders
      USING (tenant_id = current_setting('app.current_tenant')::uuid);
    
  • 모든 테넌트 범위 테이블에 tenant_id를 인덱싱하세요.
  • 테넌트 간 데이터 격리를 확인하는 통합 테스트를 작성하세요.
  • 모든 객체 스토리지 경로 범위를 지정하세요: s3://bucket/{tenantId}/uploads/{fileId}
  • 예측 가능한 순차적 ID를 절대 노출하지 마세요. — UUID(v7, 정렬 가능)를 사용하세요.

구독 및 과금(Subscription & Billing)

정액제 구독(Flat-rate subscriptions)

  • 과금은 제공업체(Stripe, Paddle, LemonSqueezy)에 위임하세요. — 절대 자체적으로 결제 처리를 구현하지 마세요.
  • 데이터베이스에는 customerId와 subscriptionId만 저장하고, 카드 데이터는 절대 저장하지 마세요.
  • 웹훅을 통해 상태를 동기화하세요. — 데이터베이스를 과금 제공업체 진실의 최종적 일관성을 가진 읽기 복제본으로 취급하세요:
    customer.subscription.created  → 플랜 활성화
    customer.subscription.updated  → 업그레이드 / 다운그레이드 / 시트 변경
    customer.subscription.deleted  → 취소 + 유예 기간 시작
    invoice.payment_failed         → 연체 관리: 알림 → 제한 → 취소
    
  • 웹훅 이벤트 id를 멱등성 키로 사용하세요. — 처리 전에 저장하고, 이미 본 경우 건너뛰세요.
  • 감사 및 재생을 위해 원시 웹훅 페이로드를 저장하세요.

사용량 기반 / 계량 과금(Usage-based / metered billing) (현대적 기본값)

  • 사용 이벤트가 발생할 때마다 추적하세요 (API 호출, 시트, 소비된 토큰, 스토리지 GB):
    await stripe.subscriptionItems.createUsageRecord(subscriptionItemId, {
      quantity: tokensUsed,
      timestamp: Math.floor(Date.now() / 1000),
      action: 'increment',
    });
    
  • 고객에게 실시간 사용량 대시보드를 제공하세요. — 청구일의 예상치 못한 요금은 이탈을 유발합니다.
  • 과금 기간별로 소프트 제한(80%에서 경고)과 하드 제한(100%에서 차단)을 구현하세요.
  • 제품 주도 성장(PLG) 온램프로서 무료 티어 + 사용량 기반 업셀을 고려하세요.

제품 주도 성장 (Product-Led Growth, PLG)

  • 무료 티어 / 프리미엄(Freemium) — 사용자가 결제 전에 가치를 경험하게 하세요. 유료 플랜이 명백히 가치 있도록 제한을 설정하세요.
  • 가치 실현 시간(Time-to-value, TTV) — 가입부터 첫 번째 의미 있는 행동("아하 모먼트")까지의 시간을 측정하고 최소화하세요.
  • 인앱 업그레이드 프롬프트 — 플랜 제한에 도달하는 순간에 상황에 맞게 업그레이드 CTA를 표시하세요. 일반 설정 페이지가 아닙니다.
  • 제품 적격 리드(Product-qualified leads, PQL) — 데모 요청뿐만 아니라 제품 사용 신호를 기반으로 사용자 점수를 매겨 영업팀에 전달하세요.
  • 바이럴 / 협업 기능 — 팀원 초대, 보고서 공유, 공개 페이지 — 각 행동은 배포 채널입니다.

AI 기능 통합 (AI Feature Integration)

현대 SaaS 제품은 AI 네이티브 기능을 제공할 것으로 예상됩니다. 다음 패턴을 적용하세요:

AI 어시스턴트 및 코파일럿:

  • 스트리밍 응답(SSE / ReadableStream)을 사용하여 사용자가 토큰이 도착하는 즉시 볼 수 있게 하세요. — 전체 응답을 기다리게 하지 마세요.
  • AI 출력물을 항상 편집 가능하게 만드세요. — 사용자는 출력물을 수정, 개선, 소유해야 합니다.
  • 신뢰도 신호를 표시하고 출처(RAG)를 인용하여 신뢰를 구축하세요.

검색 증강 생성(Retrieval-Augmented Generation, RAG):

  • 테넌트별 벡터 임베딩을 격리된 네임스페이스(Pinecone, pgvector, Qdrant)에 저장하세요. — 절대 테넌트 데이터를 혼합하지 마세요.
  • 쓰기 시 콘텐츠를 임베드하고, 읽기 시 코사인 유사도로 쿼리하세요.
  • 문서를 지능적으로 청크하세요 (512-1024 토큰, 중첩 포함). 소스 콘텐츠가 변경되면 다시 임베드하세요.

사용량 및 비용 제어:

  • 테넌트별 LLM 토큰 소비를 추적하고, 그에 따라 과금 또는 속도 제한을 적용하세요.
  • 결정론적 프롬프트(동일 입력 → 동일 출력)를 캐싱하여 비용을 절감하세요.
  • 분류/트리아지에는 저렴한 모델을 사용하고, 생성 작업에는 최첨단 모델을 사용하세요.

데이터 프라이버시:

  • 테넌트 데이터가 모델 학습에 사용되는지 여부를 명시적으로 명시하는 데이터 처리 부록(DPA)을 제공하세요.
  • 옵트아웃을 제공하세요. 엔터프라이즈 고객은 이를 요구합니다.

기능 플래그 및 요금제 게이팅 (Feature Flags & Plan Gating)

  • 서비스 계층에서 기능을 게이팅하세요. — 클라이언트 측 숨김은 UI 개선일 뿐 보안이 아닙니다.
  • 권한 로직을 단일 can(tenant, feature) 헬퍼에 중앙화하세요:
    function can(tenant: Tenant, feature: Feature): boolean {
      return tenant.plan.features.includes(feature);
    }
    
  • 기능 배포를 기능 출시와 분리하려면 플래그 시스템(LaunchDarkly, Unleash, GrowthBook 또는 간단한 DB 기반 테이블)을 사용하세요.
  • 플래그 사용 사례: 점진적 롤아웃(테넌트 비율), 베타 프로그램, A/B 테스트, 킬 스위치.

온보딩 (Onboarding)

  • TTV 최소화: 사용자를 5분 이내에 첫 번째 의미 있는 행동으로 안내하세요.
  • 명확한 진행 표시기가 있는 체크리스트 기반 온보딩 흐름을 사용하세요.
  • 제품이 처음 사용 시 비어 보이지 않도록 샘플/데모 데이터를 미리 채워두세요.
  • 초기에는 최소한의 데이터만 수집하고, 나머지는 나중으로 미루세요.
  • 시간 기반이 아닌 제품 이벤트를 기반으로 온보딩 이메일을 트리거하세요: 가입 시 환영, 24시간 후 2단계 미완료 시 알림, 첫 번째 이정표 축하.
  • 대화형 제품 둘러보기(비디오 아님)를 제공하여 사용자가 직접 해보며 배우게 하세요.

관찰 가능성 및 운영 (Observability & Operations)

구조화된 로깅(Structured logging) — 모든 로그 라인에는 다음이 포함되어야 합니다:

{ "level": "info", "msg": "...", "tenantId": "...", "userId": "...", "requestId": "...", "timestamp": "..." }

OpenTelemetry를 사용한 분산 추적(Distributed tracing):

  • 프레임워크 수준에서 계측하세요 (Next.js instrumentation.ts, Express 미들웨어).
  • 서비스 경계 전반에 걸쳐 추적 컨텍스트를 전파하세요 (traceparent 헤더).
  • 선택한 백엔드(Jaeger, Honeycomb, Datadog, Grafana Tempo)로 내보내세요.

추적할 테넌트별 메트릭:

  • 활성 사용자 (DAU/MAU), API 호출량, 오류율, p50/p95/p99 지연 시간, 기능 채택률, 소비된 스토리지.

알림(Alerting):

  • 테넌트 수준 이상 징후에 대해 알림하세요. — 갑작스러운 오류 급증, 사용량 감소, 과금 실패.
  • 티어별 SLO를 정의하세요: 무료 (최선 노력), 프로 (99.5%), 엔터프라이즈 (99.9%).

관리 도구 (내부):

  • 지원을 위해 테넌트를 가장하세요 (모든 가장은 감사).
  • 감사 로그 보기, 구독 관리, 기능 플래그 재정의, 재온보딩 트리거.
  • 상태 대시보드 구축: 테넌트별 상태, 최근 오류, 사용량 추세.

데이터, 분석 및 규정 준수 (Data, Analytics & Compliance)

이벤트 추적(Event tracking):

  • 구조화된 제품 이벤트(user.signed_up, feature.used, subscription.upgraded)를 데이터 웨어하우스(Snowflake, BigQuery) 또는 CDP(Segment, PostHog, RudderStack)로 내보내세요.
  • 이러한 이벤트를 이탈 예측, 코호트 분석, PLG 점수 매기기에 사용하세요.

GDPR / 개인정보 보호(Privacy):

  • 데이터 내보내기 엔드포인트와 계정 삭제 엔드포인트를 제공하세요. — GDPR에서 요구하며 엔터프라이즈 구매자가 기대합니다.
  • 데이터 보존 정책을 문서화하고 자동 삭제를 구현하세요.
  • 마케팅 쿠키 및 분석을 위해 동의 관리를 사용하세요.

SOC 2 / 규정 준수 위생 (엔터프라이즈 준비):

  • 모든 상태 변경 사용자 행동(누가, 무엇을, 언제, 어떤 IP에서)에 대한 감사 로그를 유지하세요.
  • 최소 권한 기본값으로 역할 기반 액세스 제어(RBAC)를 구현하세요.
  • 모든 관리자 계정에 MFA를 적용하고 엔터프라이즈 테넌트를 위해 SSO(SAML/OIDC)를 제공하세요.
  • 정기적인 종속성 취약점 스캔 및 침투 테스트를 수행하세요.
  • 취약점 공개 정책을 유지하세요.

데이터 격리 및 보안 (Data Isolation & Security)

  • 인증된 세션에 대해 모든 API 호출에서 tenant_id의 유효성을 검증하세요. — 요청 본문을 절대 신뢰하지 마세요.
  • 테넌트별로 속도 제한을 적용하여 잡음이 많은 이웃(noisy-neighbour) 효과를 방지하세요.
  • 데이터베이스에 API 키를 해시하여 저장하세요 (bcrypt 또는 Argon2). 절대 평문으로 저장하지 마세요.
  • HTTPS를 요구하고 긴 max-age와 함께 Strict-Transport-Security를 설정하세요.
  • 세션 쿠키에 대해 HttpOnly; Secure; SameSite=Lax를 사용하세요.

예제 (Examples)

// RLS 컨텍스트를 사용한 테넌트 범위 리포지토리
class TenantRepository<T> {
  constructor(
    private readonly tenantId: string,
    private readonly db: PrismaClient,
  ) {}

  protected async withTenantContext<R>(fn: () => Promise<R>): Promise<R> {
    return this.db.$transaction(async (tx) => {
      await tx.$executeRaw`SELECT set_config('app.current_tenant', ${this.tenantId}, true)`;
      return fn();
    });
  }

  async findById(id: string): Promise<T | null> {
    return this.withTenantContext(() =>
      (this.db as any).findFirst({ where: { id, tenantId: this.tenantId } })
    );
  }
}

// 테넌트별 사용량 계량을 포함한 AI 스트리밍 엔드포인트
export async function POST(req: Request) {
  const session = await getSession(req);
  const { prompt } = await req.json();

  const stream = await openai.chat.completions.create({
    model: 'gpt-4o',
    messages: [{ role: 'user', content: prompt }],
    stream: true,
  });

  let tokensUsed = 0;
  const readable = new ReadableStream({
    async start(controller) {
      for await (const chunk of stream) {
        const text = chunk.choices[0]?.delta?.content ?? '';
        controller.enqueue(new TextEncoder().encode(text));
        tokensUsed += chunk.usage?.completion_tokens ?? 0;
      }
      controller.close();
      // 스트리밍 완료 후 사용량 계량
      await meteringService.record(session.tenantId, 'ai_tokens', tokensUsed);
    },
  });

  return new Response(readable, { headers: { 'Content-Type': 'text/plain; charset=utf-8' } });
}

// 멱등성을 갖춘 Stripe 웹훅
app.post('/webhooks/stripe', express.raw({ type: 'application/json' }), async (req, res) => {
  const event = stripe.webhooks.constructEvent(
    req.body, req.headers['stripe-signature']!, process.env.STRIPE_WEBHOOK_SECRET!
  );

  const seen = await db.webhookEvent.findUnique({ where: { stripeEventId: event.id } });
  if (seen) return res.sendStatus(200);
  await db.webhookEvent.create({ data: { stripeEventId: event.id, payload: event } });

  switch (event.type) {
    case 'customer.subscription.updated':
      await subscriptionService.sync(event.data.object);
      break;
    case 'customer.subscription.deleted':
      await subscriptionService.cancel(event.data.object.metadata.tenantId);
      break;
  }

  res.sendStatus(200);
});

참고 자료 (References)

관련 스킬