주니어 개발자들의 배포 날 밤, 가장 많이 터지는 대참사가 무엇일까요? 바로 "자바 코드는 최신으로 배포했는데, 운영 DB에 컬럼(Column) 추가하는 걸 깜빡해서 서버가 죽는 현상"입니다. JPA hibernate.ddl-auto=update 기능이 있긴 하지만, 실무 운영 서버에서 이 옵션을 켜는 것은 "내 손으로 DB를 날려먹고 사직서를 쓰겠다"는 선언과 같습니다(절대 사용 금지). 결국 개발자가 수동으로 ALTER TABLE 쿼리를 짜서 DBA에게 넘겨주거나 직접 날려야 하는데, 이 과정에서 사람이 실수하거나 쿼리를 누락하면 걷잡을 수 없는 재앙이 벌어집니다. "소스코드는 Git으로 1.0, 1.1 버전을 깔끔하게 관리하면서, 왜 데이터베이스 스키마(DDL)는 원시인처럼 구두로 관리하는가? DB 스키마도 코드로 만들어서 Git에 올리고, 서버가 켜질 때 알아서 버전을 맞춰주면 안 될까?" 이 위대한 철학을 실현한 도구가 바로 Flyway(플라이웨이)입니다.
이번 [Spring Boot 실무 완벽 가이드] 시리즈에서는 "DB 배포 사고 0%를 위한 마이그레이션 툴!" Spring Boot와 Flyway 연동 아키텍처에 대하여 다뤄보겠습니다.

🗂️ 1. Flyway의 핵심: 규칙적인 파일명과 히스토리 테이블
Flyway를 도입하는 순간, 여러분은 더 이상 DB 툴(DBeaver)을 열고 테이블을 만들지 않습니다. 오직 .sql 파일만 작성합니다.

스프링 부트 프로젝트의 src/main/resources/db/migration 폴더에 아주 엄격한 이름 규칙(Naming Convention)을 지켜서 파일을 만듭니다.
V1__init.sql: (V + 숫자 + 언더바 2개 + 설명) 최초 테이블 생성 DDLV2__add_email_to_user.sql: User 테이블에 email 컬럼을 추가하는 DDLV3__insert_admin_data.sql: 관리자 기본 데이터 삽입 DML
이제 스프링 부트 서버를 켜면(Run), Flyway가 DB에 flyway_schema_history라는 자기만의 비밀 테이블을 몰래 만듭니다. 그리고 "음, 저번에 V1과 V2까지는 내가 실행했었지. 어? 이번에 V3 파일이 새로 생겼네? V3 파일만 실행하고 히스토리에 기록해야지!"라고 아주 똑똑하게 밀린 숙제(Migration)를 처리합니다.
⚙️ 2. Spring Boot에서 Flyway 1분 세팅하기
설정은 너무나도 쉽습니다. 의존성 한 줄만 추가하면 스프링 부트가 알아서 Flyway를 낚아챕니다.
// build.gradle
implementation 'org.flywaydb:flyway-core'
implementation 'org.flywaydb:flyway-mysql' // MySQL 쓰면 이것도 추가 (Boot 3.x 기준)
-- resources/db/migration/V1__create_users_table.sql
CREATE TABLE users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
이 상태에서 서버를 켜면, 콘솔에 Successfully applied 1 migration to schema라는 로그가 뜨며 실제 DB에 users 테이블이 생성됩니다. 앞으로 application.yml의 ddl-auto 속성은 무조건 validate(엔티티와 DB가 똑같이 생겼는지만 검사하고 안 고쳐줌)로 고정해 두고 안심하고 꿀잠을 자면 됩니다.
💀 3. Flyway의 절대 금기: 체크섬(Checksum)의 저주
Flyway를 쓸 때 초보들이 가장 많이 저지르는 치명적인 실수가 있습니다. 바로 '이미 배포가 끝난 과거의 파일(예: V1)의 내용을 얍삽하게 수정하는 행위'입니다.
개발자 철수가 "아, V1 파일에 username 길이를 50에서 100으로 바꾸고 싶은데... 귀찮은데 V1 파일을 그냥 슥 고치자!"라고 파일 내용을 변경한 뒤 서버를 켭니다. 그러면 서버 기동이 실패하며 새빨간 에러가 터집니다!
Flyway는 SQL 파일을 실행할 때 파일 내용의 고유한 지문(Checksum)을 히스토리 테이블에 기록해 둡니다. 서버가 뜰 때마다 "내가 옛날에 실행했던 V1 파일의 지문과, 지금 폴더에 있는 V1 파일의 지문이 다르네? 누군가 몰래 과거를 조작했어! 위험해! 서버 구동 중지!"라며 막아버리는 것입니다. 한 번 배포된 V1 파일은 무덤까지 가져가야 하는 '불변(Immutable)'의 영역입니다. 수정하고 싶다면 무조건 새로운 V2__alter_username.sql 파일을 만들어서 ALTER TABLE 쿼리를 짜야만 합니다!
🎯 4. 마무리 및 다음 단계
지금까지 수동 쿼리 실행의 공포에서 벗어나, DB 스키마 변경 내역을 자바 코드와 함께 Git으로 완벽하게 형상 관리하고, CI/CD 파이프라인에서 무인 배포를 가능하게 만들어주는 기적의 툴, Flyway의 원리와 절대 금기 사항(Checksum)에 대해 상세히 다루어 보았습니다. 이제 여러분의 배포 날 밤은 평화로울 것입니다.
서버 코드도, DB 스키마도 완벽합니다. 이제 마지막 관문은 '테스트(Test)'입니다. 20단계에서 배운 단위 테스트(Unit Test)는 가짜 객체(Mock)를 써서 아주 빠르지만, 실제 DB에 쿼리가 잘 먹히는지는 검증하지 못합니다. 그래서 통합 테스트(Integration Test)를 짜는데, 내 노트북 환경(윈도우 + 로컬 MySQL)에서는 테스트가 성공했는데, 젠킨스 서버(리눅스 + 텅 빈 DB)에서는 테스트가 다 깨져버립니다! "테스트 코드를 돌릴 때마다, 그 테스트 전용으로 완벽하게 격리된 '진짜 MySQL과 진짜 Redis'를 1회용으로 띄웠다가 부수고 끝낼 순 없을까?" 이어지는 48단계 포스팅에서는 "테스트 환경 불일치의 저주를 완벽하게 끊어버리는 궁극의 통합 테스트 무기!" Docker와 연동하여 1회용 컨테이너를 띄우는 Testcontainers 완벽 가이드에 대해 아주 뼈 때리게 파헤쳐 보겠습니다!
'Framework > Spring Boot' 카테고리의 다른 글
| [Spring Boot] SonarQube (소나큐브): 코드 퀄리티를 감시하는 무자비한 경찰관 (0) | 2026.08.12 |
|---|---|
| [Spring Boot] Testcontainers: 완벽히 고립된 통합 테스트의 마법 (0) | 2026.08.12 |
| [Spring Boot] 멀티 모듈(Multi-Module) 아키텍처: 거대해진 모놀리스의 분리 (0) | 2026.08.12 |
| [Spring Boot] WebRTC 화상 회의: P2P 시그널링 서버의 완성 (0) | 2026.08.12 |
| gRPC 통신: JSON을 버리고 초고속 바이너리(Binary)를 달다 (0) | 2026.08.05 |