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

147 lines
9.6 KiB
Markdown

---
title: Multi-tenancy를 기본값이 아니라 Opt-in 계약으로 두기
source_type: blog
status: verified
confidence: high
tags: [blog, ca-tmpl, multi-tenancy, saas]
related_projects: [ca-tmpl]
last_reviewed: 2026-07-03
canonical_sources:
- wiki/projects/ca-tmpl/multi-tenancy-isolation-patterns
audience: backend-engineer
target_publish:
status_label: 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
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 (있다면)
```yaml
# 출처: [[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
```
```yaml
# 출처: [[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)"
```
```java
// 출처: [[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;
}
```
```java
// 출처: [[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-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
- [x] 모든 사실 주장에 canonical 링크 있음
- [x] 사실 vs 의견 분리 명시됨
- [x] 금지 마케팅 표현 없음
- [x] 코드 예제 출처 명시
- [x] 타깃 독자 가정과 톤 일치
- [x] `/lint` 통과
- [ ] 게시 URL 기록 (게시 후):
## Related / 관련
- 후속 글 후보: [[wiki/blog/ca-tmpl-security-baseline-jwt-actuator-secrets-2026-07-02]]
- 후속 글 후보: [[wiki/blog/ca-tmpl-idempotency-key-design-2026-07-02]]