feat(db): Flyway 마이그레이션 도입 (ddl-auto update → validate)
스키마를 '앱이 자동 변경'에서 '버전 관리되는 마이그레이션'으로 전환: - flyway-core + flyway-database-postgresql 추가 - V1__baseline.sql: 현재 운영 스키마(테이블 13·제약 31·인덱스 1)를 그대로 박제 - ddl-auto: update → validate (Hibernate는 이제 검증만, 스키마는 Flyway 소유) - baseline-on-migrate: 기존 운영 DB는 V1 미실행·baseline 표시, 신규 DB는 V1 실행 안전 절차로 진행: - 도입 전 운영 DB 백업(pg_dump -Fc) - 신규 DB 경로 검증(빈 DB→V1 실행→validate 통과→기동) 후 운영 배포 - 운영: 기존 스키마 baseline(무실행)→validate 통과→기동, 데이터 무손실(users=4·profile=3) 확인 - 테스트 잔재 pgcrypto 확장 제거 향후 스키마 변경은 V2, V3...로 의도를 남긴다. 엔티티만 바꾸고 마이그레이션을 안 쓰면 validate가 기동 시 실패해 알려 준다. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
0e3ab14b14
commit
db54b9bc19
@ -38,6 +38,17 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
<!-- DB 마이그레이션 (Flyway) — 스키마 변경을 버전 관리한다.
|
||||
학습 포인트: flyway-core는 엔진, flyway-database-postgresql은 Postgres 방언 지원.
|
||||
(Flyway 10부터 DB별 지원이 별도 모듈로 분리됐다.) 버전은 Spring Boot BOM이 관리. -->
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-database-postgresql</artifactId>
|
||||
</dependency>
|
||||
<!-- 로그인/권한 (세션 기반) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
|
||||
@ -10,20 +10,27 @@ spring:
|
||||
password: ${DB_PASSWORD:mirim123}
|
||||
jpa:
|
||||
hibernate:
|
||||
# 학습 포인트: ddl-auto: update의 의미 —
|
||||
# 앱이 뜰 때 Hibernate가 엔티티(@Entity 클래스)와 실제 테이블을 비교해서
|
||||
# 없는 테이블/컬럼을 자동으로 만들어 준다(CREATE/ALTER를 대신 실행).
|
||||
# 개발 단계에선 편하지만 운영에선 위험하다:
|
||||
# - 컬럼 삭제/이름 변경은 반영하지 못해 엔티티와 테이블이 조용히 어긋난다.
|
||||
# - 어떤 DDL이 언제 실행됐는지 기록이 남지 않아 되돌리기가 불가능하다.
|
||||
# 운영에선 validate(검증만)로 바꾸고 Flyway 같은 마이그레이션 도구로 스키마를 관리한다.
|
||||
ddl-auto: update # 개발 단계: 엔티티에 맞춰 테이블 자동 생성/수정
|
||||
# 학습 포인트: 이제 스키마는 Flyway가 소유한다. Hibernate는 validate로 "엔티티와 실제
|
||||
# 테이블이 일치하는지 검증"만 하고, 테이블을 만들거나 바꾸지 않는다.
|
||||
# - update였을 때의 문제(컬럼 삭제 미반영·변경 이력 없음·조용한 드리프트)를 끝낸다.
|
||||
# - 엔티티를 바꿨는데 마이그레이션(Vn.sql)을 안 쓰면 validate가 기동 시 실패해 알려 준다
|
||||
# → "엔티티와 DB를 함께 바꿔라"를 기계가 강제한다.
|
||||
ddl-auto: validate
|
||||
# open-in-view: 화면 렌더링이 끝날 때까지 DB 커넥션을 물고 있는 기능. REST API 서버에선
|
||||
# 커넥션 낭비일 뿐이라 끈다. (끄면 LAZY 로딩은 반드시 @Transactional 안에서 끝내야 한다)
|
||||
open-in-view: false
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
# Flyway — DB 스키마 마이그레이션(버전 관리). classpath의 db/migration/V*.sql을 순서대로 적용.
|
||||
flyway:
|
||||
# 학습 포인트: baseline-on-migrate —
|
||||
# 이미 테이블이 있는(비어있지 않은) 기존 운영 DB엔 V1을 "실행"하지 않고 "이미 반영됨"으로
|
||||
# 표시(baseline)만 한다. 안 그러면 V1의 CREATE TABLE이 "이미 존재"로 터진다.
|
||||
# 빈 DB(신규/개발)에선 baseline이 일어나지 않고 V1부터 정상 실행돼 스키마를 만든다.
|
||||
# → 같은 마이그레이션 파일로 "기존 운영 DB 편입"과 "신규 DB 생성"을 모두 안전하게 처리.
|
||||
baseline-on-migrate: true
|
||||
baseline-version: 1
|
||||
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
370
backend/src/main/resources/db/migration/V1__baseline.sql
Normal file
370
backend/src/main/resources/db/migration/V1__baseline.sql
Normal file
@ -0,0 +1,370 @@
|
||||
-- V1__baseline.sql — 운영 DB의 "현재 스키마"를 그대로 박제한 베이스라인.
|
||||
--
|
||||
-- 이 파일이 하는 일:
|
||||
-- Flyway 도입 시점의 스키마 전체(테이블 13개·제약·인덱스)를 기록한다.
|
||||
-- 이 시점 이후의 모든 스키마 변경은 V2, V3... 로 "의도를 남기며" 추가한다.
|
||||
--
|
||||
-- 학습 포인트 - 왜 ddl-auto:update에서 Flyway로 옮기나?
|
||||
-- ddl-auto:update는 앱이 뜰 때 엔티티에 맞춰 테이블을 자동 변경했다. 편했지만:
|
||||
-- - 컬럼 삭제/이름변경을 반영 못 해 엔티티와 DB가 조용히 어긋나고,
|
||||
-- - 무엇이 언제 바뀌었는지 기록이 없어 되돌릴 수 없었다.
|
||||
-- - 그 흔적이 이 스키마에도 남아 있다(예: submissions에 같은 컬럼 unique 제약이
|
||||
-- 두 개 - 하나는 명시명, 하나는 Hibernate 자동해시명 uk...). 이런 드리프트를
|
||||
-- 끝내려고 Flyway로 옮긴다.
|
||||
-- 이제부터 스키마는 "버전 관리되는 마이그레이션"으로만 바뀐다. Hibernate는 ddl-auto:validate로
|
||||
-- "엔티티와 DB가 맞는지 검증"만 한다(스키마를 건드리지 않는다).
|
||||
--
|
||||
-- 주의: 이 베이스라인은 자동 캡처된 현재 상태라 제약/시퀀스 이름이 Hibernate 자동생성(해시)이다.
|
||||
-- 깔끔한 재명명은 이후 별도 마이그레이션(Vn)에서 다룬다 - 베이스라인은 "있는 그대로"가 원칙.
|
||||
|
||||
CREATE TABLE public.assignments (
|
||||
id bigint NOT NULL,
|
||||
day integer NOT NULL,
|
||||
kind character varying(255) NOT NULL,
|
||||
summary text,
|
||||
title character varying(255) NOT NULL,
|
||||
week integer NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE public.assignments ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
|
||||
SEQUENCE NAME public.assignments_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1
|
||||
);
|
||||
|
||||
CREATE TABLE public.audit_log (
|
||||
id bigint NOT NULL,
|
||||
action character varying(30) NOT NULL,
|
||||
actor character varying(40) NOT NULL,
|
||||
actor_role character varying(20) NOT NULL,
|
||||
created_at timestamp(6) with time zone NOT NULL,
|
||||
detail character varying(200),
|
||||
ip character varying(45) NOT NULL,
|
||||
target_user_id bigint,
|
||||
CONSTRAINT audit_log_action_check CHECK (((action)::text = ANY ((ARRAY['EXPORT_STUDENT_CSV'::character varying, 'VIEW_STUDENT_ROSTER'::character varying, 'RESET_STUDENT_PASSWORD'::character varying, 'PURGE_STUDENT'::character varying])::text[])))
|
||||
);
|
||||
|
||||
ALTER TABLE public.audit_log ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
|
||||
SEQUENCE NAME public.audit_log_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1
|
||||
);
|
||||
|
||||
CREATE TABLE public.checklist_items (
|
||||
id bigint NOT NULL,
|
||||
day integer,
|
||||
label character varying(255) NOT NULL,
|
||||
sort_order integer NOT NULL,
|
||||
week integer NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE public.checklist_items ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
|
||||
SEQUENCE NAME public.checklist_items_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1
|
||||
);
|
||||
|
||||
CREATE TABLE public.coding_problem (
|
||||
id bigint NOT NULL,
|
||||
description text NOT NULL,
|
||||
difficulty character varying(8) NOT NULL,
|
||||
function_name character varying(60),
|
||||
language character varying(8) NOT NULL,
|
||||
sort_order integer NOT NULL,
|
||||
starter_code text NOT NULL,
|
||||
tests_json text,
|
||||
title character varying(255) NOT NULL,
|
||||
topic character varying(60) NOT NULL,
|
||||
setup_sql text
|
||||
);
|
||||
|
||||
ALTER TABLE public.coding_problem ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
|
||||
SEQUENCE NAME public.coding_problem_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1
|
||||
);
|
||||
|
||||
CREATE TABLE public.coding_submission (
|
||||
id bigint NOT NULL,
|
||||
attempts integer NOT NULL,
|
||||
code text NOT NULL,
|
||||
feedback text,
|
||||
passed_count integer,
|
||||
results_json text,
|
||||
status character varying(12) NOT NULL,
|
||||
submitted_at timestamp(6) with time zone NOT NULL,
|
||||
total_count integer,
|
||||
problem_id bigint NOT NULL,
|
||||
user_id bigint NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE public.coding_submission ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
|
||||
SEQUENCE NAME public.coding_submission_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1
|
||||
);
|
||||
|
||||
CREATE TABLE public.course_progress (
|
||||
id bigint NOT NULL,
|
||||
completed_at timestamp(6) with time zone NOT NULL,
|
||||
course_slug character varying(60) NOT NULL,
|
||||
user_id bigint NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE public.course_progress ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
|
||||
SEQUENCE NAME public.course_progress_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1
|
||||
);
|
||||
|
||||
CREATE TABLE public.documents (
|
||||
id bigint NOT NULL,
|
||||
audience character varying(255) NOT NULL,
|
||||
category character varying(255) NOT NULL,
|
||||
file_path character varying(255) NOT NULL,
|
||||
slug character varying(255) NOT NULL,
|
||||
sort_order integer NOT NULL,
|
||||
title character varying(255) NOT NULL,
|
||||
week integer
|
||||
);
|
||||
|
||||
ALTER TABLE public.documents ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
|
||||
SEQUENCE NAME public.documents_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1
|
||||
);
|
||||
|
||||
CREATE TABLE public.progress (
|
||||
id bigint NOT NULL,
|
||||
checked_at timestamp(6) with time zone NOT NULL,
|
||||
item_id bigint NOT NULL,
|
||||
user_id bigint NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE public.progress ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
|
||||
SEQUENCE NAME public.progress_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1
|
||||
);
|
||||
|
||||
CREATE TABLE public.quiz_question (
|
||||
id bigint NOT NULL,
|
||||
answer_index integer NOT NULL,
|
||||
course_slug character varying(60) NOT NULL,
|
||||
explanation text NOT NULL,
|
||||
option1 text NOT NULL,
|
||||
option2 text NOT NULL,
|
||||
option3 text NOT NULL,
|
||||
option4 text NOT NULL,
|
||||
question text NOT NULL,
|
||||
sort_order integer NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE public.quiz_question ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
|
||||
SEQUENCE NAME public.quiz_question_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1
|
||||
);
|
||||
|
||||
CREATE TABLE public.quiz_result (
|
||||
id bigint NOT NULL,
|
||||
course_slug character varying(60) NOT NULL,
|
||||
score integer NOT NULL,
|
||||
submitted_at timestamp(6) with time zone NOT NULL,
|
||||
total integer NOT NULL,
|
||||
user_id bigint NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE public.quiz_result ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
|
||||
SEQUENCE NAME public.quiz_result_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1
|
||||
);
|
||||
|
||||
CREATE TABLE public.student_profile (
|
||||
id bigint NOT NULL,
|
||||
address text NOT NULL,
|
||||
birth_date date NOT NULL,
|
||||
company_email_local character varying(40) NOT NULL,
|
||||
emergency_name character varying(30) NOT NULL,
|
||||
emergency_phone character varying(20) NOT NULL,
|
||||
emergency_relation character varying(20) NOT NULL,
|
||||
major character varying(60) NOT NULL,
|
||||
phone character varying(20) NOT NULL,
|
||||
school character varying(60) NOT NULL,
|
||||
user_id bigint NOT NULL,
|
||||
privacy_consent_at timestamp(6) with time zone NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE public.student_profile ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
|
||||
SEQUENCE NAME public.student_profile_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1
|
||||
);
|
||||
|
||||
CREATE TABLE public.submissions (
|
||||
id bigint NOT NULL,
|
||||
content text NOT NULL,
|
||||
feedback text,
|
||||
link character varying(255),
|
||||
status character varying(255) NOT NULL,
|
||||
submitted_at timestamp(6) with time zone NOT NULL,
|
||||
assignment_id bigint NOT NULL,
|
||||
user_id bigint NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE public.submissions ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
|
||||
SEQUENCE NAME public.submissions_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1
|
||||
);
|
||||
|
||||
CREATE TABLE public.users (
|
||||
id bigint NOT NULL,
|
||||
created_at timestamp(6) with time zone NOT NULL,
|
||||
name character varying(255) NOT NULL,
|
||||
password_hash character varying(255) NOT NULL,
|
||||
role character varying(255) NOT NULL,
|
||||
track character varying(255) NOT NULL,
|
||||
username character varying(255) NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE public.users ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
|
||||
SEQUENCE NAME public.users_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1
|
||||
);
|
||||
|
||||
ALTER TABLE ONLY public.assignments
|
||||
ADD CONSTRAINT assignments_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY public.audit_log
|
||||
ADD CONSTRAINT audit_log_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY public.checklist_items
|
||||
ADD CONSTRAINT checklist_items_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY public.coding_problem
|
||||
ADD CONSTRAINT coding_problem_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY public.coding_submission
|
||||
ADD CONSTRAINT coding_submission_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY public.course_progress
|
||||
ADD CONSTRAINT course_progress_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY public.documents
|
||||
ADD CONSTRAINT documents_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY public.progress
|
||||
ADD CONSTRAINT progress_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY public.quiz_question
|
||||
ADD CONSTRAINT quiz_question_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY public.quiz_result
|
||||
ADD CONSTRAINT quiz_result_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY public.student_profile
|
||||
ADD CONSTRAINT student_profile_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY public.submissions
|
||||
ADD CONSTRAINT submissions_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY public.progress
|
||||
ADD CONSTRAINT uk3328ayw5asr3mnmp0c31n21jf UNIQUE (user_id, item_id);
|
||||
|
||||
ALTER TABLE ONLY public.student_profile
|
||||
ADD CONSTRAINT uk3whrp0g2trsa8qsb2nk142jea UNIQUE (company_email_local);
|
||||
|
||||
ALTER TABLE ONLY public.student_profile
|
||||
ADD CONSTRAINT uk99mm2qc8gq78mojsjmdhqqrtd UNIQUE (user_id);
|
||||
|
||||
ALTER TABLE ONLY public.submissions
|
||||
ADD CONSTRAINT uk_submissions_assignment_user UNIQUE (assignment_id, user_id);
|
||||
|
||||
ALTER TABLE ONLY public.coding_submission
|
||||
ADD CONSTRAINT ukepcri27qov9okseajuck21p1q UNIQUE (problem_id, user_id);
|
||||
|
||||
ALTER TABLE ONLY public.course_progress
|
||||
ADD CONSTRAINT ukk2refcthyrufe59nnxsjkxklv UNIQUE (user_id, course_slug);
|
||||
|
||||
ALTER TABLE ONLY public.submissions
|
||||
ADD CONSTRAINT ukmg9jk4whp661fe4kyo8pdapkf UNIQUE (assignment_id, user_id);
|
||||
|
||||
ALTER TABLE ONLY public.documents
|
||||
ADD CONSTRAINT ukpbof73ceduubbvfrqc0wj4eb1 UNIQUE (slug);
|
||||
|
||||
ALTER TABLE ONLY public.users
|
||||
ADD CONSTRAINT ukr43af9ap4edm43mmtq01oddj6 UNIQUE (username);
|
||||
|
||||
ALTER TABLE ONLY public.users
|
||||
ADD CONSTRAINT users_pkey PRIMARY KEY (id);
|
||||
|
||||
CREATE INDEX ix_audit_log_created_at ON public.audit_log USING btree (created_at);
|
||||
|
||||
ALTER TABLE ONLY public.progress
|
||||
ADD CONSTRAINT fk4gq8i0p0nmn2dwgcq11nmpjm4 FOREIGN KEY (item_id) REFERENCES public.checklist_items(id);
|
||||
|
||||
ALTER TABLE ONLY public.submissions
|
||||
ADD CONSTRAINT fk760bgu69957phd7hax608jdms FOREIGN KEY (user_id) REFERENCES public.users(id);
|
||||
|
||||
ALTER TABLE ONLY public.coding_submission
|
||||
ADD CONSTRAINT fk78tg75ufvjeo1lc6g7jc8y9g0 FOREIGN KEY (problem_id) REFERENCES public.coding_problem(id);
|
||||
|
||||
ALTER TABLE ONLY public.progress
|
||||
ADD CONSTRAINT fk7fyumbty8qgbd7sfbbjnqdo62 FOREIGN KEY (user_id) REFERENCES public.users(id);
|
||||
|
||||
ALTER TABLE ONLY public.student_profile
|
||||
ADD CONSTRAINT fkh6555c9k0gv0yddac6llslk3t FOREIGN KEY (user_id) REFERENCES public.users(id);
|
||||
|
||||
ALTER TABLE ONLY public.coding_submission
|
||||
ADD CONSTRAINT fkiym0dwju9tunrvp1mjdep8bl4 FOREIGN KEY (user_id) REFERENCES public.users(id);
|
||||
|
||||
ALTER TABLE ONLY public.course_progress
|
||||
ADD CONSTRAINT fkmootcccgd4bqe8cjtma7stkp1 FOREIGN KEY (user_id) REFERENCES public.users(id);
|
||||
|
||||
ALTER TABLE ONLY public.quiz_result
|
||||
ADD CONSTRAINT fknuben697e3lorbhgsbif22f2t FOREIGN KEY (user_id) REFERENCES public.users(id);
|
||||
|
||||
ALTER TABLE ONLY public.submissions
|
||||
ADD CONSTRAINT fkrirbb44savy2g7nws0hoxs949 FOREIGN KEY (assignment_id) REFERENCES public.assignments(id);
|
||||
Loading…
x
Reference in New Issue
Block a user