Session 8JPA Workshop

Flyway DB 마이그레이션

Flyway를 이용한 데이터베이스 스키마 버전 관리와 마이그레이션 전략을 학습합니다.

학습 목표

  • Flyway의 필요성과 동작 원리를 이해한다
  • 마이그레이션 스크립트 명명 규칙을 이해한다
  • 스키마 변경과 데이터 마이그레이션 스크립트를 작성할 수 있다
  • 환경별 Flyway 설정을 구성할 수 있다
  • 무중단 배포 시 마이그레이션 전략을 수립할 수 있다

1. Flyway란?

Flyway는 데이터베이스 스키마 버전 관리 도구입니다. SQL 스크립트를 버전별로 관리하여 일관된 DB 마이그레이션을 보장합니다.

1.1 왜 Flyway가 필요한가?

문제: ddl-auto=update의 한계

  • • 컬럼 삭제, 이름 변경 불가
  • • 데이터 마이그레이션 불가
  • • 운영 환경에서 위험 (예측 불가)
  • • 팀원 간 스키마 불일치

해결: Flyway로 스키마 버전 관리

  • • SQL 스크립트로 명시적 변경
  • • 버전 이력 추적 (flyway_schema_history)
  • • 팀 간 스키마 동기화
  • • CI/CD 파이프라인 통합

1.2 Flyway 설정

// build.gradle
dependencies {
    implementation 'org.flywaydb:flyway-core'
    implementation 'org.flywaydb:flyway-mysql'  // MySQL 사용 시
}

// application.yml
spring:
  flyway:
    enabled: true
    locations: classpath:db/migration
    baseline-on-migrate: true  # 기존 DB에 적용 시
    baseline-version: 0
    out-of-order: false        # 순서 강제
    validate-on-migrate: true  # 마이그레이션 전 검증
  jpa:
    hibernate:
      ddl-auto: validate  # Flyway 사용 시 validate 권장

1.3 마이그레이션 파일 명명 규칙

V{버전}__{설명}.sql

V1__Create_products_table.sql

V2__Create_orders_table.sql

V3__Add_category_to_products.sql

V1.1__Add_index_to_products.sql

R__{설명}.sql (반복 실행)

R__Create_views.sql

R__Refresh_stored_procedures.sql

주의사항

  • • 버전 번호는 순차적이어야 함
  • • 한 번 적용된 파일은 절대 수정 금지
  • • 언더스코어 2개(__) 필수
  • • 파일명에 공백 사용 금지

1.4 디렉토리 구조

src/main/resources/
└── db/
    └── migration/
        ├── V1__Create_initial_schema.sql
        ├── V2__Add_categories.sql
        ├── V3__Add_version_column.sql
        ├── V4__Migrate_status_values.sql
        └── R__Create_views.sql

Flyway 핵심 원칙

  • • 운영 환경: ddl-auto=none 또는 validate
  • • 모든 스키마 변경은 마이그레이션 스크립트로
  • • 적용된 스크립트는 불변 (immutable)
  • • 코드 리뷰에 마이그레이션 스크립트 포함

2. 마이그레이션 스크립트 작성

2.1 초기 스키마 생성

V1__Create_initial_schema.sql
-- V1__Create_initial_schema.sql
-- 초기 스키마 생성

CREATE TABLE members (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    email VARCHAR(100) NOT NULL UNIQUE,
    name VARCHAR(50) NOT NULL,
    password VARCHAR(255) NOT NULL,
    status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE products (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(200) NOT NULL,
    sku VARCHAR(50) NOT NULL UNIQUE,
    price DECIMAL(10,2) NOT NULL,
    stock_quantity INT NOT NULL DEFAULT 0,
    status VARCHAR(20) NOT NULL DEFAULT 'DRAFT',
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE orders (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    member_id BIGINT NOT NULL,
    status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
    total_amount DECIMAL(12,2) NOT NULL,
    order_date DATETIME NOT NULL,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    CONSTRAINT fk_orders_member FOREIGN KEY (member_id) 
        REFERENCES members(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 인덱스 생성
CREATE INDEX idx_orders_member ON orders(member_id);
CREATE INDEX idx_orders_status ON orders(status);
CREATE INDEX idx_products_status ON products(status);

2.2 테이블 추가

V2__Add_categories.sql
-- V2__Add_categories.sql
-- 카테고리 테이블 추가

CREATE TABLE categories (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    parent_id BIGINT,
    sort_order INT DEFAULT 0,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    CONSTRAINT fk_category_parent FOREIGN KEY (parent_id) 
        REFERENCES categories(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- products에 category_id 컬럼 추가
ALTER TABLE products ADD COLUMN category_id BIGINT AFTER status;
ALTER TABLE products ADD CONSTRAINT fk_product_category 
    FOREIGN KEY (category_id) REFERENCES categories(id);

CREATE INDEX idx_products_category ON products(category_id);

2.3 컬럼 추가/수정

V3__Add_version_column.sql
-- V3__Add_version_column.sql
-- 낙관적 락을 위한 version 컬럼 추가

ALTER TABLE products ADD COLUMN version BIGINT NOT NULL DEFAULT 0;
ALTER TABLE orders ADD COLUMN version BIGINT NOT NULL DEFAULT 0;
ALTER TABLE members ADD COLUMN version BIGINT NOT NULL DEFAULT 0;

2.4 데이터 마이그레이션

V4__Migrate_status_values.sql
-- V4__Migrate_status_values.sql
-- 상태값 변경 (ACTIVE -> PUBLISHED)

-- 트랜잭션으로 안전하게 처리
START TRANSACTION;

UPDATE products SET status = 'PUBLISHED' WHERE status = 'ACTIVE';
UPDATE products SET status = 'INACTIVE' WHERE status = 'DISABLED';

-- 기본 카테고리 데이터 삽입
INSERT INTO categories (name, sort_order) VALUES 
    ('전자제품', 1),
    ('의류', 2),
    ('식품', 3),
    ('도서', 4);

COMMIT;

대용량 데이터 마이그레이션 주의

-- 대용량 테이블 업데이트 시 배치 처리
-- V5__Batch_update_products.sql

-- 한 번에 1000건씩 처리
SET @batch_size = 1000;
SET @rows_affected = 1;

WHILE @rows_affected > 0 DO
    UPDATE products 
    SET status = 'PUBLISHED' 
    WHERE status = 'ACTIVE' 
    LIMIT @batch_size;
    
    SET @rows_affected = ROW_COUNT();
    
    -- 잠시 대기 (락 경합 방지)
    DO SLEEP(0.1);
END WHILE;

마이그레이션 스크립트 Best Practices

  • • 하나의 스크립트는 하나의 논리적 변경만
  • • 롤백 가능한 형태로 작성
  • • 대용량 데이터는 배치 처리
  • • 인덱스 생성은 별도 스크립트로 분리

3. 배포 전략과 정리

3.1 환경별 설정

# application-local.yml (개발)
spring:
  flyway:
    enabled: true
    clean-disabled: false  # 개발에서만 clean 허용
    validate-on-migrate: true
  jpa:
    hibernate:
      ddl-auto: validate

# application-prod.yml (운영)
spring:
  flyway:
    enabled: true
    clean-disabled: true   # 운영에서 clean 절대 금지!
    baseline-on-migrate: false
    validate-on-migrate: true
    out-of-order: false
  jpa:
    hibernate:
      ddl-auto: none       # 운영에서는 none

3.2 Flyway 명령어

flyway migrate

마이그레이션 실행

flyway info

마이그레이션 상태 확인

flyway validate

스크립트 유효성 검증

flyway repair

실패한 마이그레이션 복구

flyway baseline

기존 DB에 기준점 설정

flyway clean

모든 테이블 삭제 (위험!)

3.3 CI/CD 파이프라인 통합

# GitHub Actions 예시
name: Deploy

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Validate Flyway migrations
        run: ./gradlew flywayValidate
        
      - name: Run Flyway migrations
        run: ./gradlew flywayMigrate
        env:
          FLYWAY_URL: ${{ secrets.DB_URL }}
          FLYWAY_USER: ${{ secrets.DB_USER }}
          FLYWAY_PASSWORD: ${{ secrets.DB_PASSWORD }}
          
      - name: Deploy application
        run: ./deploy.sh

마이그레이션 실패 시 대응

-- 1. flyway_schema_history 테이블 확인
SELECT * FROM flyway_schema_history 
ORDER BY installed_rank DESC;

-- 2. 실패한 마이그레이션 확인 (success = 0)
SELECT * FROM flyway_schema_history 
WHERE success = 0;

-- 3. repair 명령으로 복구
./gradlew flywayRepair

-- 4. 수동으로 실패 기록 삭제 (최후의 수단)
DELETE FROM flyway_schema_history 
WHERE success = 0;

3.4 JPA Workshop 전체 정리

Session 1: ORM, 영속성 컨텍스트, 엔티티 생명주기

Session 2: @Entity, @Id, @Column, 타입 매핑

Session 3: 연관관계 매핑, mappedBy, 연관관계의 주인

Session 4: N+1 문제, Fetch Join, @BatchSize

Session 5: Spring Data JPA, Query Methods, Pageable

Session 6: CQRS, Projection, readOnly 최적화

Session 7: @Transactional, 낙관적/비관적 락

Session 8: Flyway DB 마이그레이션

핵심 키워드

FlywayDB MigrationVersion Controlbaselineddl-auto=validateflyway_schema_historyCI/CD

🎉 JPA Workshop 완료!

8개 세션을 통해 JPA의 기초부터 실무 활용까지 학습했습니다. 이제 PHP에서 Spring Boot + JPA로 마이그레이션할 준비가 되었습니다.

4. 실무 운영 전략

4.1 환경별 설정

application.yml 환경별 설정
# application-local.yml
spring:
  flyway:
    clean-disabled: false  # 로컬에서만 clean 허용
  jpa:
    hibernate:
      ddl-auto: validate

# application-prod.yml
spring:
  flyway:
    clean-disabled: true
    out-of-order: false
  jpa:
    hibernate:
      ddl-auto: none  # 운영에서는 절대 auto 금지

4.2 무중단 배포 전략

무중단 배포 시 주의

스키마 변경은 이전 버전 앱과 호환되어야 합니다. 컬럼 삭제/이름 변경은 여러 단계로 나누어 진행합니다.

컬럼 이름 변경 (3단계)
-- Step 1: 새 컬럼 추가 (V1.1)
ALTER TABLE member ADD COLUMN user_name VARCHAR(100);
UPDATE member SET user_name = name;

-- Step 2: 앱 코드 변경 (배포 2)
-- 앱이 새 컬럼(user_name) 사용하도록 변경

-- Step 3: 기존 컬럼 삭제 (V1.2)
ALTER TABLE member DROP COLUMN name;

4.3 트러블슈팅

Checksum 불일치

이미 적용된 파일 수정 시 발생 → flyway_schema_history에서 checksum 업데이트

마이그레이션 실패 후 재시도

실패 기록 삭제 또는 ./gradlew flywayRepair 실행

Flyway Best Practices

  • • 마이그레이션 파일은 절대 수정하지 않는다
  • • 운영 환경에서는 clean-disabled: true 필수
  • • 무중단 배포 시 호환성 고려
  • • 롤백 계획을 항상 준비