테넌트 서비스
이 페이지는 테넌트 레지스트리, 수명주기 상태, 공개 테넌트 식별자,
온보딩 메타데이터, 브랜딩 자산을 담당하는 플랫폼 모듈
lumie-backend/modules/tenant의 레퍼런스입니다.
소스 경로
| 경로 | 역할 |
|---|---|
lumie-backend/modules/tenant/src/main/java/com/lumie/tenant/adapter/in/web/TenantController.java | 공개 및 인증 테넌트 엔드포인트 |
lumie-backend/modules/tenant/src/main/java/com/lumie/tenant/application/service/{TenantCommandService,TenantQueryService,TenantRegistrationService,TenantLogoService}.java | 수명주기, 조회, OWNER 등록, logo 흐름 |
lumie-backend/modules/tenant/src/main/java/com/lumie/tenant/adapter/in/internal/TenantServiceAdapter.java | 다른 모듈과 scheduler가 소비하는 공개 프로세스 내부 테넌트 API |
lumie-backend/modules/tenant/src/main/java/com/lumie/tenant/domain/entity/Tenant.java | 전역 테넌트 aggregate |
lumie-backend/modules/tenant/src/main/resources/db/migration/public/V1__create_platform_tables.sql | 원래의 테넌트 레지스트리 생성 |
lumie-backend/app/src/main/resources/db/migration/public/{V8__inline_tenant_logo,V9__rename_enterprise_to_max_and_add_custom_id,V10__add_custom_domain_fields,V50__custom_id_not_null,V68__tenant_onboarding_completed_at}.sql | branding, 공개 라우팅 ID, 커스텀 도메인, onboarding completion의 현재 기준 |
공개 인터페이스
| 엔드포인트 | 목적 |
|---|---|
GET /v1/tenants/public/by-custom-id/{customId} | white-라벨 path segment에서 공개 테넌트 해석 |
GET /v1/tenants/public/by-domain?host=... | 커스텀 도메인에서 공개 테넌트 해석 |
POST /v1/tenants | 테넌트 레코드 생성 및 활성화 |
GET /v1/tenants/{slug}, GET /v1/tenants | 테넌트 조회 |
PATCH /v1/tenants/{slug} | 테넌트 정보, 연락처 필드, custom ID, 숨김 sidebar item 갱신 |
POST /v1/tenants/{slug}/complete-onboarding | 인증된 테넌트 OWNER의 onboarding completion 데이터 저장 |
DELETE /v1/tenants/{slug} | tenant를 삭제 상태로 표시 |
POST /v1/tenants/{slug}/suspend, POST /v1/tenants/{slug}/reactivate | 수명주기 상태 전이 |
POST /v1/tenants/{slug}/logo, DELETE /v1/tenants/{slug}/logo, GET /v1/tenants/{slug}/logo | 테넌트 브랜딩 자산 관리 |
내부 인터페이스와 의존성
| 인터페이스 | 역할 |
|---|---|
lumie-backend/libs/internal-api/src/main/java/com/lumie/tenant/api/TenantService.java | 테넌트 조회, 검증, 활성 테넌트 목록, 테넌트 생성, plan 갱신을 위한 공개 내부 API |
lumie-backend/libs/internal-api/src/main/java/com/lumie/tenant/api/TenantCreatedEvent.java | billing이 trial 프로비저닝에 소비하는 AFTER_COMMIT event |
TenantLogoStoragePort | TenantLogoService가 사용하는 object-storage 추상화 |
CacheConfig의 캐시 키 | slug 및 domain 기준 테넌트 조회를 캐시하고, 업데이트 시 명시적으로 제거 |
집계와 레지스트리 필드
| 필드 영역 | 참고 |
|---|---|
slug | header, JWT claim, 내부 API 전반에서 쓰이는 안정적인 내부 식별자 |
customId | homepage와 공개 라우팅에서 쓰는 white-라벨 URL segment |
customDomain | 선택적 host 기반 공개 라우팅 키 |
status | PENDING, PROVISIONING, ACTIVE, SUSPENDED, DELETED |
logoObjectKey | object storage 안의 브랜딩 자산 위치 |
onboardingCompletedAt | OWNER onboarding 흐름 완료 시점을 표시 |
hiddenSidebarItemIds | frontend에서 tenant가 숨기는 허용 메뉴 ID의 JSON 배열 |
런타임 흐름
일반적인 컨트롤러 경로에서도 TenantCommandService.createTenant(...)가
같은 수명주기을 가집니다.
계약 참고 사항
customId는 의도적으로 최초 설정 후 변경할 수 없으며, route처럼 보이는
예약 값도 거부합니다.
// lumie-backend/modules/tenant/src/main/java/com/lumie/tenant/domain/entity/Tenant.java
if (alreadySet) {
if (!this.customId.equals(incoming)) {
throw new BusinessException(TenantErrorCode.CUSTOM_ID_IMMUTABLE,
"학원 ID는 한번 설정되면 변경할 수 없습니다.");
}
return;
}
엔티티는 또한 api, login, pricing, _next 같은 값을 예약하여,
공개 테넌트 landing page가 frontend route와 충돌하지 않게 합니다.
예시 계약
이 예시는 TenantController, CreateTenantRequest,
CompleteTenantOnboardingRequest, PublicTenantResponse, TenantResponse,
TenantLogoService에서 직접 가져왔습니다.
공개 테넌트 조회
GET /v1/tenants/public/by-custom-id/acme
HTTP/1.1 200 OK
{
"slug": "inst-acme",
"name": "Acme Academy",
"customId": "acme",
"logoUrl": "/api/v1/tenants/inst-acme/logo"
}
Owner 온보딩 완료
POST /v1/tenants/inst-acme/complete-onboarding
Content-Type: application/json
{
"phone": "02-555-0100",
"address": "Seoul",
"customId": "acme",
"hiddenSidebarItemIds": ["reviews", "sms"]
}
HTTP/1.1 200 OK
{
"slug": "inst-acme",
"status": "ACTIVE",
"customId": "acme",
"onboardingCompletedAt": "<timestamp>",
"hiddenSidebarItemIds": ["reviews", "sms"]
}
로고 업로드
POST /v1/tenants/inst-acme/logo
Content-Type: multipart/form-data
file=@logo.png
HTTP/1.1 200 OK
{
"slug": "inst-acme",
"logoUrl": "/api/v1/tenants/inst-acme/logo"
}
TenantLogoService는 image/png, image/jpeg, image/jpg,
image/svg+xml, image/webp만 허용하며, 5 MiB를 넘는 파일은 거부합니다.
실패, 재시도, 관측성
- 테넌트 생성은 이제 persistence-only입니다. 모듈은 더 이상 signup 시 tenant별 schema를 provision하거나 Flyway를 실행하지 않습니다.
TenantRegistrationService와TenantCommandService.createTenant(...)는 둘 다TenantCreatedEvent를 발행하며, downstream provisioning은 커밋 이후에 일어납니다.completeOnboarding(...)는 OWNER 인증을 요구하며, path의 slug가 인증된 테넌트 컨텍스트와 일치하지 않으면 요청을 거부합니다.TenantLogoService는 5 MiB 제한과 소수의 image MIME type allowlist를 강제합니다.deleteTenant(...)는 tenant를DELETED로 표시하며 registry row를 hard-delete하지 않습니다.getPublicTenantByCustomDomain(...)는 캐시된 쿼리 method에 도달하기 전에 controller가 들어온 host를 소문자로 정규화할 것을 전제합니다.
검증
cd lumie-backend
./gradlew :modules:tenant:test
./gradlew :modules:tenant:test --tests '*TenantCommandService*'
./gradlew :modules:tenant:test --tests '*TenantRegistrationService*'
예상 성공 신호:
- Gradle이
BUILD SUCCESSFUL로 종료되고, 테넌트 모듈 테스트가 여전히 create, onboarding, public 조회 경로를 다룹니다. TenantController.getPublicTenantByCustomDomain(...)가 여전히host를 소문자로 바꾸고,TenantLogoService가 문서화된 MIME type 및 5 MiB 업로드 제한을 계속 강제합니다.