테넌트 모듈
테넌트 모듈은 Lumie 플랫폼의 멀티테넌시를 관리하는 핵심 모듈입니다. 학원(테넌트) 등록, 테넌트별 설정 관리를 담당합니다. 2026-05-12 RLS 전환 이후 테넌트 프로비저닝은 public.tenants 안에서 끝나며, 별도 스키마 생성이나 Flyway 동적 실행은 없습니다.
서비스 개요
- 배포: lumie-backend 모놀리스의
modules/tenant - 데이터베이스: PostgreSQL
public스키마 (테넌트 메타데이터) - 주요 연동:
TenantCreatedEvent발행, billing 모듈의 after-commit 리스너가 구독 생성 처리
주요 기능
1. 테넌트 관리
- 테넌트 생성, 조회, 수정
- 테넌트 상태 관리 (PENDING → PROVISIONING → ACTIVE)
- 슬러그 기반 테넌트 식별
2. 테넌트 프로비저닝
public.tenants저장과 상태 전이로 완료 (스키마 생성 없음)- PENDING → PROVISIONING → ACTIVE 상태 머신 유지 (빌링 훅 시점 확보 목적)
- 프로비저닝은 사실상 즉시 완료 (별도 비동기 작업 불필요)
3. 테넌트 설정
- 학원명, 대표 연락처, 주소 관리
- 학원 로고 오브젝트 키 관리
- 커스텀 ID 관리
- 커스텀 도메인 공개 조회용 값 보관
- 관리자 사이드바 숨김 메뉴 목록 관리
헥사고날 아키텍처
modules/tenant/src/main/java/com/lumie/tenant/
├── adapter/
│ ├── in/web/ # REST 컨트롤러 (TenantController)
│ ├── in/internal/ # TenantService 구현체
│ └── out/{persistence,storage}/ # JPA 저장소, 로고 스토리지 어댑터
├── application/
│ ├── service/ # TenantCommandService, TenantRegistrationService
│ └── port/out/ # TenantPersistencePort
└── domain/
└── entity/Tenant.java # extends BaseEntity (public.tenants, RLS 미적용)
테넌트 프로비저닝
흐름 (RLS 전환 이후)
스키마 생성(CREATE SCHEMA)과 Flyway 동적 실행은 없습니다. PENDING → PROVISIONING → ACTIVE 상태 머신은 유지하지만, billing 모듈은 직접 호출하지 않고 TenantCreatedEvent를 after-commit 리스너로 소비합니다.
TenantCommandService
TenantCommandService는 REST 경로의 생성, 수정, 삭제, 정지, 재활성화 명령을 처리합니다. 생성 시 Tenant.create(...)로 PENDING 테넌트를 저장한 뒤 startProvisioning()과 activate()를 호출하고, TenantCreatedEvent를 발행합니다. 원장 가입 경로는 TenantServiceAdapter를 통해 TenantRegistrationService.registerTenant(...)를 호출하며, 같은 상태 전이와 저장 흐름을 사용합니다.
Flyway 마이그레이션 구조
app/src/main/resources/db/migration/public/
├── ...
└── V18__rls_baseline.sql # 모든 테넌트 스코프 테이블에 RLS 정책 일괄 적용
db/migration/tenant/ 디렉토리(기존 50개 파일)는 RLS 전환 시 삭제되었습니다. 신규 테넌트 스코프 테이블 마이그레이션은 public/ 하위에 추가합니다.
API 명세
테넌트 조회
GET /v1/tenants/{slug}
{
"id": 1,
"slug": "awesome-academy",
"name": "어썸 학원",
"status": "ACTIVE",
"hiddenSidebarItemIds": ["attendance"],
"createdAt": "2024-01-01T00:00:00Z"
}
테넌트 수정
PATCH /v1/tenants/{slug}
{
"name": "어썸 학원",
"phone": "02-1234-5678",
"address": "서울시 강남구",
"customId": "awesome-academy",
"hiddenSidebarItemIds": ["attendance", "sms"]
}
hiddenSidebarItemIds는 관리자 사이드바에서 숨길 메뉴 ID 목록입니다. 요청 값은 소문자 영숫자와 하이픈 형식이어야 하며, 백엔드 허용 목록에 없는 ID는 400 Bad Request로 거부됩니다.
customId는 최초 설정 후 변경할 수 없습니다. 이미 값이 있는 테넌트에 다른 customId를 요청하면 도메인 검증에서 거부됩니다.
schemaName 필드는 RLS 전환(2026-05-12)으로 삭제되었습니다. TenantData, TenantResponse, CreateTenantResult, TenantInfo 모두 동일하게 해당 필드가 제거되었습니다.
internal-api
TenantService 인터페이스를 구현하여 auth 모듈, billing 모듈, student 모듈 등이 테넌트 정보를 조회하거나 검증할 수 있도록 합니다.
// libs/internal-api/src/main/java/com/lumie/tenant/api/TenantService.java
public interface TenantService {
Optional<TenantData> getTenant(Long tenantId);
Optional<TenantData> getTenantBySlug(String slug);
Optional<TenantData> getTenantByCustomId(String customId);
List<String> listActiveTenantSlugs();
List<TenantData> listActiveTenants();
ValidateTenantResult validateTenant(String slug);
CreateTenantResult createTenant(String instituteName, String businessRegistrationNumber,
String ownerEmail, String ownerName);
}
데이터베이스 스키마
공통 스키마 (public)
CREATE TABLE tenants (
id BIGSERIAL PRIMARY KEY,
slug VARCHAR(30) NOT NULL UNIQUE,
name VARCHAR(100) NOT NULL,
display_name VARCHAR(200),
status VARCHAR(20) NOT NULL,
owner_email VARCHAR(255),
plan_id VARCHAR(20) NOT NULL,
institute_name VARCHAR(200),
business_registration_number VARCHAR(12),
phone VARCHAR(30),
address VARCHAR(255),
logo_object_key VARCHAR(255),
custom_id VARCHAR(50) NOT NULL,
custom_domain VARCHAR(253),
hidden_sidebar_item_ids JSONB NOT NULL DEFAULT '[]'::jsonb,
version BIGINT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
tenant_settings 테이블은 V8에서 제거되었고, 로고는 tenants.logo_object_key에 저장합니다. schema_name 컬럼은 RLS 전환(V18)으로 삭제되었습니다.
트러블슈팅
테넌트 상태 확인
-- 상태 확인
SELECT id, slug, status FROM tenants WHERE slug = 'xxx';
-- 구독 프로비저닝이 누락된 경우: TenantCreatedEvent 처리와 subscriptions 상태를 함께 확인
SELECT * FROM event_publication WHERE listener_id LIKE '%TenantCreated%';
SELECT * FROM subscriptions WHERE tenant_id = 18;
RLS로 인해 데이터가 보이지 않는 경우
-- 현재 세션 GUC 확인
SELECT current_setting('app.tenant_id', true);
-- 특정 테넌트로 임시 조회 (psql 직접 접속 시)
SET LOCAL app.tenant_id = '18';
SELECT * FROM students LIMIT 5;
관련 문서
- 멀티테넌시 — RLS 아키텍처, 요청/비동기 라이프사이클, 크로스 테넌트 쿼리
- 인증 & 멀티테넌시 — 컨텍스트 전파 흐름
- Auth Service — 원장 등록 연동
- Billing Service — 구독 생성 연동