멀티테넌시 스토어 아키텍처 설계 보고서
Problem & Condition
Definition of Problem Context
멀티테넌시 시스템의 설계 및 구축 과정을 상세히 기술합니다. 이 플랫폼은 초기 기획 단계부터 단일 인프라 위에서 다수의 독립적인 브랜드를 효율적으로 운영해야 하는 비즈니스 목표를 가지고 있었습니다. 이러한 요구사항을 충족시키기 위해 여러 아키텍처 전략을 검토했으며, 그 과정에서 기술적 타당성과 비즈니스적 영향을 종합적으로 분석하여 최종 설계를 결정했습니다. 본 문서는 각 설계 선택의 배경과 근거를 명확히 하고, 최종적으로 채택된 아키텍처가 플랫폼의 확장성과 운영 효율성에 어떻게 기여하는지 분석하는 것을 목표로 합니다.
Definition of Solution Strategies and Trade-offs
이 플랫폼의 핵심 아키텍처 과제는 단일 애플리케이션 인프라 위에서 콘텐츠 관리, 결제 처리, 구독 시스템, 음식 배달 기능을 통합적으로 제공하는 여러 독립 브랜드를 지원하는 것이었습니다. 각 브랜드(테넌트)는 고유의 정체성과 기능 설정을 유지하면서도, 공유된 자원을 통해 운영 비용을 최소화해야 했습니다. 이는 단순히 기술적인 도전을 넘어, 신규 브랜드를 신속하게 시장에 진입시키고 플랫폼 전체의 유지보수 비용을 절감하여 비즈니스 민첩성을 확보하기 위한 전략적 결정이었습니다.
이러한 배경 하에, 다음과 같은 핵심 요구사항과 제약 조건이 도출되었습니다.
- 핵심 요구사항
- 데이터 격리 (Data Isolation): 각 테넌트의 고객 정보, 상품, 주문 데이터는 다른 테넌트로부터 완벽하게 분리되어야 합니다. 이는 시스템의 신뢰성과 보안의 기본 전제조건으로, 설계상 가장 높은 우선순위를 가졌습니다.
- 브랜드 커스터마이징 (Brand Customization): 테넌트별로 독립적인 브랜딩(로고, 테마 색상 등)과 기능 설정을 적용할 수 있어야 합니다. 예를 들어, 특정 테넌트는 배달 기능을 활성화하고 다른 테넌트는 쇼핑 기능만 사용하도록 유연하게 제어할 수 있는 구조가 필요했습니다. 이는 각 브랜드의 고유한 시장 전략을 지원하기 위해 필수적이었습니다.
- 신속한 확장성 (Rapid Expansion): 새로운 브랜드를 입점시킬 때, 최소한의 개발 노력과 시간으로 서비스를 론칭할 수 있어야 합니다. 이는 플랫폼의 비즈니스 확장 속도와 직결되는 요구사항으로, 아키텍처는 신규 테넌트 추가가 거의 자동화될 수 있는 수준의 유연성을 갖춰야 했습니다.
- 운영 효율성 (Operational Efficiency): 모든 테넌트가 서버, 데이터베이스 등 핵심 인프라를 공유함으로써 전체적인 운영 및 관리 오버헤드를 최소화해야 합니다. 이는 플랫폼의 장기적인 수익성과 직결되는 목표입니다.
- 기술 스택 제약 조건
- 백엔드 프레임워크는 Python 기반의 FastAPI를 사용해야 했습니다.
- 데이터베이스는 SQLite를 사용하며, 데이터 접근은 SQLAlchemy ORM을 통해 이루어져야 했습니다.
Solution
Multi-tenancy Architecture Strategies and Trade-offs
멀티테넌시 데이터베이스 전략을 선택하는 것은 데이터 격리 수준, 운영 비용, 그리고 향후 확장성 사이의 균형을 맞추는 매우 중요한 아키텍처 결정입니다. 이 플랫폼의 비즈니스 요구사항과 기술적 제약 조건을 고려하여, 우리는 세 가지 주요 후보 전략을 심도 있게 검토했습니다. 각 전략은 뚜렷한 장단점을 가지며, 플랫폼의 장기적인 목표에 미치는 영향이 달랐습니다.
전략 1: 독립 데이터베이스 (Separate Databases)
테넌트마다 별도의 데이터베이스 인스턴스를 할당하는 방식으로, 물리적 수준에서 데이터를 분리합니다.
| 요구사항 충족 | 아키텍처 비용/리스크 |
|---|---|
| 완벽한 데이터 격리: 물리적으로 데이터베이스가 분리되어 있어 데이터 유출 위험이 원천적으로 차단됩니다. | 높은 운영 비용: 테넌트 수에 비례하여 데이터베이스 인스턴스 비용과 관리 리소스가 증가합니다. |
| 테넌트별 최적화 용이: 각 데이터베이스를 테넌트의 특성에 맞춰 개별적으로 최적화할 수 있습니다. | 복잡한 관리: 스키마 변경, 백업, 복구 등 모든 관리 작업을 모든 테넌트에 걸쳐 반복해야 합니다. |
| 보안 및 규정 준수 용이: 특정 규제가 요구하는 물리적 데이터 분리 요건을 쉽게 충족시킬 수 있습니다. | 느린 확장 속도: 신규 테넌트 추가 시마다 데이터베이스 프로비저닝 과정이 필요하여 확장이 더딥니다. |
분석: 이 전략은 가장 강력한 ‘데이터 격리’를 제공하지만, 테넌트가 증가할수록 운영 비용과 관리 복잡성이 기하급수적으로 증가합니다. 이는 플랫폼의 핵심 요구사항인 ‘운영 효율성’과 ‘신속한 확장성’에 정면으로 위배되어 이 플랫폼에는 부적합하다고 판단되었습니다.
전략 2: 공유 데이터베이스, 분리 스키마 (Shared Database, Separate Schemas)
단일 데이터베이스 인스턴스 내에서 테넌트별로 논리적인 스키마를 분리하여 데이터를 격리하는 방식입니다.
| 요구사항 충족 | 아키텍처 비용/리스크 |
|---|---|
| 높은 수준의 데이터 격리: 논리적으로 스키마가 분리되어 있어 데이터 격리 수준이 높습니다. | 복잡한 스키마 마이그레이션: 공통 스키마 변경 시 모든 테넌트의 스키마를 개별적으로 마이그레이션해야 합니다. |
| 비용 효율성: 독립 데이터베이스 방식보다 인프라 비용이 저렴합니다. | 애플리케이션 복잡도 증가: 요청마다 적절한 스키마로 동적으로 연결을 전환하는 로직이 필요합니다. |
| 데이터베이스 자원 공유: 유휴 상태의 테넌트 자원을 다른 테넌트가 활용할 수 있어 자원 효율성이 높습니다. | 교차 테넌트 쿼리 어려움: 여러 테넌트의 데이터를 통합하여 분석하는 것이 복잡합니다. |
분석: 이 전략은 비용과 격리 수준 사이에서 합리적인 절충안을 제시합니다. 하지만 모든 테넌트에 걸쳐 스키마 마이그레이션을 수행하는 복잡성은 여전히 ‘신속한 확장성’에 걸림돌이 될 수 있었습니다.
전략 3: 공유 데이터베이스, 공유 스키마 (Shared Database, Shared Schema)
모든 테넌트가 하나의 데이터베이스와 스키마를 공유하며, 각 테이블에 tenant_id와 같은 식별자 컬럼을 추가하여 애플리케이션 레벨에서 데이터를 구분하는 방식입니다.
| 요구사항 충족 | 아키텍처 비용/리스크 |
|---|---|
| 최고의 운영 효율성: 단일 데이터베이스와 스키마만 관리하면 되므로 운영이 매우 간편합니다. | 애플리케이션 레벨 격리 의존: 데이터 격리를 전적으로 애플리케이션 코드에 의존하므로, 코드 오류 시 데이터 유출 위험이 존재합니다. |
| 매우 빠른 확장성: 신규 테넌트 추가는 tenants 테이블에 행을 추가하는 것만으로 완료되어 확장이 매우 빠릅니다. | ‘시끄러운 이웃’ 문제: 특정 테넌트가 과도한 리소스를 사용하면 다른 테넌트의 성능에 영향을 줄 수 있습니다. |
| 개발 단순성: 개발자는 단일 스키마만 고려하면 되므로 개발 과정이 단순화됩니다. | 데이터베이스 커스터마이징 불가: 테넌트별로 데이터베이스 스키마를 다르게 구성할 수 없습니다. |
분석: 이 전략은 ‘운영 효율성’과 ‘신속한 확장성’ 측면에서 압도적인 이점을 제공했습니다. 이 전략이 데이터 격리의 책임을 애플리케이션 계층에 부여하지만, 우리는 이 리스크가 견고하고 타협 불가능한 미들웨어 및 의존성 주입 패턴을 통해 체계적으로 완화될 수 있다고 판단했습니다. 이 접근 방식은 격리 로직을 중앙화하여 잠재적 취약점을 통제 가능하고 테스트 가능한, 아키텍처 수준에서 강제되는 안전장치로 전환합니다.
이 플랫폼의 비즈니스 목표인 빠른 시장 진입과 운영 비용 최소화를 고려했을 때, ‘공유 데이터베이스, 공유 스키마’ 전략이 가장 합리적인 선택이었습니다.
Implementation: Shared Database, Shared Schema Architecture
여러 후보 전략에 대한 심도 있는 트레이드오프 분석 끝에, 이 플랫폼은 ‘공유 데이터베이스, 공유 스키마’ 전략을 최종적으로 채택했습니다. 이 결정은 비용 효율성을 극대화하고 신규 브랜드의 신속한 입점을 지원하여 비즈니스 확장 유연성을 확보하려는 플랫폼의 핵심 전략과 직접적으로 부합했습니다. 단일 코드베이스와 인프라를 통해 모든 테넌트를 관리함으로써 개발 및 운영 리소스를 최소화하고, 이를 통해 확보된 자원을 핵심 기능 개발에 집중할 수 있었습니다.
테넌트 식별 및 라우팅 (Tenant Identification and Routing)
모든 멀티테넌시 로직의 시작점은 현재 요청이 어느 테넌트에 속하는지 식별하는 것입니다. 이 과정은 tenant_aware.py 미들웨어에서 처리됩니다.
- 미들웨어는 모든 수신 HTTP 요청의 헤더에서 host 또는 프록시 환경을 위한 x-forwarded-host 값을 추출합니다.
- 추출된 도메인에서 www. 또는 api. 같은 일반적인 서브도메인을 정규화 과정을 통해 제거하여 핵심 도메인을 식별합니다. 이 정규화는 다양한 서브도메인으로 접근하더라도 동일한 테넌트로 인식되도록 보장하는 중요한 단계입니다.
- 식별된 도메인을 기준으로 데이터베이스의 tenants 테이블을 조회하여 현재 요청에 대한 테넌트 컨텍스트를 확립합니다.
데이터 격리 메커니즘 (Data Isolation Mechanism)
데이터 격리 메커니즘의 핵심은 아키텍처 강제 계층 역할을 하는 FastAPI의 의존성 주입 시스템에 기반합니다.
- 사용자, 상품, 주문 등 테넌트에 종속되는 모든 테이블은 tenant_id 외래 키(Foreign Key)를 포함하도록 설계되었습니다.
- 모든 데이터 접근 엔드포인트에 get_current_tenant 함수를 필수 의존성으로 주입함으로써, 유효한 테넌트 컨텍스트 없이는 어떠한 쿼리도 실행될 수 없음을 보장합니다.
- 이 설계는 개발자가 데이터 격리를 ‘선택적으로 제외’하는 것을 불가능하게 만들어, 개발자의 규율에 의존하는 대신 멀티테넌시 데이터 유출 버그의 가장 흔한 유형을 아키텍처 수준에서 원천적으로 제거합니다. 모든 데이터 접근이 자동으로 현재 테넌트의 범위 내에서만 이루어지도록 강제됩니다.
테넌트별 커스터마이징 (Per-Tenant Customization)
각 테넌트는 독립적인 기능 조합을 가질 수 있도록 유연하게 설계되었습니다.
- Tenant 모델 내 features라는 JSON 타입의 필드를 활용하여 기능 토글(Feature Toggles) 시스템을 구현했습니다.
- 관리자는 이 필드를 통해 각 테넌트의 기능(예: content, shopping, payment, reviews 등)을 활성화하거나 비활성화할 수 있습니다.
- 예를 들어, 전자상거래에 집중하는 테넌트는 shopping, payment, reviews 기능을 활성화하고, 콘텐츠 중심의 파트너는 content와 analytics 기능만 사용하도록 설정할 수 있습니다. 이를 통해 각 테넌트의 비즈니스 모델에 맞춘 서비스 제공이 가능해졌습니다.
전략적 기능: 프록시 테넌트 (Strategic Feature: Proxy Tenant)
‘신속한 확장성’ 요구사항을 극대화하기 위해 ‘프록시 테넌트’라는 전략적 기능을 구현했습니다. 이 기능은 화이트 라벨링 또는 프랜차이즈 패턴의 아키텍처적 구현입니다.
- 하나의 테넌트가 자체 도메인과 브랜딩은 유지하면서, 다른 기존 테넌트의 비즈니스 로직(상품, 기능 설정 등)을 그대로 공유하여 사용할 수 있도록 합니다.
- 예를 들어, 한 테넌트가 자체 브랜딩을 유지하면서 다른 테넌트의 비즈니스 로직을 공유하는 방식입니다. 이 경우, 신규 브랜드는 별도의 백엔드 설정이나 데이터베이스 구성 없이 즉시 서비스를 시작할 수 있습니다.
- 이를 통해 신규 파트너가 시장에 진입하는 장벽을 극적으로 낮춤으로써, 플랫폼의 ‘신속한 확장성’이라는 비즈니스 목표를 직접적으로 가속화하는 강력한 도구가 되었습니다.
Effects
멀티테넌시 아키텍처의 도입은 이 플랫폼의 기술적 기반을 견고히 하고 비즈니스적 유연성을 크게 향상시키는 긍정적 효과를 가져왔습니다. 그러나 모든 아키텍처 결정에는 트레이드오프가 따르듯이, ‘공유 스키마’ 방식이 가진 내재적 리스크를 인지하고 이를 관리하기 위한 보완 설계 또한 중요합니다. 본 섹션에서는 도입 효과를 종합적으로 평가하고, 잠재적 리스크와 그 완화 전략, 그리고 향후 발전 과제를 논합니다.
-
운영 비용 절감: 단일 서버와 데이터베이스 인스턴스를 모든 테넌트가 공유함으로써 인프라 유지보수, 모니터링, 백업 등에 소요되는 비용과 인력을 획기적으로 절감했습니다. 이는 플랫폼의 규모가 커져도 비용 증가를 완만하게 만들어 수익성을 개선하는 데 기여했습니다.
-
개발 및 배포 효율성 증대: 모든 테넌트가 단일 코드베이스를 공유하므로, 새로운 기능 개발이나 버그 수정이 한 번의 배포로 모든 테넌트에 일괄 적용됩니다. 이는 개발 생산성을 높이고, 배포 파이프라인을 단순화하여 신속하고 일관된 서비스 업데이트를 가능하게 했습니다.
-
비즈니스 확장 유연성: ‘프록시 테넌트’와 ‘기능 토글’은 플랫폼의 가장 강력한 경쟁 우위가 되었습니다. 새로운 브랜드가 입점할 때, 복잡한 인프라 구축 과정 없이 tenants 테이블에 레코드를 추가하고 설정을 조정하는 것만으로 며칠 내에 서비스를 론칭할 수 있게 되었습니다. 이는 시장 변화에 민첩하게 대응하고 새로운 비즈니스 기회를 빠르게 포착하는 원동력이 되었습니다.
Defense-in-Depth 완화 전략
‘공유 스키마’ 방식의 가장 큰 내재적 리스크는 데이터 격리를 전적으로 애플리케이션 로직에 의존한다는 점입니다. 개발자의 실수로 tenant_id 필터가 누락될 경우, 한 테넌트의 데이터가 다른 테넌트에게 유출될 수 있는 가능성이 핵심 리스크입니다. 이 리스크를 통제하기 위해, 우리는 다음과 같은 다층적인 방어 설계를 적용했습니다.
-
계층 1: 경계 (tenant_aware.py 미들웨어): 이는 모든 요청의 필수 진입점입니다. 모든 수신 요청은 이 계층에서 검증되며, 요청 헤더에서 테넌트를 식별할 수 없는 경우 비즈니스 로직에 도달하기 전에 즉시 거부됩니다.
-
계층 2: 강제 (의존성 주입): 경계를 통과한 요청이라도, 이후의 모든 동작(특히 데이터베이스 상호작용)은 FastAPI 의존성 주입 시스템을 통해 설정된 테넌트 컨텍스트 내에서 강제로 실행됩니다. 이는 아키텍처 수준에서 격리를 강제하는 핵심 메커니즘입니다.
-
계층 3: 정책 (중앙화된 데이터 접근 로직): 모든 쿼리를 중앙화된 서비스 또는 리포지토리 계층을 통해 전달함으로써, 테넌트 범위 쿼리라는 정책이 일관되게 적용됩니다. 이 계층은 의존성 주입 시스템을 활용하여, 안전장치를 우회할 수 있는 개별적인 직접 쿼리를 방지합니다.
이러한 설계는 개발자가 tenant_id 필터링을 ‘기억’할 필요 없이, 프레임워크와 아키텍처 수준에서 데이터 격리를 보장합니다. 이는 사람의 실수를 시스템적으로 방지하는 효과적인 리스크 완화 전략입니다.
Future Challenges & Roadmap
현재의 멀티테넌시 아키텍처는 이 플랫폼의 비즈니스 목표를 성공적으로 지원하며 높은 안정성과 효율성을 입증했습니다. 이 견고한 기반 위에서, 향후 플랫폼의 성장에 따라 다음과 같은 과제들을 지속적으로 검토하고 발전시켜 나갈 것입니다.
-
‘시끄러운 이웃(Noisy Neighbor)’ 문제 방지: 특정 테넌트의 과도한 리소스 사용이 다른 테넌트의 성능에 영향을 미치는 것을 방지하기 위해, 테넌트별 리소스 사용량을 모니터링하고 제한하는 메커니즘을 강화할 계획입니다.
-
대규모 테넌트를 위한 데이터베이스 확장: 향후 특정 테넌트의 데이터 규모가 폭발적으로 증가할 경우를 대비하여, 해당 테넌트의 데이터를 물리적으로 분리하는 tenant_id 기반의 데이터베이스 파티셔닝(Partitioning) 전략 도입을 검토할 수 있습니다.
이러한 과제들은 현재 아키텍처의 유연성을 바탕으로 점진적으로 도입될 수 있으며, 이 플랫폼이 장기적으로 안정적인 서비스를 제공하는 데 기여할 것입니다.