본문으로 건너뛰기

테넌트 스키마

목적

Lumie는 더 이상 tenant별 스키마 persistence를 사용하지 않습니다. 테넌트 소유 레코드는 공유 public 테이블에 있으며 tenant_id와 RLS로 격리됩니다. 이 페이지는 모델의 테넌트 범위 부분과 이를 변경하는 규칙을 설명합니다.

소스 경로

경로역할
V18__rls_baseline.sql기준선 테넌트 범위 테이블과 policy입니다
V26__rename_admin_tables_to_staff.sqlstaff 용어 마이그레이션입니다
V54__create_file_links.sql테넌트-safe file link 모델입니다
V55__create_announcement_class_targets.sqlannouncement targeting 조인 테이블입니다
V57__create_lecture_class_targets.sqllecture targeting 조인 테이블입니다
V62__add_read_receipt_tables.sqlannouncement와 textbook 읽음 추적입니다
V65__repair_read_receipt_and_file_download_rls.sql읽음/다운로드 테이블용 RLS 수리입니다
V81__assignment_redesign_expand.sqlassignment 워크플로 컬럼, submission answer 필드, assignment_targets를 추가합니다
V82__assignment_redesign_tenant_safe_indexes.sql테넌트-safe assignment redesign 인덱스와 복합 foreign key를 추가합니다
V83__drop_assignment_title_description.sql레거시 assignment title 및 description 필드를 제거합니다
V84__drop_assignment_type_and_draft.sqlassignment type과 draft 상태를 제거합니다
V85__assignment_exam_only_active_guard.sql비시험 레거시 행을 닫고 active 행을 linked-exam assignment로 제한합니다
V86__assignment_exam_fk_restrict.sqlassignment가 참조하는 exam 삭제를 방지합니다
V89__require_assignment_due_date.sql레거시 null assignment 마감일을 백필하고 assignments.due_date를 필수화합니다
V90__assignment_overdue_close_index.sql마감이 지난 active 과제 종료 쿼리를 위한 partial index를 추가합니다

도메인 그룹

그룹테이블비고
Identityusers, staff, staff_permissions, studentsuser 행은 테넌트 범위이며 role을 가집니다
Class, assignment, lectureclasses, class_enrollments, assignments, assignment_targets, assignment_submissions, lectures, lecture_class_targets대상자 조인은 조인 행에 tenant_id를 유지합니다
Contentannouncements, announcement_class_targets, qna_boards, textbooks, textbook_foldersread receipt와 file link도 테넌트 범위입니다
Examexams, questions, question_results, exam_results, omr_grading_jobs워커 콜백은 여전히 백엔드 소유 테넌트 테이블로 들어옵니다
Attendanceattendance_sessions, attendance_records세션 고유성은 수업/date 패턴별로 유지됩니다
Communicationsms_messages, sms_templates, ai_chat_messages, ai_conversationschat persistence는 여전히 백엔드 소유입니다
File 메타데이터file_metadata, file_links, file_download테넌트-safe FK 패턴은 (id, tenant_id) 쌍을 포함합니다

교차 모듈 참조 정책

모든 관계가 데이터베이스 FK인 것은 아닙니다. Lumie는 같은 모듈 경계를 지원하거나 테넌트-safe FK 형태가 명시적인 경우 하드 FK를 유지합니다. 하드 FK가 모듈을 너무 강하게 결합하면 느슨한 참조를 사용합니다.

예시는 다음과 같습니다.

  • file link는 교차 테넌트 첨부 실수를 막기 위해 (file_id, tenant_id)로 file metadata를 참조합니다
  • announcement와 lecture 대상자 조인은 tenant_id와 테넌트-safe 참조를 포함합니다
  • assignment에 연결된 exam result는 assignments.linked_exam_id, assignment_targets.assignment_id, assignment_submissions.exam_result_id에 대해 테넌트-safe 복합 참조를 사용합니다
  • tuition 테이블은 billing 모듈과 identity/수업 모듈이 분리된 경계로 남아 있으므로 일부 user/수업 개념을 하드 FK 없이 의미적으로 참조합니다

과제 재설계 테이블

assignments는 assignment aggregate 행으로 남아 있지만, V81은 workflow와 학생 대상 결과 제어를 추가합니다.

컬럼 그룹컬럼비고
제출 워크플로submission_mode, evaluation_mode, linked_exam_idActive 행은 EXAM_MANUALEXAM_AUTO를 사용하며, active 행에는 linked_exam_id가 필요합니다. 비시험 레거시 행은 V85에서 닫습니다.
마감일due_date모든 assignment에 필수입니다. 레거시 null 값은 V89에서 백필한 뒤 컬럼에 NOT NULL을 강제합니다.
결과 가시성show_score, show_pass_fail, show_grade, show_question_results, show_correct_answersActive assignment는 점수, pass/fail, 문항별 정오를 항상 공개합니다. 등급은 비활성이고, 제품에서 설정 가능한 값은 show_correct_answers뿐입니다.

assignment_targets는 redesign의 정식 대상자 테이블입니다.

컬럼역할
assignment_id배포되는 assignment 행입니다
target_typeCLASS 또는 STUDENT입니다
target_idCLASS일 때는 수업 ID, STUDENT일 때는 student ID입니다
tenant_id테넌트 범위 테이블 패턴에서 상속된 필수 RLS 경계입니다

기존 assignments.class_id는 예전 수업 중심 조회와의 호환성을 위해 여전히 채워집니다. 새로운 학생 가시성은 assignment_targets를 사용해야 합니다.

assignment_submissions는 학생이 수동 입력한 객관식 답안 payload와 인라인 채점으로 생성된 시험 결과를 저장합니다.

컬럼역할
answersEXAM_MANUAL용 수동 입력 객관식 답안을 담는 JSONB 맵입니다
exam_result_id인라인 수동 채점으로 생성된 시험 결과를 가리키는 선택적 링크입니다
passed연결된 시험 정책에서 나온 pass/fail 채점 결과입니다
grade_value스키마 호환성을 위해 남은 레거시 컬럼입니다. assignment는 등급을 노출하지 않습니다

V82는 필요한 인덱스를 만든 뒤 테넌트-safe 복합 foreign key를 추가합니다. 연결된 시험과 시험 결과 관계에는 tenant_id가 포함되므로, 교차 테넌트 ID 충돌이 데이터베이스 참조를 만족시킬 수 없습니다. V86은 연결된 exam 관계를 ON DELETE RESTRICT로 강화해 active assignment가 의존하는 시험지를 잃지 않게 합니다. V90은 스케줄된 마감 과제 종료 쿼리를 위해 active 과제에 대한 partial (tenant_id, due_date, id) index를 추가합니다.

변경 규칙

  1. 새로운 테넌트 범위 테이블은 tenant_id bigint not null을 포함해야 합니다.
  2. RLS는 활성화되고 강제되어야 합니다.
  3. insert/update 코드는 사용자 입력을 신뢰하지 말고 현재 테넌트 id를 써야 합니다.
  4. 값이 설계상 전역 고유가 아닌 한 unique index에는 tenant_id를 포함해야 합니다.
  5. 하드 FK를 선언하지 않는 교차 모듈 참조는 migration 주석에 문서화해야 합니다.

운영 실패 양상

증상가능성 높은 원인첫 확인 포인트
유효한 테넌트 데이터인데 결과가 비어 있음app.tenant_id 컨텍스트 누락호출자에서 테넌트 컨텍스트 설정을 검색합니다
교차 테넌트 쓰기가 거부됨RLS WITH CHECK가 쓰기를 차단함삽입된 tenant_id를 검증합니다
테넌트 간 duplicate-key 충돌unique index에 tenant_id가 없음migration 인덱스 정의를 점검합니다
워커 콜백이 job을 업데이트하지 못함tenantSlug 또는 테넌트 컨텍스트 누락/오류백엔드 콜백 listener와 테넌트 조회를 점검합니다

검증

rg -n "CREATE TABLE|tenant_id|ENABLE ROW LEVEL SECURITY|tenant_isolation" \
lumie-backend/app/src/main/resources/db/migration/public
rg -n "assignment_targets|linked_exam_id|exam_result_id|show_correct_answers|due_date" \
lumie-backend/app/src/main/resources/db/migration/public/V81__assignment_redesign_expand.sql \
lumie-backend/app/src/main/resources/db/migration/public/V82__assignment_redesign_tenant_safe_indexes.sql \
lumie-backend/app/src/main/resources/db/migration/public/V85__assignment_exam_only_active_guard.sql \
lumie-backend/app/src/main/resources/db/migration/public/V86__assignment_exam_fk_restrict.sql \
lumie-backend/app/src/main/resources/db/migration/public/V89__require_assignment_due_date.sql \
lumie-backend/app/src/main/resources/db/migration/public/V90__assignment_overdue_close_index.sql
rg -n "tenant_id|TenantContextHolder" lumie-backend/modules

성공 기준은 migration이 tenant_id, RLS 활성화, 테넌트-isolation policy, assignment redesign 필드, 테넌트-safe assignment foreign key가 있는 테넌트 소유 테이블을 보여주고, 백엔드 모듈은 테넌트 범위 읽기/쓰기에 앞서 요청 시점 테넌트 컨텍스트 전파를 보여주는 것입니다.