콘텐츠 대표 이미지 - DB 버전 관리, 아직도 수동으로 하세요? 😱코드로 스키마 변경 추적하는 프로의 세계로! 🔄

DB 버전 관리, 아직도 수동으로 하세요? 😱
코드로 스키마 변경 추적하는 프로의 세계로! 🔄

야근을 부르는 데이터베이스 수작업은 이제 그만! Git처럼 똑똑하게 DB를 관리하는 비법, 지금 바로 공개합니다.

들어가며: 그날의 악몽, 그리고 한 줄기 빛

자, 여기 한 개발자가 있습니다. 이름은 김대리. 늦은 밤, 드디어 신규 기능 배포를 마쳤습니다. 홀가분한 마음으로 퇴근하려던 찰나, 전화벨이 지옥의 소리처럼 울려 퍼집니다. 📞 "김대리! 지금 서비스 전체가 다운됐어! 빨리 확인해 봐!"

가슴이 철렁 내려앉은 김대리는 부리나케 원인 파악에 나섭니다. 로그를 뒤지고, 서버를 재시작해봐도 소용이 없습니다. 몇 시간의 사투 끝에 발견한 원인은... 아, 신규 기능을 위해 추가했던 데이터베이스 컬럼 하나가 운영 서버에 반영되지 않았던 것입니다. 개발 서버에서는 잘만 돌아가던 코드가 운영 환경에서 `Column not found` 에러를 뿜어내며 장렬히 전사한 거죠.

옆 팀 박과장이 깜빡하고 SQL 스크립트 실행을 놓쳤다고 합니다. 하지만 누구를 탓할 수 있을까요? 중요한 변경 사항을 사람의 기억과 수동 실행에 의존했던 시스템 자체가 문제인 것을요. 이 이야기가 너무나 익숙하게 들린다면, 당신은 이 글을 읽을 준비가 되었습니다. 이건 비단 김대리만의 이야기가 아니니까요. 우리 모두의 이야기일 수 있습니다.

애플리케이션 코드는 Git으로 철저하게 버전을 관리하면서, 왜 데이터베이스는 여전히 '야생의 땅'으로 남아있을까요? `ALTER TABLE...` 쿼리를 DB 툴에서 직접 실행하고, 변경 사항은 위키에 대충 기록하는 방식. 이건 마치 안전장치 없이 외줄 타기를 하는 것과 같습니다. 언젠가는 반드시 떨어지게 되어있죠.

오늘 우리는 이 불안하고 위험한 외줄 타기를 끝내고, 데이터베이스 스키마를 코드처럼 우아하고 안전하게 관리하는 '데이터베이스 버전 관리'의 세계로 떠나보려 합니다. 이 여정이 끝나면, 당신은 더 이상 DB 변경 때문에 밤잠 설치지 않아도 될 겁니다. 자, 준비되셨나요? 프로 개발자로 가는 다음 레벨의 문을 함께 열어봅시다! 🚀


1장: 구시대의 유물 - 우리가 DB를 관리하던 위험한 방법들

데이터베이스 버전 관리의 필요성을 제대로 느끼려면, 먼저 우리가 얼마나 위험한 방식으로 일해왔는지 돌아볼 필요가 있습니다. 아마 아래 방법 중 하나 이상은 당신에게도 익숙할 겁니다. "어? 우리 팀 얘긴데?" 싶어도 너무 좌절하지 마세요. 대부분의 팀이 겪는 성장통이니까요. 😉

방법 1: 카우보이 코딩 🤠 - "일단 운영 DB에 접속해서..."

가장 원시적이고 가장 위험한 방법입니다. 개발자가 DBeaver, HeidiSQL, DataGrip 같은 GUI 툴을 사용해 개발, 스테이징, 심지어 운영 데이터베이스에 직접 접속해서 `ALTER`, `CREATE`, `DROP` 쿼리를 실행하는 방식입니다.

문제점:

  • 추적 불가능: 누가, 언제, 왜 스키마를 변경했는지 알 길이 없습니다. 로그가 남는다 해도, 그 변경이 어떤 기능과 관련 있는지 파악하기는 거의 불가능에 가깝습니다.
  • 인간의 실수(Human Error): `WHERE` 절을 빼먹고 `UPDATE`나 `DELETE`를 실행하는 끔찍한 상상, 해보셨나요? 혹은 개발 DB에 실행해야 할 쿼리를 운영 DB에 실행하는 아찔한 순간... 생각만 해도 등골이 오싹합니다.
  • 협업의 부재: 팀원 A가 추가한 컬럼을 팀원 B가 모르고 다른 작업을 하다가 충돌이 발생합니다. "어? 이 컬럼 언제 생겼어요?" 라는 질문이 터져 나오기 시작하죠.
  • 환경 간 불일치: 개발, 스테이징, 운영 환경의 DB 스키마가 제각각 달라집니다. "제 컴퓨터에선 잘 됐는데요?" 라는 단골 멘트의 주된 원인이 됩니다.
경고! 🚨
운영 DB에 직접 DDL(Data Definition Language) 쿼리를 실행하는 것은 달리는 자동차의 바퀴를 갈아 끼우려는 시도와 같습니다. 아주, 아주 위험합니다. 긴급한 핫픽스가 아니라면 절대 시도해서는 안 됩니다.

방법 2: 공유 폴더의 SQL 파일들 📁 - "final_real_final_v3.sql"

카우보이 코딩보다는 조금 발전한 형태입니다. DB 스키마 변경이 필요할 때마다 SQL 스크립트 파일을 작성해서 팀 공유 폴더(NAS, 구글 드라이브 등)에 업로드하는 방식이죠. 그리고 배포 시 담당자가 이 파일들을 순서대로 실행합니다.

문제점:

  • 버전 관리의 부재: 파일 이름으로 버전을 관리하려는 처절한 노력이 보입니다. `01_create_users.sql`, `02_add_email_to_users.sql`... 처음에는 잘 되는 것 같지만, 곧 `02_add_email_to_users_fix.sql` 같은 파일이 등장하며 혼돈이 시작됩니다.
  • 실행 순서의 모호함: 어떤 파일을 먼저 실행해야 할까요? 파일 이름만으로는 의존성을 파악하기 어렵습니다. 순서를 잘못 실행하면 에러 파티가 열립니다.
  • 적용 여부 확인 불가: "이 스크립트, 운영 서버에 적용됐던가?" 아무도 확신할 수 없습니다. 누군가 수동으로 기록한 문서에 의존하거나, 직접 DB 스키마를 확인해야 합니다.
  • 롤백의 어려움: 변경 사항을 되돌리려면 어떻게 해야 할까요? `DROP TABLE`을 실행했다면? 데이터를 백업해두지 않았다면? 끔찍한 결과가 기다립니다.

방법 3: 위키와 문서 📜 - "Confluence에 정리해뒀어요"

가장 체계적으로 보이지만, 가장 쉽게 현실과 멀어지는 방법입니다. 모든 DB 변경 사항을 위키나 Confluence 같은 문서 도구에 꼼꼼하게 기록하는 방식입니다. 변경 내용, 담당자, 실행일, 관련 티켓 번호까지... 완벽해 보이죠?

문제점:

  • 문서는 거짓말을 한다: 문서는 살아있는 시스템이 아닙니다. 누군가 기록하는 것을 잊거나, 바쁘다는 핑계로 나중에 하려다 까먹으면, 문서는 순식간에 실제 DB 스키마와 달라집니다. 결국 아무도 믿지 못하는 문서가 되어버립니다.
  • 동기화의 어려움: 코드는 Git에, DB 변경 기록은 위키에... 정보가 분산되어 있으면 특정 코드 변경이 어떤 DB 변경과 관련 있는지 추적하기가 매우 어렵습니다.
  • 자동화 불가능: 문서는 그저 문서일 뿐입니다. 실제 DB에 변경 사항을 적용하는 것은 여전히 누군가의 '수작업'에 의존해야 합니다.

이런 구시대적 방법들은 소규모 프로젝트나 1인 개발 환경에서는 어찌어찌 굴러갈 수 있습니다. 하지만 팀의 규모가 커지고 서비스가 복잡해질수록, 이 방식들은 시한폭탄이 되어 우리를 기다릴 뿐입니다. 이제 이 폭탄을 해체하고, 더 안전하고 스마트한 길로 나아갈 시간입니다.


2장: 새로운 희망 - 데이터베이스 버전 관리란 무엇인가?

앞서 살펴본 문제들의 공통점이 무엇일까요? 바로 '추적성(Traceability)', '재현성(Reproducibility)', '자동화(Automation)'의 부재입니다. 데이터베이스 버전 관리는 이 세 가지 문제를 해결하기 위한 철학이자 기술적인 접근 방식입니다.

가장 쉽게 비유하자면, "데이터베이스를 위한 Git"이라고 생각할 수 있습니다. Git이 소스 코드의 모든 변경 이력을 기록하고, 언제든 과거 버전으로 돌아갈 수 있게 해주며, 여러 사람이 협업할 수 있는 기반을 제공하는 것처럼, 데이터베이스 버전 관리 도구는 데이터베이스 스키마에 대해 동일한 역할을 수행합니다.

핵심 컨셉 1: 마이그레이션 (Migrations) 🚚

데이터베이스 버전 관리의 심장과도 같은 개념입니다. 마이그레이션은 데이터베이스 스키마에 대한 *하나의 논리적인 변경 단위*를 담고 있는 스크립트 파일입니다.

예를 들어, `users` 테이블에 `phone_number` 컬럼을 추가하는 작업이 필요하다면, 다음과 같은 내용을 담은 마이그레이션 파일을 하나 생성합니다.

-- V1__Add_phone_number_to_users.sql
ALTER TABLE users ADD COLUMN phone_number VARCHAR(20);

이 작은 파일 하나가 엄청난 변화를 가져옵니다.

  • 명시적인 변경: 이제 DB 변경은 더 이상 누군가의 머릿속이나 임시 메모장에 존재하는 추상적인 작업이 아닙니다. 코드와 마찬가지로 파일로 명확하게 정의됩니다.
  • 버전 관리: 이 마이그레이션 파일은 애플리케이션 코드와 함께 Git으로 관리됩니다. 누가, 언제, 어떤 목적으로 이 파일을 만들었는지 `git blame`으로 추적할 수 있습니다.
  • Up & Down: 좋은 마이그레이션은 변경을 적용하는 'Up' 스크립트와 변경을 되돌리는 'Down' 스크립트를 함께 가집니다. 이를 통해 변경 사항을 안전하게 롤백할 수 있습니다.
    -- V1_UP__Add_phone_number_to_users.sql
    ALTER TABLE users ADD COLUMN phone_number VARCHAR(20);
    
    -- V1_DOWN__Add_phone_number_to_users.sql
    ALTER TABLE users DROP COLUMN phone_number;

핵심 컨셉 2: 스키마 히스토리 테이블 (Schema History Table) 📖

그렇다면, 이 수많은 마이그레이션 파일들 중 어떤 것이 현재 DB에 적용되었는지 어떻게 알 수 있을까요? 바로 '스키마 히스토리 테이블' 덕분입니다.

데이터베이스 버전 관리 도구를 처음 실행하면, 해당 데이터베이스에 `schema_migrations`나 `flyway_schema_history` 같은 이름의 특별한 테이블을 하나 생성합니다. 그리고 마이그레이션을 실행할 때마다, 성공적으로 실행된 마이그레이션의 버전 정보, 실행 시간 등을 이 테이블에 기록합니다.

이것이 바로 '진실의 원천(Source of Truth)'입니다!
어떤 DB의 현재 스키마 상태가 궁금하다면, 더 이상 테이블 구조를 일일이 확인할 필요가 없습니다. 그냥 이 히스토리 테이블만 조회하면 됩니다. 테이블에 기록된 마지막 버전이 바로 해당 DB의 현재 버전인 셈이죠.

도구가 `migrate` 명령을 실행할 때의 동작 순서는 다음과 같습니다.

  1. 데이터베이스에 접속하여 스키마 히스토리 테이블을 확인한다.
  2. 프로젝트의 마이그레이션 파일 목록을 읽어온다.
  3. 히스토리 테이블에 기록되지 않은 새로운 마이그레이션 파일들을 찾아낸다.
  4. 버전 순서대로 새로운 마이그레이션들을 하나씩 실행한다.
  5. 실행에 성공할 때마다 해당 마이그레이션 정보를 히스토리 테이블에 기록한다.

이 간단한 메커니즘 덕분에, 어떤 환경의 데이터베이스든 항상 최신 상태로, 그리고 모든 환경이 동일한 상태로 유지될 수 있습니다.

데이터베이스 마이그레이션 워크플로우 1. 개발자 마이그레이션 작성 2. Git 코드와 함께 커밋 3. CI/CD 파이프라인 자동 실행 4. 데이터베이스 스키마 변경 CI/CD 파이프라인 내부 동작 1. `flyway migrate` 명령어 실행 2. DB의 'schema_history' 테이블 조회 3. 프로젝트의 SQL 파일과 비교 4. 아직 적용되지 않은 새로운 마이그레이션 실행 5. 'schema_history' 테이블에 실행 결과 기록

상태 기반(State-based) vs 마이그레이션 기반(Migration-based)

데이터베이스 버전 관리 접근법은 크게 두 가지로 나뉩니다. 이 둘의 차이를 이해하는 것은 우리 팀에 맞는 도구를 선택하는 데 매우 중요합니다.

1. 마이그레이션 기반 (Migration-based / Imperative)

지금까지 설명한 방식입니다. 개발자가 '어떻게' 변경할지를 명시적인 스크립트(마이그레이션)로 작성하는 방식입니다. 마치 내비게이션에게 "여기서 우회전, 다음 사거리에서 좌회전"이라고 단계별로 지시하는 것과 같습니다.

  • 대표 도구: Flyway, Liquibase, Ruby on Rails Migrations
  • 장점:
    • 제어권: 개발자가 모든 변경 과정을 완벽하게 제어할 수 있습니다. 복잡한 데이터 마이그레이션이나 성능 최적화가 필요한 DDL 변경에 유리합니다.
    • 명확성: 각 버전 사이의 변경 내용이 마이그레이션 파일에 명확하게 기록되어 있어 히스토리를 파악하기 쉽습니다.
  • 단점:
    • 수고로움: 모든 변경 사항에 대해 마이그레이션 스크립트를 수동으로 작성해야 합니다. (물론 일부 도구는 자동 생성을 도와줍니다.)
    • 충돌 가능성: 여러 개발자가 동시에 각자의 브랜치에서 마이그레이션을 생성하면 버전 번호가 겹치거나 충돌할 수 있습니다.

2. 상태 기반 (State-based / Declarative)

개발자가 '무엇을' 원하는지, 즉 최종적으로 바라는 데이터베이스의 '상태'를 정의하면, 도구가 현재 상태와 목표 상태를 비교하여 그 차이를 메우는 SQL 스크립트를 *자동으로 생성*하고 실행하는 방식입니다. 내비게이션에게 최종 목적지만 알려주면 경로를 알아서 찾아주는 것과 같죠.

  • 대표 도구: Django Migrations, Alembic (Python/SQLAlchemy), Entity Framework Migrations (.NET)
  • 장점:
    • 편의성: 개발자는 ORM 모델 코드(예: Python 클래스)만 수정하면 됩니다. 마이그레이션 파일은 도구가 자동으로 생성해주므로 매우 편리하고 생산성이 높습니다.
    • 충돌 감소: 개발자는 마이그레이션 파일 자체를 직접 다룰 일이 적어 충돌 가능성이 줄어듭니다.
  • 단점:
    • 제어의 어려움: 도구가 생성하는 SQL이 항상 최적이 아닐 수 있습니다. 특히 대용량 테이블의 컬럼을 변경하거나 복잡한 데이터 조작이 필요할 때, 개발자가 원하는 대로 스크립트가 생성되지 않을 수 있습니다. (물론 대부분 수동으로 수정할 수 있는 기능을 제공합니다.)
    • 예측 불가능성: 가끔 도구가 현재 스키마를 잘못 파악하거나 의도치 않은 변경 사항을 만들어낼 가능성이 존재합니다.
어떤 방식을 선택해야 할까? 🤔
정답은 없습니다. 팀의 기술 스택, 선호도, 프로젝트의 복잡성에 따라 다릅니다.
  • ORM을 깊게 사용하고 빠른 개발 속도를 중시한다면 상태 기반 방식이 훌륭한 선택입니다.
  • 데이터베이스에 대한 세밀한 제어가 필요하고, SQL에 더 익숙하며, 여러 언어로 구성된 마이크로서비스 환경이라면 마이그레이션 기반 방식이 더 안정적일 수 있습니다.
중요한 것은 어떤 방식이든 '하나의 방식'을 정하고 팀 전체가 일관되게 따르는 것입니다.

3장: 실전! 최고의 도구들을 만나보자 🛠️

개념을 익혔으니 이제 실전에 뛰어들 차례입니다. 세상에는 수많은 데이터베이스 버전 관리 도구가 있지만, 여기서는 가장 널리 쓰이고 검증된 '빅3'를 집중적으로 파헤쳐 보겠습니다. 각 도구의 철학과 사용법, 장단점을 비교하며 우리 팀의 '인생 도구'를 찾아보세요.

도구 1: Flyway - 단순함의 미학 ✈️

"Simple. SQL-based. All you need."

Flyway의 철학은 이 한 문장으로 요약됩니다. 복잡한 XML이나 YAML 설정 파일 대신, 순수한 SQL 파일로 마이그레이션을 관리합니다. '규칙(Convention)을 따르면 설정(Configuration)은 필요 없다'는 원칙을 따르기 때문에 배우기 쉽고 빠르게 적용할 수 있는 것이 최대 장점입니다.

주요 특징

  • 순수 SQL: 개발자에게 가장 익숙한 SQL로 마이그레이션을 작성합니다. 별도의 형식을 배울 필요가 없습니다.
  • 강력한 네이밍 컨벤션: 파일 이름 규칙만 지키면 Flyway가 알아서 버전을 관리하고 순서대로 실행합니다.
  • 다양한 플랫폼 지원: Java 라이브러리로 시작했지만, 지금은 커맨드 라인(CLI) 도구, Maven/Gradle 플러그인 등 다양한 환경을 지원하여 어떤 언어로 된 프로젝트에도 쉽게 통합할 수 있습니다.
  • 자동 체크섬 검증: 한번 적용된 마이그레이션 파일이 나중에 수정되면, Flyway가 체크섬(Checksum) 불일치를 감지하여 실수를 방지합니다.

Flyway 맛보기: 사용자 테이블 만들기

Flyway를 사용하는 것은 정말 간단합니다. 프로젝트의 특정 폴더(기본값: `sql`)에 정해진 규칙에 따라 SQL 파일을 만들기만 하면 됩니다.

파일 이름 규칙: `V{버전}__{설명}.sql`

  • `V`: 버전 마이그레이션을 의미하는 접두사 (반복 가능한 `R`도 있음)
  • `{버전}`: `1`, `1.1`, `202304011030` 등 순서를 나타내는 버전. `_`나 `.`으로 구분.
  • `__`: (언더바 두 개) 버전과 설명을 구분하는 구분자.
  • `{설명}`: 이 마이그레이션이 무엇을 하는지 설명.

1. 마이그레이션 파일 생성

프로젝트 내 `db/migration` 폴더를 만들고, 첫 번째 마이그레이션 파일을 작성합니다.

`db/migration/V1__Create_users_table.sql`

CREATE TABLE users (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50) NOT NULL UNIQUE,
    email VARCHAR(100) NOT NULL UNIQUE,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

2. 두 번째 마이그레이션 파일 생성

사용자 테이블에 `password` 컬럼을 추가해 봅시다.

`db/migration/V2__Add_password_to_users.table.sql`

ALTER TABLE users ADD COLUMN password_hash VARCHAR(255) NOT NULL;

3. 마이그레이션 실행

터미널에서 Flyway CLI를 사용하여 마이그레이션을 실행합니다. (DB 연결 정보는 설정 파일에 미리 정의)

$ flyway migrate

이 명령 하나로 Flyway는 다음 작업을 수행합니다.

  1. DB에 접속하여 `flyway_schema_history` 테이블이 없으면 생성합니다.
  2. `db/migration` 폴더를 스캔하여 `V1`과 `V2` 파일을 찾습니다.
  3. `flyway_schema_history` 테이블을 확인하여 두 파일이 아직 적용되지 않았음을 확인합니다.
  4. `V1` 스크립트를 실행하고, 성공하면 히스토리 테이블에 기록합니다.
  5. `V2` 스크립트를 실행하고, 성공하면 히스토리 테이블에 기록합니다.

이제 DB에는 `users` 테이블이 생성되었고, `password_hash` 컬럼까지 추가된 상태가 됩니다. `flyway_schema_history` 테이블을 조회해보면 다음과 같은 기록이 남아있을 겁니다.

+-------------+---------+--------------------------+----------+---------------------+---------+
| installed_rank | version | description              | type     | installed_on        | success |
+-------------+---------+--------------------------+----------+---------------------+---------+
| 1           | 1       | Create users table       | SQL      | 2023-10-27 10:00:00 | 1       |
| 2           | 2       | Add password to users... | SQL      | 2023-10-27 10:00:01 | 1       |
+-------------+---------+--------------------------+----------+---------------------+---------+
Flyway, 언제 사용하면 좋을까?
  • SQL에 익숙하고, 순수 SQL로 모든 것을 제어하고 싶을 때.
  • 빠르게 데이터베이스 버전 관리를 도입하고 싶을 때.
  • Java, Kotlin, Scala 등 JVM 기반 프로젝트 (스프링 부트와 통합이 매우 뛰어남).
  • 마이크로서비스 아키텍처에서 각 서비스가 자신의 DB 스키마를 관리할 때.

도구 2: Liquibase - 유연함의 끝판왕 🌊

"Flexible. Database-agnostic. Powerful."

Flyway가 단순함과 규약을 중시한다면, Liquibase(리퀴베이스)는 유연성과 데이터베이스 독립성을 최고의 가치로 둡니다. 마이그레이션을 SQL뿐만 아니라 XML, YAML, JSON 형식으로도 작성할 수 있는 것이 가장 큰 특징입니다.

주요 특징

  • 다양한 포맷 지원: 개발팀이 가장 선호하는 형식(XML, YAML, JSON, SQL)으로 변경사항(Changeset)을 작성할 수 있습니다.
  • 데이터베이스 독립성: `` 같은 추상화된 태그를 사용하면, Liquibase가 타겟 데이터베이스(MySQL, PostgreSQL, Oracle 등)에 맞는 실제 SQL을 생성해줍니다. 이를 통해 특정 DB 벤더에 종속되지 않는 마이그레이션 작성이 가능합니다.
  • 고급 기능: 롤백(Rollback) 기능이 매우 강력하며, 변경 실행 전 사전 조건을 체크하는 `preConditions`, 컨텍스트에 따라 다른 변경을 적용하는 `contexts` 등 엔터프라이즈급 기능을 다수 제공합니다.
  • 변경세트(Changeset): Liquibase의 마이그레이션 단위입니다. 각 변경세트는 `author`와 `id`로 고유하게 식별되며, 트랜잭션처럼 하나의 단위로 실행됩니다.

Liquibase 맛보기: YAML로 사용자 테이블 만들기

Liquibase는 `db.changelog-master.yaml` 같은 마스터 파일이 다른 변경 로그 파일들을 포함하는 계층적인 구조를 가집니다.

`db/changelog/db.changelog-master.yaml`

databaseChangeLog:
  - include:
      file: db/changelog/001-create-users-table.yaml

`db/changelog/001-create-users-table.yaml`

databaseChangeLog:
  - changeSet:
      id: 1
      author: jaenung-dev
      changes:
        - createTable:
            tableName: users
            columns:
              - column:
                  name: id
                  type: BIGINT
                  autoIncrement: true
                  constraints:
                    primaryKey: true
                    nullable: false
              - column:
                  name: username
                  type: VARCHAR(50)
                  constraints:
                    unique: true
                    nullable: false
              - column:
                  name: email
                  type: VARCHAR(100)
                  constraints:
                    unique: true
                    nullable: false
        - rollback:
            dropTable:
              tableName: users

위 YAML 파일을 보세요. SQL을 한 줄도 쓰지 않았지만, `users` 테이블을 어떻게 만들어야 하는지 명확하게 정의되어 있습니다. `` 태그는 Liquibase가 알아서 MySQL용 `CREATE TABLE ...` 구문이나 PostgreSQL용 구문으로 변환해줍니다. 또한, `rollback` 섹션에 이 변경을 되돌리는 방법(`dropTable`)까지 명시해두어 안전성을 높였습니다.

마이그레이션 실행

터미널에서 Liquibase CLI를 사용하여 마이그레이션을 실행합니다.

$ liquibase update

Liquibase는 `DATABASECHANGELOG`와 `DATABASECHANGELOGLOCK`이라는 두 개의 테이블을 생성하여 히스토리와 동시 실행 방지를 관리합니다. `update` 명령을 실행하면, `001-create-users-table.yaml` 파일의 `changeSet`이 실행되고 그 결과가 `DATABASECHANGELOG` 테이블에 기록됩니다.

Liquibase, 언제 사용하면 좋을까? 🚨
  • 프로젝트가 여러 종류의 데이터베이스를 동시에 지원해야 할 때.
  • XML이나 YAML 형식의 선언적인 스키마 정의를 선호할 때.
  • 롤백, 사전 조건 체크 등 복잡하고 정교한 마이그레이션 시나리오가 필요할 때.
  • DBA(데이터베이스 관리자)와 개발자가 협업하며, DBA가 SQL이 아닌 변경세트를 검토하는 워크플로우를 가질 때.

도구 3: ORM 통합 도구 (Alembic, Django Migrations) - 코드와 하나 되다 🧩

"Your code is the source of truth."

이 접근법은 애플리케이션의 모델 코드(ORM 모델)를 '진실의 원천'으로 삼습니다. 개발자는 SQL이나 XML 대신, 익숙한 프로그래밍 언어(Python, C# 등)로 모델 클래스를 수정합니다. 그러면 마이그레이션 도구가 이전 모델 상태와 현재 모델 상태를 비교하여 필요한 스키마 변경 스크립트를 *자동으로 생성*해줍니다.

주요 특징

  • 자동 생성(Autogeneration): 개발 생산성이 극적으로 향상됩니다. 모델 코드만 바꾸고 명령어 한 줄만 실행하면 마이그레이션 파일이 뚝딱 만들어집니다.
  • 코드 중심: 데이터베이스 스키마가 더 이상 코드와 분리된 존재가 아닙니다. 코드의 일부로서 완벽하게 통합되어 관리됩니다.
  • 높은 추상화: 개발자는 특정 데이터베이스의 SQL 문법을 몰라도 스키마를 변경할 수 있습니다. ORM이 모든 것을 처리해줍니다.
  • 안전 장치: 자동 생성된 스크립트는 개발자가 최종적으로 검토하고 수정할 수 있습니다. 복잡한 변경은 수동으로 스크립트를 편집하여 보완할 수 있습니다.

Alembic (Python/SQLAlchemy) 맛보기

Python 생태계에서 가장 널리 쓰이는 SQLAlchemy ORM을 위한 마이그레이션 도구인 Alembic을 예로 들어보겠습니다.

1. 모델 코드 작성

먼저 SQLAlchemy 모델을 정의합니다.

`models.py`

from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    name = Column(String(50))
    fullname = Column(String(50))

2. 마이그레이션 자동 생성

터미널에서 Alembic에게 마이그레이션을 생성하라고 명령합니다.

$ alembic revision --autogenerate -m "Create user table"

이 명령을 실행하면, Alembic은 현재 DB 스키마(아무것도 없는 상태)와 `models.py`의 상태를 비교하여, `versions` 폴더 안에 다음과 같은 Python 스크립트를 생성합니다.

`versions/s0m3r4nd0mh4sh_create_user_table.py`

"""Create user table

Revision ID: s0m3r4nd0mh4sh
Revises: 
Create Date: 2023-10-27 11:00:00.000000

"""
from alembic import op
import sqlalchemy as sa

# revision identifiers, used by Alembic.
revision = 's0m3r4nd0mh4sh'
down_revision = None
branch_labels = None
depends_on = None

def upgrade():
    op.create_table('users',
        sa.Column('id', sa.Integer(), nullable=False),
        sa.Column('name', sa.String(length=50), nullable=True),
        sa.Column('fullname', sa.String(length=50), nullable=True),
        sa.PrimaryKeyConstraint('id')
    )

def downgrade():
    op.drop_table('users')

보시다시피, 테이블을 생성하는 `upgrade` 함수와 테이블을 삭제하는 `downgrade` 함수가 자동으로 만들어졌습니다.

3. 모델 코드 수정 및 2차 마이그레이션

이제 `User` 모델에 `email` 컬럼을 추가해 봅시다.

`models.py` (수정 후)

class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    name = Column(String(50))
    fullname = Column(String(50))
    email = Column(String(100), nullable=False) # 이 줄 추가!

다시 자동 생성 명령을 실행합니다.

$ alembic revision --autogenerate -m "Add email column to user"

Alembic은 `users` 테이블에 `email` 컬럼이 없는 것을 감지하고, 컬럼을 추가하고 삭제하는 `upgrade`/`downgrade` 함수를 가진 새로운 마이그레이션 파일을 생성해줍니다.

4. 마이그레이션 적용

생성된 마이그레이션들을 DB에 적용하려면 다음 명령을 실행합니다.

$ alembic upgrade head

이 명령은 `alembic_version` 테이블을 참조하여 아직 적용되지 않은 모든 마이그레이션을 최신 버전(`head`)까지 순차적으로 실행합니다.

ORM 통합 도구, 언제 사용하면 좋을까? 🤔
  • Django, SQLAlchemy, Ruby on Rails, Entity Framework 등 특정 ORM 프레임워크를 깊이 사용하고 있을 때.
  • 빠른 프로토타이핑과 개발 속도가 매우 중요할 때.
  • 팀원들이 SQL보다 애플리케이션 코드에 더 익숙할 때.
  • 대부분의 스키마 변경이 모델 클래스의 간단한 수정으로 끝날 때.

이러한 도구들은 개발의 편의성을 극대화해주지만, 때로는 너무 많은 것을 감춰서 문제가 발생했을 때 원인 파악이 어려울 수도 있다는 점을 기억해야 합니다. 자동 생성된 코드를 맹신하지 말고, 항상 내용을 확인하는 습관이 중요합니다.


4장: 프로의 길 - 데이터베이스 버전 관리 베스트 프랙티스 🏆

좋은 도구를 선택하는 것은 전투에서 좋은 무기를 고르는 것과 같습니다. 하지만 전투에서 이기려면 무기를 잘 다루는 법, 즉 전략과 전술을 알아야 합니다. 데이터베이스 버전 관리도 마찬가지입니다. 단순히 도구를 사용하는 것을 넘어, 팀의 생산성과 안정성을 극대화하는 '베스트 프랙티스'를 알아봅시다.

1. 마이그레이션은 작고, 원자적으로 (Small & Atomic)

하나의 마이그레이션 파일에는 하나의 논리적인 변경만 담아야 합니다. 예를 들어, `users` 테이블에 컬럼을 추가하고, `products` 테이블을 새로 만들고, `orders` 테이블의 인덱스를 수정하는 세 가지 작업을 하나의 마이그레이션 파일에 모두 넣지 마세요.

나쁜 예 👎: `V3__add_user_column_and_create_product_table_and_update_order_index.sql`

좋은 예 👍:

  • `V3__add_nickname_to_users.sql`
  • `V4__create_products_table.sql`
  • `V5__add_index_on_created_at_to_orders.sql`

왜 그래야 할까요?

  • 문제 해결 용이: 만약 마이그레이션 실행 중 에러가 발생하면, 어떤 변경에서 문제가 생겼는지 즉시 파악할 수 있습니다. 거대한 마이그레이션 파일의 중간에서 에러가 나면, 어디까지 적용되었고 어디부터 롤백해야 하는지 파악하기가 매우 어렵습니다.
  • 코드 리뷰 용이: 팀 동료가 코드 리뷰를 할 때, 작고 명확한 단위의 변경은 이해하고 검토하기가 훨씬 쉽습니다.
  • 롤백 용이: 특정 기능 하나만 롤백해야 할 때, 해당 기능과 관련된 마이그레이션만 정확히 되돌릴 수 있습니다.

2. 모든 마이그레이션은 되돌릴 수 있어야 한다 (Reversible)

실수는 언제나 발생할 수 있습니다. 배포한 기능에 심각한 버그가 발견되어 긴급 롤백을 해야 할 수도 있습니다. 이때 애플리케이션 코드는 `git revert`로 쉽게 되돌릴 수 있지만, 데이터베이스는 어떨까요?

'Down' 마이그레이션을 작성하는 것을 절대 게을리하지 마세요. Flyway에서는 `undo` 스크립트를, Liquibase에서는 `rollback` 구문을, Alembic에서는 `downgrade` 함수를 반드시 작성해야 합니다.

주의: 파괴적인 변경 (Destructive Changes)
`DROP TABLE`이나 `DROP COLUMN` 같은 파괴적인 변경의 `down` 마이그레이션은 테이블이나 컬럼을 다시 만드는 것입니다. 하지만 그 안의 데이터는 어떻게 할까요? 한번 삭제된 데이터는 돌아오지 않습니다. 따라서 이런 변경은 특히 더 신중해야 하며, 롤백 시나리오를 팀과 충분히 논의해야 합니다. 데이터를 백업하거나, 바로 삭제하는 대신 `is_deleted` 같은 플래그를 사용하는 소프트 삭제(Soft Delete) 전략을 고려해볼 수 있습니다.

3. 한번 적용된 마이그레이션은 절대 수정하지 마라 (Immutable)

Git의 커밋 히스토리를 마음대로 `rebase`나 `force push`로 수정하지 않는 것처럼, 한번 팀원들과 공유되거나 운영 서버에 적용된 마이그레이션 파일은 절대 수정해서는 안 됩니다.

예를 들어, `V2__add_email_to_users.sql`을 실행했는데, 컬럼 타입을 `VARCHAR(100)` 대신 `VARCHAR(50)`으로 잘못 만들었다는 사실을 뒤늦게 깨달았다고 가정해봅시다. 이때 `V2` 파일을 직접 수정해서는 안 됩니다. 왜냐하면 동료들의 개발 DB나 스테이징 DB에는 이미 체크섬이 기록된 `V2`가 적용되어 있기 때문입니다. 파일을 수정하면 Flyway 같은 도구는 체크섬 오류를 뿜어내며 실행을 거부할 것입니다.

올바른 해결책: 실수를 바로잡는 새로운 마이그레이션을 추가하세요.

`V3__change_email_column_type_in_users.sql`

ALTER TABLE users MODIFY COLUMN email VARCHAR(100);

마이그레이션은 데이터베이스 스키마의 '역사'입니다. 역사를 바꾸려 하지 말고, 새로운 역사를 써나가야 합니다.

4. CI/CD 파이프라인에 통합하라 (Automate Everything)

데이터베이스 버전 관리의 진정한 힘은 '자동화'에서 나옵니다. 마이그레이션 실행을 개발자의 로컬 PC나 수동 작업에 의존하는 순간, 다시 예전의 실수들이 반복될 수 있습니다.

이상적인 워크플로우는 다음과 같습니다.

  1. 개발자가 기능 브랜치에서 코드와 함께 마이그레이션 스크립트를 작성하여 커밋/푸시합니다.
  2. Pull Request(PR)가 생성되면, CI(Continuous Integration) 서버가 자동으로 코드를 빌드하고 테스트를 실행합니다. 이때, 테스트용 DB에 마이그레이션을 적용하여 유효성을 검증할 수 있습니다.
  3. PR이 메인 브랜치에 머지되면, CD(Continuous Deployment) 파이프라인이 트리거됩니다.
  4. CD 파이프라인은 애플리케이션을 배포하기 *직전*에, 스테이징 또는 운영 데이터베이스에 대해 마이그레이션 도구(`flyway migrate`, `liquibase update` 등)를 실행합니다.
  5. 마이그레이션이 성공하면, 새로운 버전의 애플리케이션을 배포합니다. 만약 마이그레이션이 실패하면, 배포는 즉시 중단되고 팀에게 알림이 갑니다.

이러한 자동화는 사람의 실수를 원천적으로 차단하고, 모든 환경의 DB 스키마가 코드와 항상 일치하도록 보장합니다. 재능넷(Jaenung.net)과 같은 대규모 플랫폼에서는 이런 자동화된 워크플로우가 서비스 안정성의 핵심적인 역할을 합니다.

5. 스키마 변경과 데이터 변경을 구분하라 (Schema vs. Data Migration)

마이그레이션은 주로 스키마(테이블 구조, 인덱스 등)를 변경하는 데 사용되지만, 때로는 대량의 데이터를 수정하거나 이동해야 할 때도 있습니다. 이를 '데이터 마이그레이션'이라고 합니다.

예를 들어, `users` 테이블에 있던 `first_name`과 `last_name` 컬럼을 합쳐서 `full_name`이라는 새로운 컬럼으로 옮기는 작업을 상상해봅시다. 이 작업은 두 단계로 이루어집니다.

  1. 스키마 마이그레이션: `full_name` 컬럼을 추가한다.
  2. 데이터 마이그레이션: 기존 모든 유저의 `first_name`과 `last_name`을 조합하여 `full_name`에 채워 넣는 스크립트를 실행한다.

데이터 마이그레이션은 수백만, 수천만 건의 데이터를 처리해야 할 수도 있기 때문에 오랜 시간이 걸릴 수 있습니다. 배포 파이프라인에서 이런 작업이 실행되면 전체 배포가 지연되거나, 테이블에 락(Lock)이 걸려 서비스 장애를 유발할 수도 있습니다. 따라서 데이터 마이그레이션은 별도의 스크립트로 작성하고, 서비스 트래픽이 적은 시간에 신중하게 실행하는 전략이 필요합니다.


5장: 심화 과정 - 무중단 배포와 전쟁 이야기 ⚔️

기본기를 다졌다면 이제 고수들의 영역으로 나아갈 시간입니다. 실제 운영 환경에서는 단순히 스키마를 바꾸는 것을 넘어, '어떻게 하면 서비스를 중단시키지 않고 바꿀 수 있는가'가 핵심 과제가 됩니다. 여기서는 몇 가지 고급 시나리오와 그 해결책을 다뤄보겠습니다.

시나리오 1: 무중단 배포를 위한 스키마 변경 (Zero-Downtime Deployment)

사용자가 서비스를 이용하는 중에 DB 스키마를 변경하고 애플리케이션을 배포하려면 어떻게 해야 할까요? 구버전 애플리케이션과 신버전 애플리케이션이 잠시 동안 공존하는 상황을 고려해야 합니다. 이때 사용하는 가장 대표적인 패턴이 바로 '확장 및 축소 (Expand and Contract)' 패턴입니다.

미션: `users` 테이블의 `name` 컬럼을 `nickname`으로 변경하기

단순히 `ALTER TABLE users RENAME COLUMN name TO nickname;`을 실행하고 새 코드를 배포하면, 구버전 코드는 `nickname` 컬럼을 찾지 못해 에러를 뿜고, 신버전 코드는 `name` 컬럼을 찾지 못해 에러를 뿜는 대혼란이 발생합니다.

올바른 작전 계획 (총 3단계 배포):

1단계: 확장 (Expand)

  • DB 변경 (마이그레이션 V10): `nickname` 컬럼을 새로 추가합니다. 아직 `name` 컬럼은 삭제하지 않습니다.
    ALTER TABLE users ADD COLUMN nickname VARCHAR(50);
  • 애플리케이션 배포 (v2 코드):
    • 데이터를 읽을 때: `nickname`이 있으면 `nickname`을 읽고, 없으면 `name`을 읽습니다.
    • 데이터를 쓸 때: `name`과 `nickname` 두 컬럼에 모두 같은 값을 씁니다.

👉 이 단계가 완료되면, 구버전 코드(v1)와 신버전 코드(v2)가 동시에 실행되어도 문제가 없습니다. 새로 생성/수정되는 데이터는 두 컬럼에 모두 기록됩니다.

2단계: 데이터 이전 (Data Migration)

  • DB 변경 (데이터 마이그레이션 V11): 아직 `nickname`이 채워지지 않은 오래된 데이터들을 위해 `name`의 값을 `nickname`으로 복사하는 스크립트를 실행합니다.
    UPDATE users SET nickname = name WHERE nickname IS NULL;
  • 애플리케이션 배포 (v3 코드):
    • 이제 모든 코드는 `nickname` 컬럼만 읽고 쓰도록 수정합니다. 더 이상 `name` 컬럼은 참조하지 않습니다.

👉 이 단계가 완료되면, 모든 데이터가 `nickname`으로 이전되었고, 애플리케이션 코드도 더 이상 `name` 컬럼에 의존하지 않게 됩니다.

3단계: 축소 (Contract)

  • DB 변경 (마이그레이션 V12): 더 이상 사용되지 않는 `name` 컬럼을 안전하게 삭제합니다.
    ALTER TABLE users DROP COLUMN name;

이처럼 여러 단계에 걸쳐 점진적으로 변경을 적용함으로써, 서비스 중단 없이 안전하게 데이터베이스 스키마를 진화시킬 수 있습니다. 복잡해 보이지만, 서비스 안정성을 위해서는 반드시 필요한 과정입니다.

시나리오 2: 브랜치와 마이그레이션 충돌 지옥 🔥

팀 개발 환경에서 흔히 발생하는 문제입니다.

  • 개발자 A가 `feature/add-profile` 브랜치에서 `V10__create_profiles_table.sql` 마이그레이션을 만들었습니다.
  • 동시에 개발자 B가 `feature/add-posts` 브랜치에서 `V10__create_posts_table.sql` 마이그레이션을 만들었습니다.

두 브랜치를 `main`에 머지하려는 순간, 버전 번호 `V10`이 충돌합니다! Flyway나 Liquibase는 버전이 중복되는 것을 허용하지 않습니다. 어떻게 해결해야 할까요?

해결책:

  1. 타임스탬프 기반 버전 관리: 버전 번호를 `10`, `11` 같은 정수 대신 `20231027143000` (YYYYMMDDHHMMSS) 같은 타임스탬프로 사용하는 전략입니다. 이렇게 하면 개발자들이 동시에 마이그레이션을 생성해도 버전이 겹칠 확률이 거의 없습니다. 많은 도구들이 이 방식을 지원합니다.
  2. 리베이스(Rebase)와 버전 수정: 먼저 머지된 브랜치를 기준으로, 나중에 머지하려는 브랜치를 `rebase`합니다. 예를 들어, A의 브랜치가 먼저 머지되었다면, B는 자신의 브랜치를 `main`에 리베이스한 후, 자신의 마이그레이션 파일 이름을 `V11__create_posts_table.sql`로 수정하여 커밋해야 합니다. 약간의 수고가 필요하지만, 버전 히스토리를 깔끔하게 유지할 수 있습니다.

어떤 방식을 사용하든, 팀 내에서 명확한 규칙을 정하고 공유하는 것이 중요합니다.

프로팁: 마이그레이션은 PR의 최상단에!
코드 리뷰를 할 때, 마이그레이션 스크립트는 매우 중요하게 검토해야 할 대상입니다. PR 설명의 가장 윗부분에 어떤 DB 변경이 포함되어 있는지 명시하고, 리뷰어들이 마이그레이션 파일을 가장 먼저 보도록 안내하는 것이 좋습니다. 잘못된 SQL 쿼리 하나가 전체 서비스를 마비시킬 수도 있으니까요.

마치며: 이제 당신도 DB 마스터!

정말 긴 여정이었습니다. 우리는 데이터베이스를 수동으로 관리하던 혼돈의 시대를 지나, 마이그레이션이라는 개념을 배우고, Flyway, Liquibase, Alembic 같은 강력한 도구들을 만났습니다. 그리고 마지막으로 무중단 배포와 같은 실전 전략까지 살펴보았죠.

데이터베이스 버전 관리는 단순히 '편한' 기술이 아닙니다. 이것은 소프트웨어 개발의 '전문성'과 '안정성'을 가르는 중요한 척도입니다. 코드로 스키마를 관리하고, 모든 변경 사항을 추적하며, 배포를 자동화하는 것은 현대적인 개발팀이 갖춰야 할 필수 역량입니다.

오늘 배운 내용이 조금은 복잡하고 어렵게 느껴질 수도 있습니다. 괜찮습니다. 처음부터 모든 것을 완벽하게 할 필요는 없습니다. 지금 당장 당신의 작은 사이드 프로젝트에 Flyway를 한번 적용해보세요. `V1__init.sql` 파일을 만들고 `flyway migrate`를 실행해보는 그 작은 첫걸음이, 당신을 'DB 변경이 두렵지 않은 개발자'로 만들어 줄 것입니다.

이 기술 하나만 제대로 익혀도, 당신의 개발자로서의 가치는 퀀텀 점프할 거예요! 더 이상 DB 작업 때문에 야근하지 말고, 칼퇴해서 소중한 저녁 시간을 즐기시길 바랍니다. 만약 이 과정이 너무 어렵게 느껴진다면, 재능 공유 플랫폼 재능넷에서 데이터베이스 전문가의 도움을 받아보는 것도 좋은 방법이 될 수 있습니다. 함께 성장하고 지식을 나누는 문화가 우리를 더 나은 개발자로 만들어 줄 테니까요.

이제 당신의 데이터베이스에 질서를 부여할 시간입니다. 행운을 빕니다! 💻✨

댓글 작성

이 글에 대한 여러분의 생각을 들려주세요

댓글 0
댓글을 불러오는 중입니다.

아직 댓글이 없습니다.