Spring Data JPA를 사용하면 ddl-auto=update 옵션 하나로 엔티티 변경 사항을 데이터베이스에 쉽게 반영할 수 있습니다. 하지만 서비스가 점차 커지고 실제 운영 환경(Production)에 가까워질수록 이 자동 기능만으로는 한계에 부딪히게 됩니다.
단순한 컬럼 추가가 아니라 기존 데이터의 값을 보정해야 하거나, 복잡한 제약조건(Constraint)을 변경해야 할 때 개발자는 딜레마에 빠집니다.
이때 활용할 수 있는 강력하고 직관적인 방법이 바로 ApplicationRunner와 JdbcTemplate을 활용한 커스텀 스키마 마이그레이션(Schema Migration) 설정입니다.

1. 이 파일(SchemaCompatibilityConfig)의 진짜 용도는 무엇일까요?
이 설정 파일은 "스프링 부트 애플리케이션이 구동되는 최우선 시점에, 개발자가 작성한 생(Raw) SQL 스크립트를 순차적으로 실행하여 DB 스키마와 데이터를 최신 상태로 동기화(패치)하는 역할"을 합니다.
주로 다음과 같은 상황에서 JPA의 빈자리를 완벽하게 메워줍니다.
- 복잡한 제약조건 관리: JPA로는 세밀하게 제어하기 힘든 UNIQUE, CHECK, FOREIGN KEY 제약조건을 안전하게 추가하거나 삭제할 때.
- 데이터 마이그레이션: 스키마가 변경되면서 기존에 쌓여있던 데이터를 새로운 규칙에 맞게 일괄 UPDATE 해야 할 때.
- 안전한 스키마 패치: 이미 운영 중인 테이블 구조를 건드리지 않으면서, 새로운 컬럼과 기본값(Default)을 안전하게 밀어 넣고 싶을 때.
2. ❓ 잠깐, 그냥 DB 툴(DBeaver, DataGrip)로 접속해서 직접 SQL을 치면 안 되나요?
코드를 보다 보면 이런 근본적인 의문이 들 수 있습니다. "어차피 SQL을 칠 거면, 굳이 자바 코드로 복잡하게 안 짜고 그냥 DB에 직접 접속해서 ALTER TABLE 쿼리 한 번 쓱 날리면 끝나는 거 아닌가?"
물론 로컬 혼자 개발할 때는 그게 훨씬 빠릅니다. 하지만 실무(팀 프로젝트, 상용 서비스)에서는 반드시 코드를 통해 DB를 제어해야 하는 4가지 치명적인 이유가 있습니다.
- 자동화와 재현성 (어디서든 똑같이!) 로컬 개발 환경(Local), 테스트 환경(Dev), 운영 환경(Prod) 등 서버는 여러 개입니다. 만약 수동으로 쿼리를 친다면, 새로운 환경에 서버를 띄울 때마다 개발자가 직접 DB에 들어가서 쿼리를 순서대로 똑같이 쳐줘야 합니다. 자바 코드에 넣어두면 서버를 켜기만 해도 어느 환경이든 100% 동일한 DB 구조가 세팅됩니다.
- 버전 관리 (Git 히스토리 추적) 직접 DB에서 쿼리를 날리면 '누가, 언제, 왜 이 컬럼을 추가했는지' 기록이 남지 않습니다. 하지만 자바 파일에 SQL을 적어두면 Git을 통해 스키마 변경 역사(History)가 코드와 함께 영구적으로 보존됩니다.
- 휴먼 에러(Human Error) 방지 "운영 서버 DB에 컬럼 추가하는 거 깜빡했다!" ➡️ 바로 서버 에러와 서비스 장애로 이어집니다. 사람의 기억력에 의존하는 수동 작업은 언젠가 반드시 사고를 냅니다. 코드에 맡기면 까먹을 일이 없습니다.
- 애플리케이션 코드와의 강력한 동기화 백엔드 코드(Entity)가 변경되었다면, DB 스키마도 그에 맞게 변경되어야 합니다. 이 두 가지가 동시에 배포되고 기동되는 시점(Application Start)에 맞춰 동기화를 강제함으로써, "코드는 새 버전인데 DB는 옛날 버전이라서 발생하는 에러"를 원천 차단할 수 있습니다.
3. 어떻게 동작하는 걸까요? (핵심 어노테이션 분석)
코드를 보면 스프링의 강력한 기능들이 조합되어 있습니다.
@Configuration
@RequiredArgsConstructor
public class SchemaCompatibilityConfig {
private final JdbcTemplate jdbcTemplate;
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
ApplicationRunner scheduleBoardStatusConstraintUpdater() {
return args -> {
// SQL 실행부
};
}
}
- @Configuration & @Bean: 스프링 구동 시 이 클래스를 설정 파일로 인식하고 빈으로 등록합니다.
- ApplicationRunner: 스프링 부트 애플리케이션이 완전히 기동된 직후, 딱 한 번 자동으로 실행되는 콜백 인터페이스입니다.
- @Order(Ordered.HIGHEST_PRECEDENCE): 여기가 핵심입니다. 애플리케이션 기동 시 실행될 수많은 작업들 중에서 "가장 먼저(1순위로) 이 작업을 실행하라"는 뜻입니다. DB 뼈대가 완벽히 갖춰져야 이후에 실행될 JPA 로직이 에러 없이 DB에 접근할 수 있기 때문입니다.
- JdbcTemplate: JPA(Hibernate)를 거치지 않고, 빠르고 직접적으로 날것의 SQL을 실행하는 도구입니다.
4. 실전 스키마 패치 작성법 (주요 패턴 4가지)
이 설정 파일 내부에서는 다양한 PostgreSQL 문법을 활용해 스키마를 빚어냅니다. 실무에서 가장 많이 쓰이는 4가지 패턴입니다.
💡 패턴 1: 테이블이 없을 때만 생성하기 (CREATE TABLE IF NOT EXISTS)
초기 세팅이나 완전히 새로운 기능을 위한 테이블을 만들 때 사용합니다.
jdbcTemplate.execute("""
CREATE TABLE IF NOT EXISTS nickname_words (
id BIGSERIAL PRIMARY KEY,
type VARCHAR(30) NOT NULL,
value VARCHAR(50) NOT NULL
)
""");
⚠️ 주의사항: 이미 테이블이 존재한다면 이 쿼리는 통째로 무시됩니다. 따라서 기존 테이블에 '새로운 컬럼'을 추가하고 싶을 때 여기에 컬럼을 슬쩍 적어 넣으면 절대 반영되지 않습니다. 이때는 다음 패턴을 사용해야 합니다.
💡 패턴 2: 기존 테이블에 안전하게 컬럼 추가하기 (ALTER TABLE)
운영 중인 서비스에서 가장 많이 쓰이는 패턴입니다. 기존 테이블이 존재할 때만, 그리고 해당 컬럼이 없을 때만 안전하게 컬럼을 추가합니다.
jdbcTemplate.execute("""
ALTER TABLE IF EXISTS users
ADD COLUMN IF NOT EXISTS referral_code VARCHAR(20)
""");
💡 패턴 3: 복잡한 제약조건 갱신하기 (DO $$ BEGIN ... END $$;)
PostgreSQL의 익명 코드 블록(DO)을 사용하여 프로그래밍하듯 스키마를 다루는 고급 기법입니다. "기존에 잘못된 제약조건이 있으면 삭제하고, 새로 만들어라" 같은 분기 처리가 가능합니다.
jdbcTemplate.execute("""
DO $$
BEGIN
-- 1. 테이블이 존재하는지 먼저 확인
IF to_regclass('match_record_events') IS NULL THEN
RETURN;
END IF;
-- 2. 해당 제약조건이 존재하지 않을 때만 생성
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'uk_nickname_words_type_value'
) THEN
ALTER TABLE nickname_words
ADD CONSTRAINT uk_nickname_words_type_value UNIQUE (type, value);
END IF;
END
$$;
""");
💡 패턴 4: 데이터 마이그레이션 (UPDATE)
JPA 자동 생성 기능이 절대 해주지 못하는 영역입니다. 스키마 변경과 함께 기존 데이터를 보정합니다.
jdbcTemplate.execute("""
UPDATE transfer_market_posts p
SET city_code = r.code
FROM region r
WHERE p.city_code = r.name
AND r.parent_code IS NULL
""");
지역 이름(name)으로 저장되어 있던 과거 데이터를, 새롭게 바뀐 지역 코드(code) 기준으로 일괄 변환해 주는 훌륭한 데이터 마이그레이션 쿼리입니다.
5. 마무리하며: Flyway / Liquibase 와의 비교
물론 실무 환경이 거대해지고 팀원이 많아지면, 이렇게 자바 파일 하나에 모든 SQL을 몰아넣는 것은 관리의 한계(버전 관리, 히스토리 추적, 롤백 등)가 옵니다. 그럴 때는 Flyway나 Liquibase 같은 전문적인 데이터베이스 마이그레이션 형상 관리 도구를 도입하는 것이 정석입니다.
하지만, 빠르게 이터레이션(반복 개발)을 도는 초기 스타트업 환경이나, 외부 툴의 복잡한 설정 없이 스프링 부트 내장 기능만으로 확실하고 가벼운 DB 동기화를 보장하고 싶을 때, SchemaCompatibilityConfig 와 같은 ApplicationRunner + JdbcTemplate 조합은 매우 직관적이고 훌륭한 해결책이 되어 줍니다.
'백엔드 > 🍃 SpringBoot' 카테고리의 다른 글
| [Spring JPA] "SELECT r FROM Entity r" 도대체 무슨 뜻일까? (JPQL 완벽 이해하기) (0) | 2026.09.19 |
|---|---|
| [iOS/Web] 앱 환경에서 소셜 로그인(OAuth) 연동 시 발생하는 리다이렉트 문제와 해결법 (0) | 2026.06.05 |
| boolean 필드 매핑 안될 때 (isTermsAgreed가 false만 나오는 이유) (0) | 2026.03.20 |
| [JPA] @OneToMany, @ManyToOne 초간단 정리 (0) | 2026.03.03 |
| 배포시 spring-boot-devtools 비활성화 (1) | 2025.06.13 |