본문으로 건너뛰기

테넌트 서비스

이 페이지는 tenant registry, lifecycle state, 공개 tenant 식별자, onboarding metadata, branding asset을 담당하는 플랫폼 모듈 lumie-backend/modules/tenant의 레퍼런스입니다.

소스 경로

Path역할
lumie-backend/modules/tenant/src/main/java/com/lumie/tenant/adapter/in/web/TenantController.java공개 및 인증 tenant endpoint
lumie-backend/modules/tenant/src/main/java/com/lumie/tenant/application/service/{TenantCommandService,TenantQueryService,TenantRegistrationService,TenantLogoService}.javalifecycle, 조회, owner-registration, logo 흐름
lumie-backend/modules/tenant/src/main/java/com/lumie/tenant/adapter/in/internal/TenantServiceAdapter.java다른 모듈과 scheduler가 소비하는 공개 프로세스 내부 tenant API
lumie-backend/modules/tenant/src/main/java/com/lumie/tenant/domain/entity/Tenant.java전역 tenant aggregate
lumie-backend/modules/tenant/src/main/resources/db/migration/public/V1__create_platform_tables.sql원래의 tenant registry 생성
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}.sqlbranding, 공개 라우팅 ID, custom domain, onboarding completion의 현재 source of truth

공개 인터페이스

Endpoint목적
GET /v1/tenants/public/by-custom-id/{customId}white-label path segment에서 공개 tenant 해석
GET /v1/tenants/public/by-domain?host=...custom domain에서 공개 tenant 해석
POST /v1/tenantstenant 레코드 생성 및 활성화
GET /v1/tenants/{slug}, GET /v1/tenantstenant 조회
PATCH /v1/tenants/{slug}tenant 정보, 연락처 필드, custom ID, 숨김 sidebar item 갱신
POST /v1/tenants/{slug}/complete-onboarding인증된 tenant OWNER의 onboarding completion 데이터 저장
DELETE /v1/tenants/{slug}tenant를 삭제 상태로 표시
POST /v1/tenants/{slug}/suspend, POST /v1/tenants/{slug}/reactivatelifecycle 상태 전이
POST /v1/tenants/{slug}/logo, DELETE /v1/tenants/{slug}/logo, GET /v1/tenants/{slug}/logotenant branding asset 관리

내부 인터페이스와 의존성

Surface역할
lumie-backend/libs/internal-api/src/main/java/com/lumie/tenant/api/TenantService.javatenant 조회, 검증, 활성 tenant 목록, tenant 생성, plan 갱신을 위한 공개 internal API
lumie-backend/libs/internal-api/src/main/java/com/lumie/tenant/api/TenantCreatedEvent.javabilling이 trial provisioning에 소비하는 AFTER_COMMIT event
TenantLogoStoragePortTenantLogoService가 사용하는 object-storage 추상화
CacheConfig의 cache keyslug 및 domain 기준 tenant 조회를 cache하고, update 시 명시적으로 evict

집계와 레지스트리 필드

Field area참고
slugheader, JWT claim, internal API 전반에서 쓰이는 안정적인 내부 식별자
customIdhomepage와 공개 라우팅에서 쓰는 white-label URL segment
customDomain선택적 host 기반 공개 라우팅 키
statusPENDING, PROVISIONING, ACTIVE, SUSPENDED, DELETED
logoObjectKeyobject storage 안의 branding asset 위치
onboardingCompletedAtowner onboarding 흐름 완료 시점을 표시
hiddenSidebarItemIdsfrontend에서 tenant가 숨기는 허용 메뉴 ID의 JSON 배열

런타임 흐름

일반적인 controller 경로에서도 TenantCommandService.createTenant(...)가 같은 lifecycle을 가집니다.

계약 참고 사항

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 같은 값을 예약하여, 공개 tenant 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"
}

TenantLogoServiceimage/png, image/jpeg, image/jpg, image/svg+xml, image/webp만 허용하며, 5 MiB를 넘는 파일은 거부합니다.

실패, 재시도, 관측성

  • tenant 생성은 이제 persistence-only입니다. 모듈은 더 이상 signup 시 tenant별 schema를 provision하거나 Flyway를 실행하지 않습니다.
  • TenantRegistrationServiceTenantCommandService.createTenant(...)는 둘 다 TenantCreatedEvent를 발행하며, downstream provisioning은 commit 이후에 일어납니다.
  • completeOnboarding(...)는 OWNER 인증을 요구하며, path의 slug가 인증된 tenant context와 일치하지 않으면 요청을 거부합니다.
  • TenantLogoService는 5 MiB 제한과 소수의 image MIME type allowlist를 강제합니다.
  • deleteTenant(...)는 tenant를 DELETED로 표시하며 registry row를 hard-delete하지 않습니다.
  • getPublicTenantByCustomDomain(...)는 cache된 query 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로 종료되고, tenant 모듈 테스트가 여전히 create, onboarding, public lookup 경로를 다룹니다.
  • TenantController.getPublicTenantByCustomDomain(...)가 여전히 host를 소문자로 바꾸고, TenantLogoService가 문서화된 MIME type 및 5 MiB 업로드 제한을 계속 강제합니다.

관련 페이지