Skip to main content

테넌트 모듈

테넌트 모듈은 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;

관련 문서