Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,10 +184,25 @@ docker rm -f klljs-mysql-test

## DB 마이그레이션

운영 DB 스키마는 `src/main/resources/db/migration`의 Flyway 마이그레이션(V1~V12)으로 관리하고,
운영 DB 스키마는 `src/main/resources/db/migration`의 Flyway 마이그레이션(V1~V13)으로 관리하고,
애플리케이션은 `ddl-auto=validate`로 스키마와의 불일치만 검증할 뿐 직접 바꾸지 않는다. 로컬은
반대로 Flyway 없이 Hibernate `ddl-auto=create-drop`으로 매번 새로 만든다.

**검증은 반드시 실제 MySQL로 한다** — 로컬 프로필은 Flyway 자체를 안 타고, H2는 잠금/제약
동작이 MySQL과 달라 Flyway·FK·동시성 관련 버그를 못 잡는다. 새 마이그레이션을 추가하면
`mysql:8.0.46` Docker 컨테이너에 `V1`부터 전체 이력을 처음부터 적용해보고 통과하는지 확인할 것
(신규 스테이징 환경, MySQL 기반 로컬 셋업, 마이그레이션만으로 하는 DR 복구가 모두 이 경로를
탄다 - `V12__recreate_vision_summary_5s.sql`가 한동안 이 경로에서 "Table already exists"로
막혀 있었던 사례가 있다).

**이미 운영에 적용된 마이그레이션 파일은 원칙적으로 수정하지 않는다.** 불가피하게 수정하면
(예: `V12`처럼 사고 복구용으로 작성된 마이그레이션을 신규 환경에서도 안전하게 만들 때)
파일 내용이 바뀌어 checksum이 바뀌므로, 그 버전이 이미 적용된 환경(운영)에서는 다음 배포 시
Flyway validate가 checksum mismatch로 실패해 앱이 기동하지 않는다. 이런 수정을 배포하기 전에는
운영 DB에 대해 먼저 `flyway repair`(또는 동등하게 `flyway_schema_history`의 해당 버전 checksum을
새 파일 기준으로 수동 갱신)를 실행해야 한다 - repair는 메타데이터만 갱신하고 SQL을 실행하지
않으므로 운영 스키마 자체에는 영향이 없다.

---

## CI/CD
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,37 @@
--
-- 컬럼/제약/인덱스는 V1__init_schema.sql의 원본 정의에 V4__vision_summary_dedup_key_drop_seq.sql의
-- 변경사항(3컬럼 UNIQUE -> 2컬럼 UNIQUE, 중복 인덱스 제거)까지 반영한 "DROP 직전 최종 상태" 그대로다.
--
-- [부트스트랩 가드] 이 마이그레이션은 애초에 "테이블이 이미 DROP돼서 없는" 운영 DB만
-- 겨냥해 무조건 CREATE TABLE을 실행하도록 작성됐다. 그런데 V1부터 전체 이력을 처음부터
-- 적용하는 환경(신규 스테이징, MySQL 기반 로컬 셋업, 마이그레이션만으로 하는 DR 복구 등 -
-- 사고가 실제로 일어난 적 없는 환경)에서는 V1이 이미 이 테이블을 만들어 놓은 상태라
-- V12가 "Table 'vision_summary_5s' already exists"로 실패해 전체 마이그레이션이 막힌다.
-- 위 CREATE TABLE 정의는 V1 원본 + V4 변경사항을 반영한 스키마와 컬럼/제약/인덱스가
-- 완전히 동일함을 실제 MySQL 8.0.46으로 확인했으므로, "테이블이 없을 때만" 실행하도록
-- information_schema로 존재 여부를 확인한 뒤 PREPARE/EXECUTE로 조건부 실행한다
-- (MySQL은 CREATE INDEX에 IF NOT EXISTS를 지원하지 않아 CREATE TABLE IF NOT EXISTS만으로는
-- 부족하다 - 인덱스 생성문도 함께 가드해야 한다). 두 시나리오 모두 같은 최종 상태로 수렴한다:
-- 1) 사고 복구(운영): 테이블이 없음 -> 그대로 생성한다 (기존 동작 그대로).
-- 2) 신규 환경 부트스트랩: 테이블이 이미 있음(V1이 만듦) -> 아무것도 하지 않고 건너뛴다.
--
-- 주의(배포 운영자용): 운영 DB에는 이 마이그레이션이 이미 V12로 적용돼 있고
-- flyway_schema_history에 그때의 checksum이 기록돼 있다. 이 파일 내용을 바꿨으므로
-- checksum도 바뀐다 - 그대로 배포하면 다음 기동 시 Flyway validate가 checksum mismatch로
-- 실패해 앱이 뜨지 않는다. 이 PR을 운영에 배포하기 전에 운영 DB에 대해 반드시 먼저
-- `flyway repair`(또는 동등하게 flyway_schema_history의 version=12 행 checksum을 새
-- 파일 기준으로 수동 갱신)를 실행할 것. repair는 SQL을 실행하지 않고 메타데이터만
-- 갱신하므로 운영 스키마 자체에는 영향이 없다.
-- ============================================================

SET @vision_summary_5s_exists = (
SELECT COUNT(*)
FROM information_schema.tables
WHERE table_schema = DATABASE()
AND table_name = 'vision_summary_5s'
);

SET @ddl_create_table = IF(@vision_summary_5s_exists = 0, '
CREATE TABLE vision_summary_5s (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
media_unit_id BIGINT UNSIGNED NOT NULL,
Expand Down Expand Up @@ -71,7 +100,20 @@ CREATE TABLE vision_summary_5s (
CONSTRAINT uk_vision_summary_media_time UNIQUE (media_unit_id, event_time),
CONSTRAINT fk_vision_summary_media_unit FOREIGN KEY (media_unit_id) REFERENCES media_units (id) ON DELETE RESTRICT,
CONSTRAINT fk_vision_summary_campaign FOREIGN KEY (campaign_id) REFERENCES campaigns (id) ON DELETE SET NULL
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci', 'DO 0');

PREPARE stmt FROM @ddl_create_table;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

SET @ddl_create_index_campaign_time = IF(@vision_summary_5s_exists = 0,
'CREATE INDEX ix_vision_summary_campaign_time ON vision_summary_5s (campaign_id, event_time)', 'DO 0');
PREPARE stmt FROM @ddl_create_index_campaign_time;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

CREATE INDEX ix_vision_summary_campaign_time ON vision_summary_5s (campaign_id, event_time);
CREATE INDEX ix_vision_summary_event_time ON vision_summary_5s (event_time);
SET @ddl_create_index_event_time = IF(@vision_summary_5s_exists = 0,
'CREATE INDEX ix_vision_summary_event_time ON vision_summary_5s (event_time)', 'DO 0');
PREPARE stmt FROM @ddl_create_index_event_time;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
Loading