본문으로 건너뛰기

테넌트 서비스

이 페이지는 테넌트 레지스트리, 수명주기 상태, 공개 테넌트 식별자, 온보딩 메타데이터, 브랜딩 자산을 담당하는 플랫폼 모듈 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}.sqlbranding, 공개 라우팅 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.javabilling이 trial 프로비저닝에 소비하는 AFTER_COMMIT event
TenantLogoStoragePortTenantLogoService가 사용하는 object-storage 추상화
CacheConfig의 캐시 키slug 및 domain 기준 테넌트 조회를 캐시하고, 업데이트 시 명시적으로 제거

집계와 레지스트리 필드

필드 영역참고
slugheader, JWT claim, 내부 API 전반에서 쓰이는 안정적인 내부 식별자
customIdhomepage와 공개 라우팅에서 쓰는 white-라벨 URL segment
customDomain선택적 host 기반 공개 라우팅 키
statusPENDING, PROVISIONING, ACTIVE, SUSPENDED, DELETED
logoObjectKeyobject storage 안의 브랜딩 자산 위치
onboardingCompletedAtOWNER onboarding 흐름 완료 시점을 표시
hiddenSidebarItemIdsfrontend에서 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"
}

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

실패, 재시도, 관측성

  • 테넌트 생성은 이제 persistence-only입니다. 모듈은 더 이상 signup 시 tenant별 schema를 provision하거나 Flyway를 실행하지 않습니다.
  • TenantRegistrationServiceTenantCommandService.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 업로드 제한을 계속 강제합니다.

관련 페이지