Files
llm-wiki/wiki/blog/ca-tmpl-multi-tenancy-isolation-patterns-2026-07-02.md

9.6 KiB

title, source_type, status, confidence, tags, related_projects, last_reviewed, canonical_sources, audience, target_publish, status_label
title source_type status confidence tags related_projects last_reviewed canonical_sources audience target_publish status_label
Multi-tenancy를 기본값이 아니라 Opt-in 계약으로 두기 blog verified high
blog
ca-tmpl
multi-tenancy
saas
ca-tmpl
2026-07-03
wiki/projects/ca-tmpl/multi-tenancy-isolation-patterns
backend-engineer ready

Multi-tenancy를 기본값이 아니라 Opt-in 계약으로 두기

Parent / 부모 (필수)

타깃 독자 / Target reader

  • 독자 profile: SaaS skeleton에서 tenant isolation을 어디까지 기본 제공할지 고민하는 백엔드 엔지니어.
  • 이미 안다고 가정하는 것: tenant_id, shared DB, schema-per-tenant.
  • 처음 듣는다고 가정하는 것: multi-tenancy를 default feature가 아니라 opt-in guardrail과 migration trigger로 다루는 방식.

도입 / Hook

Multi-tenancy는 SaaS에서 중요하지만, skeleton에 처음부터 강하게 박아 넣기 어렵습니다. 모든 query에 tenant predicate를 강제하고, tenant resolver filter를 만들고, admin tenant switching을 열고, schema-per-tenant까지 고려하면 single-tenant 서비스에도 비용이 따라옵니다. 반대로 아무 계약도 없으면 나중에 tenant를 얹을 때 권한, idempotency, logging, repository 경계가 한꺼번에 흔들립니다.

ca-tmpl은 이 사이에서 opt-in 방향을 택했습니다. 기본은 APP_TENANT_ENABLED=false이고, shared DB + tenant_id 방향을 문서화하되 현재 구현은 registry, capability, idempotency scope, runbook stub 같은 foundation에 머뭅니다. repository-level tenant filter와 cross-tenant E2E isolation은 아직 planned입니다. 이 글은 multi-tenancy를 “완성했다”고 말하지 않고, 어디까지 foundation을 깔았는지 정리합니다.

본문 outline / Body outline

  1. multi-tenancy를 skeleton 기본값으로 강제하지 않는 이유.
  2. shared DB + tenant_id와 schema/db-per-tenant의 trade-off.
  3. 현재 구현된 registry/capability/idempotency foundation.
  4. 아직 없는 tenant resolver와 repository filter.
  5. migration trigger와 운영 검증 없음.

본문 / Body

Multi-tenancy의 첫 갈림길은 격리 수준입니다. 모든 tenant를 같은 DB와 table에 두고 tenant_id column으로 나누는 방식은 운영이 단순합니다. 반면 schema-per-tenant나 db-per-tenant는 isolation은 강하지만 migration, backup, connection pool, monitoring 비용이 빠르게 늘어납니다. ca-tmpl은 B2B 초기 단계, tenant 수 수십에서 수백 정도의 가정을 두고 shared DB + tenant_id를 baseline 후보로 잡았습니다.

하지만 이 선택은 “항상 shared DB가 낫다”는 뜻이 아닙니다. 규제 산업, data residency 요구, enterprise tier처럼 격리를 상품 가치로 팔아야 하는 경우에는 schema나 DB를 나누는 쪽이 맞을 수 있습니다. 그래서 ca-tmpl canonical은 migration trigger도 함께 기록합니다. 규제 요구, tenant 수와 row 수 증가, enterprise tier 등장 같은 조건이 생기면 Pool 모델에서 더 강한 isolation으로 넘어갈 수 있다는 판단입니다.

현재 코드로 구현된 것은 storage isolation 전체가 아니라 foundation입니다. env registry에는 APP_TENANT_ENABLED가 있고, header registry에는 X-Tenant-Id가 있습니다. capability registry에는 CROSS_TENANT_ADMIN이 존재합니다. error-code registry에는 tenant 미지원 상태에서 tenant header가 들어왔을 때의 TENANT_NOT_SUPPORTED가 정의되어 있고, cross-tenant mismatch runbook stub도 있습니다.

application layer에도 일부 표현이 있습니다. UseCaseCapability에는 crossTenantAdmin flag가 있습니다. 이것은 tenant 경계를 넘는 admin use case가 명시적으로 선언해야 하는 capability입니다. idempotency 쪽에는 IdempotencyScope가 single-tenant triple뿐 아니라 tenant를 앞에 둔 4-tuple을 표현할 수 있습니다. tenant가 활성화되면 idempotency key 충돌도 tenant boundary 안에서 해석되어야 하기 때문입니다.

다만 중요한 enforcement가 아직 없습니다. request에서 tenant를 해석하는 tenant resolver filter는 구현됐다고 말할 수 없습니다. repository 진입점에서 tenant_id predicate를 강제하는 rule도 아직 planned입니다. CROSS_TENANT_ADMIN을 가진 admin만 X-Tenant-Id header로 tenant switching을 할 수 있다는 정책은 문서/registry 수준에 가깝고, E2E isolation으로 검증된 상태는 아닙니다.

이 경계가 이 글의 핵심입니다. ca-tmpl은 multi-tenancy를 처음부터 모든 서비스에 강제하지 않습니다. 대신 나중에 tenant를 열 때 필요한 vocabulary와 일부 cross-cutting surface를 미리 잡아 둡니다. header, env key, capability, idempotency scope, runbook link가 그 foundation입니다. 반면 실제 data isolation은 repository filter와 E2E test가 들어와야 닫힙니다.

따라서 면접이나 블로그에서 말할 때도 “multi-tenancy를 구현했다”보다 “multi-tenancy를 opt-in으로 열 수 있게 foundation을 만들었고, storage isolation enforcement는 planned로 남겼다”가 정확합니다. 이 차이를 숨기지 않는 것이 오히려 설계 이해를 더 잘 보여줍니다.

코드 예제 / Code samples (있다면)

# 출처: [[wiki/projects/ca-tmpl/multi-tenancy-isolation-patterns]]
# 실제 파일: docs/registries/env-keys.yaml, ca-tmpl @f6fbd4e196b4
- name: APP_TENANT_ENABLED
  type: boolean
  default: false
  allowed_values: [true, false]
  reload_policy: restart-only
  owner_branch: feature-tenant-context-policy
# 출처: [[wiki/projects/ca-tmpl/multi-tenancy-isolation-patterns]]
# 실제 파일: docs/registries/capabilities.yaml, ca-tmpl @f6fbd4e196b4
- name: CROSS_TENANT_ADMIN
  scope: use_case_method
  enforcement: archunit
  annotation: "@UseCaseCapability(crossTenantAdmin = true)"
// 출처: [[wiki/projects/ca-tmpl/multi-tenancy-isolation-patterns]]
// 실제 파일: application-core/.../UseCaseCapability.java, ca-tmpl @f6fbd4e196b4
public @interface UseCaseCapability {
  TransactionMode transactionMode();
  Idempotency idempotency();
  RepositoryAccess repositoryAccess();
  boolean crossTenantAdmin() default false;
}
// 출처: [[wiki/projects/ca-tmpl/multi-tenancy-isolation-patterns]]
// 실제 파일: application-core/.../IdempotencyScope.java, ca-tmpl @f6fbd4e196b4
public record IdempotencyScope(
    String tenant, String principal, String idempotencyKey, String useCaseName) {

  public static IdempotencyScope of(
      String tenant, String principal, String idempotencyKey, String useCaseName) {
    requirePresent("principal", principal);
    requirePresent("idempotencyKey", idempotencyKey);
    requirePresent("useCaseName", useCaseName);
    String normalizedTenant = (tenant == null || tenant.isBlank()) ? null : tenant;
    return new IdempotencyScope(normalizedTenant, principal, idempotencyKey, useCaseName);
  }
}

Sources / 근거 (canonical 인용 필수, derived layer 의무)

사실 vs 의견 / Fact vs opinion 구분

  • 사실: ca-tmpl에는 APP_TENANT_ENABLED, X-Tenant-Id, CROSS_TENANT_ADMIN, tenant 관련 error code/runbook stub, UseCaseCapability.crossTenantAdmin, tenant-aware IdempotencyScope가 존재한다. 근거: wiki/projects/ca-tmpl/multi-tenancy-isolation-patterns
  • 사실: repository-level tenant predicate 강제, tenant resolver filter, cross-tenant E2E isolation은 구현/검증됐다고 말하지 않는다. 근거: wiki/projects/ca-tmpl/multi-tenancy-isolation-patterns
  • 사실: 운영 배포, tenant isolation audit, penetration test 결과는 없다. 근거: wiki/projects/ca-tmpl/multi-tenancy-isolation-patterns
  • 의견: ca-tmpl 같은 skeleton에서는 multi-tenancy를 default feature가 아니라 opt-in foundation으로 두는 편이 적용 범위를 넓힌다.
  • 알지 못하는 것: 실제 tenant 수, row 수, noisy neighbor metric, schema/db-per-tenant migration 경험.

답할 수 있는 범위 / Answer boundary

  • 자신 있게 답할 수 있는 후속 질문:
    • 왜 schema-per-tenant를 skeleton 기본값으로 두지 않았는가?
    • X-Tenant-Id header를 왜 admin only로 제한해야 하는가?
    • idempotency scope에 tenant dimension이 왜 필요한가?
  • 다음 글로 넘길 부분:
    • tenant resolver filter 구현.
    • repository-level tenant_id predicate 강제.
    • cross-tenant E2E isolation과 audit evidence.

게시 체크리스트 / Publish checklist

  • 모든 사실 주장에 canonical 링크 있음
  • 사실 vs 의견 분리 명시됨
  • 금지 마케팅 표현 없음
  • 코드 예제 출처 명시
  • 타깃 독자 가정과 톤 일치
  • /lint 통과
  • 게시 URL 기록 (게시 후):