마이그레이션 가이드
Flyway 구성
Lumie는 Flyway를 사용하여 데이터베이스 스키마를 관리합니다. V18(RLS baseline) 이후 마이그레이션은 단일 경로로 통합되었습니다.
src/main/resources/db/migration/
└── public/ # 단일 경로 — public 스키마 전체
├── V1__create_platform_tables.sql
├── V2__create_users_table.sql
├── ...
├── V52__attendance_session_unique_class_date.sql
└── V53__attendance_record_default_pending.sql
tenant/경로는 V18 RLS 마이그레이션에서 제거되었습니다. Schema-per-tenant 전략이 폐기된 이후 모든 테이블이public스키마로 통합되었습니다.
마이그레이션 실행 방식
애플리케이션 시작 시 Flyway가 public 스키마에 대해 순차적으로 실행합니다.
spring.flyway.locations=classpath:db/migration/public
spring.flyway.schemas=public
spring.flyway.user=<postgres 역할> # 마이그레이션 전용 역할 (SUPERUSER)
spring.datasource.username=lumie_app # 런타임 역할 (NOBYPASSRLS)
역할 분리:
postgres(SUPERUSER): Flyway 마이그레이션 실행 전용. RLS를 우회하여 전체 행에 접근 가능lumie_app(NOSUPERUSER NOBYPASSRLS): 애플리케이션 런타임 연결. RLS 정책이 항상 적용됨
마이그레이션 작성 규칙
파일 네이밍
V{버전}__{설명}.sql
V접두사 필수- 버전은 정수 (1, 2, 3, ...)
- 밑줄 2개(
__)로 버전과 설명 구분 - 설명은 snake_case
DO
- 하나의 마이그레이션 파일은 하나의 논리적 변경만 포함
IF NOT EXISTS를 사용하여 멱등성 확보- 큰 테이블 변경은 별도 마이그레이션으로 분리
- 인덱스 생성은
CONCURRENTLY사용 (다운타임 방지) - 새 테넌트 스코프 테이블은 반드시 RLS 패턴 적용
DON'T
- DDL과 DML을 같은 파일에 섞지 않기
- 이미 실행된 마이그레이션 파일 수정하지 않기 — 체크섬 불일치로 부트 실패
DROP TABLE/DROP COLUMN을 함부로 사용하지 않기 (데이터 손실)ALTER TYPE으로 enum 변경하지 않기 (별도 전략 필요)
신규 테넌트 스코프 테이블 패턴
-- V53__create_example_table.sql
CREATE TABLE IF NOT EXISTS example_table (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
name VARCHAR(100) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
version BIGINT NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_example_table_tenant_id
ON example_table (tenant_id);
-- RLS 적용 (필수)
ALTER TABLE example_table ENABLE ROW LEVEL SECURITY;
ALTER TABLE example_table FORCE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON example_table
USING (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::bigint)
WITH CHECK (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::bigint);
-- lumie_app 권한 부여 (필수)
GRANT SELECT, INSERT, UPDATE, DELETE ON example_table TO lumie_app;
GRANT USAGE, SELECT ON SEQUENCE example_table_id_seq TO lumie_app;
컬럼 추가 예시
-- V54__add_example_column.sql
ALTER TABLE students
ADD COLUMN IF NOT EXISTS grade_level VARCHAR(10);
플랫폼 vs 테넌트 테이블 배치 기준
| 기준 | 플랫폼 테이블 (RLS 없음) | 테넌트 테이블 (RLS 있음) |
|---|---|---|
| 전역 디렉토리 (tenants, owner_directory) | O | |
| 빌링 계약 (plans, subscriptions, invoices) | O | |
| 학원별 업무 데이터 | O | |
| 테넌트 간 공유 없는 데이터 | O |
플랫폼 테이블은 lumie_app이 tenant_id 필터 없이 전체 행을 읽을 수 있습니다. 접근 제어는 앱 계층(Spring Security)이 담당합니다.
트러블슈팅
마이그레이션 실패 시
# 실패 이력 조회
SELECT * FROM flyway_schema_history WHERE success = false;
# Flyway repair (실패 기록 정리)
./gradlew flywayRepair
# 또는 직접 삭제
DELETE FROM public.flyway_schema_history WHERE success = false;
체크섬 불일치
이미 적용된 마이그레이션 파일을 수정하면 부트 실패합니다. 절대 수정하지 마세요. 변경이 필요하면 새 버전 파 일을 만드세요.
RLS로 인해 행이 안 보일 때
-- 현재 세션의 tenant_id 설정 확인
SELECT current_setting('app.tenant_id', true);
-- postgres 역할로 접속 시 전체 행 조회 가능 (SUPERUSER)
-- lumie_app 역할로 접속 시 SET LOCAL app.tenant_id = '<id>' 필요