배경
프로젝트 초기에는 개발 속도를 우선시해
JPA의 ddl-auto: update와 Java 기반 데이터 초기화 방식을 사용했다.
이 방식은 빠른 개발에는 유리했지만,
서비스 출시를 고려하면서 다음과 같은 한계를 명확히 인식하게 되었다.
- 스키마 변경이 언제, 왜 발생했는지 추적하기 어려움
- 애플리케이션 재기동 시 DB 상태가 달라질 수 있음
- 개발 환경과 운영 환경의 데이터 경계가 흐려짐
이 시점부터 데이터베이스를
“프레임워크가 대신 관리해주는 영역”이 아니라
“서비스 안정성을 좌우하는 핵심 시스템”으로 보게 되었다.
문제 정의
겉으로 드러난 문제는 ddl-auto 설정이었지만,
본질적인 문제는 다음 질문으로 정리되었다.
“DB 스키마 변경에 대한 책임은 어디에 있어야 하는가?”
- JPA의 자동 스키마 생성에 위임할 것인가
- 아니면 애플리케이션 레벨에서 명시적으로 관리할 것인가
운영 환경에서는
- 한 번의 잘못된 스키마 변경이
- 데이터 유실이나 서비스 장애로 이어질 수 있다.
따라서 변경 이력 추적 가능성, 재현 가능성, 환경 간 일관성을 기준으로
스키마 관리 전략을 재설계하기로 결정했다.
해결 전략
1️⃣ 스키마 변경 책임의 분리
- JPA
- 역할: 엔티티 ↔ DB 스키마 간 정합성 검증
- 설정: ddl-auto: validate
- Flyway
- 역할: 실제 스키마 변경의 단일 진실 공급원(Single Source of Truth)
이 구조를 통해
“엔티티는 스키마를 설명하고,
스키마 변경은 SQL로만 발생한다”
는 명확한 책임 분리를 만들었다.
2️⃣ 마이그레이션 파일 기반 버전 관리
모든 DDL/DML 변경을
버전이 명시된 SQL 파일로 관리했다.
- V1__init.sql : 테이블 생성
- V2__insert_basic_data.sql : 서비스 필수 기초 데이터
- V3__insert_reference_data.sql : 정적 참조 데이터
- 개발 환경 전용 마이그레이션 분리
이를 통해 데이터베이스도 코드처럼
“어디까지 적용되었는지 설명 가능한 상태”가 되었다.
3️⃣ 환경 분리 전략
- 공통 마이그레이션: 개발 / 운영 공통
- 개발 전용 마이그레이션: 테스트 데이터만 포함
- 운영 환경:
- 테스트 데이터 미적용
- 기존 DB 적용 시 baseline-on-migrate 사용
4️⃣ 엔티티-DB 매핑의 명시화
Flyway + validate 조합을 사용하면서
Hibernate의 암묵적 규칙에 의존하는 것이 얼마나 위험한지 체감했다.
이에 따라 모든 엔티티에 다음을 명시했다.
- @Table(name = "...")
- @Column(name = "...")
- @GeneratedValue(strategy = GenerationType.IDENTITY)
또한 물리 네이밍 전략을 명확히 지정해
Hibernate가 예측 불가능한 컬럼명을 생성하지 않도록 했다.
👉 이 과정은 단순 설정 변경이 아니라
ORM 추상화의 한계를 의식적으로 인지하는 계기가 되었다.
구현 과정에서 겪은 주요 이슈
▪ 컬럼 네이밍 전략 불일치
- SQL은 camelCase
- Hibernate 기본 전략은 snake_case
➡️ 해결:
- 명시적 매핑 + Naming Strategy 통일
▪ @Lob 사용 시 컬럼 타입 불일치
- ORM 기대 타입과 실제 SQL 타입 불일치로 validation 실패
➡️ 해결:
- JPA/Hiberante가 생성한 스키마를 사실상의 기준으로 사용하고 있었기 때문에 SQL DDL이 기준이 아니라 도메인 정의가 스키마의 출발점이었다.
- 따라서, 실제 DB 스키마를 엔티티 정의에 맞추는 방향으로 수정하였다.
▪ ID 생성 전략 누락
- AUTO 전략 사용 시 MySQL에서 시퀀스 테이블 탐색 문제 발생
➡️ 해결:
- IDENTITY 전략 명시
- DB의 AUTO_INCREMENT 정책과 일치시킴
▪ Flyway Checksum mismatch
- 이미 적용된 마이그레이션 수정으로 인한 오류
➡️ 이슈를 통해 얻은 원칙:
적용된 마이그레이션은 절대 수정하지 않는다.
변경은 항상 “새 버전”으로만 추가한다.
결과
- DB 스키마 변경 이력이 완전히 추적 가능해짐
- 배포 시 DB 관련 리스크 감소
- 개발 / 운영 환경 간 데이터 오염 가능성 제거
- 신규 인원이 프로젝트 구조를 이해하는 데 걸리는 시간 단축
특히,
SQL 마이그레이션 로그만으로 DB의 현재 상태를 설명할 수 있게 된 점이
가장 큰 구조적 개선이었다.
회고 및 학습
이번 경험을 통해 얻은 가장 중요한 교훈은 다음과 같다.
자동화는 편의를 제공할 뿐,
설계에 대한 책임을 대신 지지 않는다.
초기에는 편리했던 자동 스키마 생성이
프로젝트 규모가 커질수록 위험 요소로 변했다
이후에는
- “지금 편한 선택”과 “운영에서 안전한 선택”을 구분해서 판단하고
- 특히 데이터베이스와 관련된 결정은 항상 되돌릴 수 있는지를 기준으로 설계하게 되었다.
'Backend > Trouble Shooting' 카테고리의 다른 글
| [Spring Boot] Swagger(Springdoc)에서 제네릭 공통 응답 스키마가 꼬이는 문제 해결기 (0) | 2026.01.22 |
|---|---|
| [SpringBoot] UTC 시간 처리, 코드가 아니라 설정으로 정리하기 (Hibernate 6 & Jackson) (0) | 2026.01.21 |
| [SpringBoot] 검증 로직의 위치(@Valid vs Service) : 테스트 코드로 드러난 설계 문제 (0) | 2026.01.14 |
| [SpringBoot] 날짜와 시간 처리를 UTC로 표준화하기 (0) | 2026.01.10 |
| [SpringBoot] 프로젝트에 SonarCloud 연동하기: Gradle + GitHub Actions (0) | 2026.01.05 |