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 |
|
|
2026-07-03 |
|
backend-engineer | ready |
Multi-tenancy를 기본값이 아니라 Opt-in 계약으로 두기
Parent / 부모 (필수)
- 핵심 canonical: wiki/projects/ca-tmpl/multi-tenancy-isolation-patterns
- 관련 개념 문서: wiki/concepts/multi-tenancy-isolation-patterns - Pool/Silo/Bridge와 Hibernate multi-tenancy 전략의 일반 배경. 현재 이 글의 구현 사실 근거는 verified project canonical에 둔다.
타깃 독자 / 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
- multi-tenancy를 skeleton 기본값으로 강제하지 않는 이유.
- shared DB +
tenant_id와 schema/db-per-tenant의 trade-off. - 현재 구현된 registry/capability/idempotency foundation.
- 아직 없는 tenant resolver와 repository filter.
- 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 의무)
- wiki/projects/ca-tmpl/multi-tenancy-isolation-patterns - 이 글의 1차 canonical. opt-in policy, shared DB +
tenant_id, registry/capability/idempotency foundation, planned repository filter, 운영 미검증 경계를 따른다. - wiki/concepts/multi-tenancy-isolation-patterns - 관련 개념 문서. Pool/Silo/Bridge와 Hibernate strategy의 일반 비교 배경으로만 둔다.
사실 vs 의견 / Fact vs opinion 구분
- 사실: ca-tmpl에는
APP_TENANT_ENABLED,X-Tenant-Id,CROSS_TENANT_ADMIN, tenant 관련 error code/runbook stub,UseCaseCapability.crossTenantAdmin, tenant-awareIdempotencyScope가 존재한다. 근거: 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-Idheader를 왜 admin only로 제한해야 하는가?- idempotency scope에 tenant dimension이 왜 필요한가?
- 다음 글로 넘길 부분:
- tenant resolver filter 구현.
- repository-level
tenant_idpredicate 강제. - cross-tenant E2E isolation과 audit evidence.
게시 체크리스트 / Publish checklist
- 모든 사실 주장에 canonical 링크 있음
- 사실 vs 의견 분리 명시됨
- 금지 마케팅 표현 없음
- 코드 예제 출처 명시
- 타깃 독자 가정과 톤 일치
/lint통과- 게시 URL 기록 (게시 후):