Spring Boot를 사용하여 애플리케이션을 개발할 때, H2 데이터베이스는 빠른 프로토타이핑과 테스트 주도 개발(TDD) 환경 구축에 있어 강력한 도구로 자리매김했습니다. 별도의 설치 없이 의존성 추가만으로 즉시 사용 가능하며, 가볍고 빠르다는 장점 덕분에 수많은 개발자의 사랑을 받고 있습니다. 하지만 이 편리함 이면에는 많은 초심자들이 한 번쯤 마주치는 함정이 존재합니다. 바로 애플리케이션을 재시작할 때마다 데이터가 깨끗이 사라지는, 소위 '데이터 초기화' 현상입니다.
이 현상은 버그가 아니라 H2 데이터베이스의 기본 동작 방식과 Spring Boot의 지능적인 자동 설정(Auto-Configuration)이 결합되어 나타나는 의도된 결과입니다. 이 글에서는 왜 이런 현상이 발생하는지 근본적인 원인을 파헤치고, 단순히 데이터를 유지하는 것을 넘어 개발, 테스트, 운영 각 단계에 맞는 최적의 데이터베이스 관리 전략을 수립하는 방법을 심도 있게 다룹니다. 인-메모리 모드와 파일 기반 모드의 차이부터, JPA의 ddl-auto
옵션 심층 분석, 그리고 프로덕션 수준의 애플리케이션을 위한 모범 사례까지, 여러분의 개발 환경을 한 단계 끌어올릴 영속적인 데이터 관리의 모든 것을 안내합니다.
문제의 본질: 왜 H2 데이터는 재시작할 때마다 사라질까?
"열심히 테스트 데이터를 입력하고 기능을 확인했는데, 서버를 재시작하니 모든 게 사라졌다." 이 허탈한 경험의 원인을 이해하기 위해서는 '인-메모리(In-Memory) 데이터베이스'의 개념부터 짚고 넘어가야 합니다.
인-메모리 데이터베이스는 이름에서 알 수 있듯이, 데이터를 하드 디스크나 SSD 같은 영구 저장 장치가 아닌, 컴퓨터의 주 기억 장치인 RAM에 저장합니다. RAM은 CPU와 직접 통신하며 데이터를 읽고 쓰는 속도가 디스크에 비해 압도적으로 빠르기 때문에, 인-메모리 데이터베이스는 엄청난 성능을 자랑합니다. 하지만 RAM은 전원이 차단되면 저장된 모든 내용이 사라지는 '휘발성' 저장 매체입니다. 따라서 RAM에 데이터를 저장하는 인-메모리 데이터베이스 역시 애플리케이션 프로세스가 종료되면(즉, 서버가 재시작되면) 모든 데이터를 잃게 됩니다.
Spring Boot는 pom.xml
이나 build.gradle
에 H2 의존성(com.h2database:h2
)이 추가되어 있으면, 개발자가 별도의 설정을 하지 않아도 즉시 데이터베이스를 사용할 수 있도록 모든 것을 자동으로 설정합니다. 이 자동 설정 과정에서 Spring Boot는 H2를 가장 간편하고 빠른 '인-메모리 모드'로 실행하도록 기본값을 정합니다. 이것이 바로 우리가 겪는 데이터 초기화 문제의 근본적인 원인입니다. Spring Boot는 개발자의 편의를 위해 빠른 테스트 환경을 기본으로 제공하는 것이며, 우리는 필요에 따라 이 기본 동작을 변경하여 데이터 영속성을 확보해야 합니다.
해결의 열쇠: H2 데이터베이스의 동작 모드 이해하기
H2는 단순히 인-메모리 데이터베이스로만 동작하지 않습니다. 오히려 다양한 환경과 요구사항에 맞춰 유연하게 작동할 수 있는 여러 모드를 지원하는 것이 H2의 진정한 강점입니다. 데이터 영속성 문제를 해결하기 위해서는 이 동작 모드들을 정확히 이해하고 상황에 맞게 선택해야 합니다.
인-메모리(In-Memory) 모드: 속도와 격리의 미학
앞서 설명했듯이, 이 모드는 모든 데이터를 RAM에 저장합니다. JDBC URL 설정은 보통 다음과 같은 형식을 따릅니다.
jdbc:h2:mem:testdb
여기서 mem
은 인-메모리 모드를 의미하며, testdb
는 생성될 데이터베이스의 논리적인 이름입니다. 이 모드의 가장 큰 장점은 다음과 같습니다.
- 최고의 속도: 디스크 I/O가 전혀 없으므로 데이터 CRUD(생성, 읽기, 수정, 삭제) 작업이 매우 빠릅니다.
- 완벽한 격리: 애플리케이션 실행 시마다 완전히 새로운, 깨끗한 데이터베이스 환경에서 시작합니다. 이는 특히 단위 테스트나 통합 테스트를 실행할 때 매우 유용합니다. 이전 테스트의 데이터가 다음 테스트에 영향을 미치는 '테스트 오염'을 원천적으로 차단할 수 있습니다.
물론 가장 큰 단점은 앞서 언급한 '휘발성'입니다. 개발 과정에서 입력한 데이터를 유지하며 기능을 점진적으로 구현하고 테스트하고 싶을 때는 이 모드가 적합하지 않습니다.
파일 기반(File-based) 모드: 영속성을 향한 첫걸음
데이터를 재부팅 후에도 유지하고 싶다면 파일 기반 모드를 사용해야 합니다. 이 모드는 데이터를 RAM이 아닌 디스크 상의 실제 파일(.mv.db)에 저장합니다. 따라서 애플리케이션이 종료되거나 시스템이 재부팅되어도 데이터는 안전하게 보존됩니다. 이것이 바로 우리가 원하는 '영속성'입니다.
파일 기반 모드를 사용하기 위해서는 application.properties
(또는 .yml
) 파일에서 데이터 소스 URL을 다음과 같이 변경해야 합니다.
# application.properties 예시
# 프로젝트 루트 디렉토리에 'data' 폴더를 만들고 그 안에 h2db라는 이름의 데이터베이스 파일을 생성
spring.datasource.url=jdbc:h2:file:./data/h2db
# 사용자 홈 디렉토리(~)에 h2db 파일을 생성
# spring.datasource.url=jdbc:h2:file:~/h2db
# 절대 경로 지정
# spring.datasource.url=jdbc:h2:file:C:/data/h2db
URL에서 mem
대신 file
을 사용하고, 그 뒤에 데이터베이스 파일이 저장될 경로를 지정합니다. ./data/h2db
와 같이 상대 경로를 사용하면 프로젝트에 종속적인 데이터베이스 파일을 만들어 관리하기 편리합니다. 처음 이 URL로 애플리케이션을 실행하면, H2는 지정된 경로에 h2db.mv.db
라는 실제 데이터베이스 파일을 생성합니다. 이후의 모든 데이터 변경 사항은 이 파일에 기록됩니다.
JPA와 Hibernate DDL 자동 생성: spring.jpa.hibernate.ddl-auto
깊이 파헤치기
H2를 파일 모드로 변경하여 데이터 저장 위치를 영구적으로 만든 것은 문제 해결의 절반에 불과합니다. 이제 우리는 JPA 구현체인 Hibernate가 데이터베이스 스키마(테이블 구조)를 어떻게 다룰지 결정해야 합니다. 이 역할을 하는 것이 바로 spring.jpa.hibernate.ddl-auto
속성입니다. 이 속성을 제대로 이해하지 못하면 파일 모드를 사용하더라도 데이터가 초기화되는 현상을 다시 겪을 수 있습니다.
DDL(Data Definition Language)이란?
DDL은 데이터 정의 언어로, 데이터베이스의 구조를 정의하고 관리하는 SQL의 한 종류입니다. 대표적인 DDL 명령어는 다음과 같습니다.
CREATE
: 새로운 데이터베이스, 테이블, 뷰 등을 생성합니다.ALTER
: 기존 테이블의 구조(컬럼 추가/삭제/변경 등)를 수정합니다.DROP
: 데이터베이스, 테이블 등을 완전히 삭제합니다.
ddl-auto
속성은 애플리케이션이 시작되거나 종료될 때, Hibernate가 이러한 DDL 명령어를 자동으로 실행할지, 실행한다면 어떻게 실행할지를 제어하는 강력한 기능입니다.
ddl-auto
속성의 5가지 옵션 완벽 분석
이 옵션들은 각각 뚜렷한 목적을 가지므로, 현재 개발 단계와 환경에 맞는 것을 신중하게 선택해야 합니다.
create
:- 동작: 애플리케이션 시작 시점에 기존 스키마(테이블)가 있다면 모두 삭제(DROP)하고, JPA 엔티티(@Entity 어노테이션이 붙은 클래스) 정의를 기반으로 새로운 스키마를 생성(CREATE)합니다.
- 특징: 항상 깨끗한 상태에서 시작합니다. 파일 모드와 함께 사용하더라도 매번 테이블을 새로 만들기 때문에, 기존에 저장된 데이터는 테이블이 삭제되면서 모두 사라집니다. 데이터 초기화의 또 다른 주범이 될 수 있습니다.
- 용도: 개발 초기 단계에서 스키마 구조가 확정되지 않았을 때, 또는 스키마 자동 생성을 확인하고 싶을 때 유용합니다.
create-drop
:- 동작:
create
와 마찬가지로 애플리케이션 시작 시 스키마를 새로 생성합니다. 그리고 애플리케이션이 정상적으로 종료되는 시점(세션 팩토리가 닫힐 때)에 생성했던 스키마를 모두 삭제(DROP)합니다. - 특징: 테스트 실행에 최적화된 옵션입니다. 각 테스트 세션이 실행될 때마다 독립적인 스키마를 가졌다가, 테스트가 끝나면 깨끗하게 정리되므로 테스트 간의 격리를 보장합니다.
- 용도: 통합 테스트(Integration Test) 환경.
- 동작:
update
:- 동작: 애플리케이션 시작 시, 데이터베이스 스키마와 JPA 엔티티 모델을 비교하여 변경된 부분만 반영(ALTER)합니다. 기존에 없던 테이블이나 컬럼은 추가해주고, 삭제된 엔티티나 필드에 해당하는 테이블/컬럼은 그대로 둡니다.
- 특징: 기존 데이터와 스키마를 최대한 보존하면서 개발을 이어나갈 수 있어 편리합니다. 많은 개발자가 개발 환경에서 이 옵션을 선호합니다.
- 주의!:
update
는 만능이 아닙니다. 컬럼 이름 변경, 타입 변경, 제약 조건 변경 등 복잡한 스키마 마이그레이션은 제대로 처리하지 못하거나 예기치 않은 문제를 일으킬 수 있습니다. 운영 환경에서는 절대 사용해서는 안 됩니다. - 용도: 데이터 유지가 필요한 로컬 개발 환경.
validate
:- 동작: 애플리케이션 시작 시, 데이터베이스 스키마와 JPA 엔티티 모델이 정확히 일치하는지 검증만 합니다. 스키마를 수정하지는 않습니다. 만약 불일치가 발견되면 애플리케이션은 예외를 발생시키며 시작되지 않습니다.
- 특징: 데이터베이스의 안전성을 보장합니다. 코드(엔티티)와 실제 DB 스키마 간의 불일치로 인해 발생할 수 있는 런타임 오류를 사전에 방지할 수 있습니다.
- 용도: 스키마가 안정화된 개발 후반 단계, 스테이징 또는 프로덕션 환경.
none
:- 동작: 아무것도 하지 않습니다. DDL 생성과 관련된 Hibernate의 기능을 완전히 끕니다.
- 특징: 데이터베이스 스키마 관리를 애플리케이션 외부에서(예: DBA가 직접 관리, Flyway나 Liquibase 같은 스키마 마이그레이션 도구 사용) 완벽하게 통제하는 환경을 위한 옵션입니다.
- 용도: 엄격한 관리가 필요한 프로덕션 환경의 정석.
환경별 권장 전략
- 로컬 개발 환경: 데이터 유지가 필요하다면 `update`. 스키마 변경이 잦고 초기화해도 상관없다면 `create`.
- 테스트 환경: `create-drop` 또는 `create`.
- 운영(Production) 환경: `validate` 또는 `none`. 절대로 `create`나 `update`를 사용하지 마십시오.
실전 적용: 영속적인 개발 환경 구축하기
이제 이론을 바탕으로 실제 Spring Boot 프로젝트에 영속적인 H2 개발 환경을 구축하는 구체적인 단계를 살펴보겠습니다.
1단계: application.properties
또는 application.yml
설정
프로젝트의 src/main/resources
디렉토리에 있는 설정 파일을 열어 아래와 같이 수정합니다.
application.properties
사용 시:
# H2 데이터베이스 설정
# 1. DataSource URL을 파일 모드로 변경하여 데이터 영속성 확보
# 프로젝트 루트에 data 폴더가 생성되고, 그 안에 devdb.mv.db 파일로 데이터가 저장됨
spring.datasource.url=jdbc:h2:file:./data/devdb
# 2. H2 DB에 접속할 드라이버 클래스
spring.datasource.driver-class-name=org.h2.Driver
# 3. H2 DB 접속 계정 정보
spring.datasource.username=sa
spring.datasource.password=
# JPA 및 Hibernate 설정
# 4. 개발 환경에서는 update 옵션을 사용하여 엔티티 변경분을 DB에 반영
spring.jpa.hibernate.ddl-auto=update
# SQL 쿼리를 로깅하여 디버깅에 활용
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
application.yml
사용 시:
spring:
datasource:
# 1. 파일 모드 URL
url: jdbc:h2:file:./data/devdb
# 2. 드라이버 클래스
driver-class-name: org.h2.Driver
# 3. 접속 계정
username: sa
password:
jpa:
hibernate:
# 4. DDL 자동 생성 전략
ddl-auto: update
properties:
hibernate:
show_sql: true
format_sql: true
이제 애플리케이션을 실행하면 프로젝트 루트 디렉토리 아래에 data
폴더가 생기고, 그 안에 devdb.mv.db
파일이 생성되는 것을 확인할 수 있습니다. 이제부터 여러분이 애플리케이션을 통해 저장하는 모든 데이터는 이 파일에 안전하게 보관됩니다.
2단계: H2 콘솔 활성화 및 데이터 확인
데이터가 파일에 제대로 저장되고 있는지, 스키마는 어떻게 생성되었는지 직접 눈으로 확인하고 싶을 때 H2 콘솔은 매우 유용한 도구입니다. 설정 파일에 다음 한 줄을 추가하여 활성화할 수 있습니다.
# application.properties
spring.h2.console.enabled=true
# (선택) 콘솔 접속 경로 변경 (기본값: /h2-console)
# spring.h2.console.path=/my-h2-console
애플리케이션을 실행한 후, 웹 브라우저에서 http://localhost:8080/h2-console
주소로 접속하면 H2 데이터베이스 로그인 화면을 볼 수 있습니다.
🚨 매우 중요한 주의사항: H2 콘솔에서 데이터를 확인하려면 로그인 창의 'JDBC URL' 필드 값이 application.properties
에 설정한 spring.datasource.url
값과 정확하게 일치해야 합니다.
만약 콘솔의 JDBC URL이 jdbc:h2:mem:testdb
와 같은 기본값으로 되어 있다면, 여러분은 파일에 저장된 데이터가 아닌 새로운 인-메모리 데이터베이스에 접속하게 됩니다. 이 경우 당연히 테이블과 데이터가 아무것도 보이지 않아 "데이터가 저장되지 않았다"고 오해할 수 있습니다. 반드시 jdbc:h2:file:./data/devdb
와 같이 여러분이 설정한 파일 경로 URL을 입력하고 연결해야 영속화된 데이터를 확인할 수 있습니다.
전문가를 위한 고급 전략 및 모범 사례
단순히 데이터를 유지하는 것을 넘어, 더 안정적이고 확장 가능한 개발 프로세스를 원한다면 다음과 같은 고급 전략들을 도입하는 것을 고려해 보세요.
데이터 초기화 스크립트 활용: schema.sql
과 data.sql
ddl-auto=create
는 편리하지만, Hibernate가 생성하는 DDL을 우리가 직접 제어할 수 없습니다. 더 정교한 스키마 정의나 초기 데이터 삽입이 필요할 때 schema.sql
과 data.sql
파일을 사용할 수 있습니다.
src/main/resources/schema.sql
: DDL(CREATE TABLE, ALTER TABLE 등) 구문을 작성합니다. 애플리케이션 시작 시 이 파일의 SQL이 실행되어 스키마를 구성합니다.src/main/resources/data.sql
: DML(INSERT, UPDATE 등) 구문을 작성합니다. 스키마가 생성된 후, 테스트에 필요한 초기 데이터를 삽입하는 데 주로 사용됩니다.
주의: Hibernate의 ddl-auto
와 schema.sql
이 동시에 스키마 생성 역할을 하려고 하면 충돌이 발생할 수 있습니다. JPA 2.1 이상을 사용하는 Spring Boot 2.x 환경에서는 spring.jpa.defer-datasource-initialization=true
속성을 설정하여 Hibernate 스키마 생성이 먼저 완료된 후, data.sql
이 실행되도록 순서를 제어할 수 있습니다. 또는, ddl-auto=none
으로 설정하고 schema.sql
로 스키마 관리를 완전히 위임하는 방법도 있습니다.
개발, 테스트, 운영 환경 분리: Spring Profile의 힘
프로페셔널한 개발 환경에서는 단일 설정 파일이 아닌, 각 환경(Profile)에 맞는 여러 설정 파일을 사용합니다. 이를 통해 각 환경의 목적에 맞는 최적의 데이터베이스 전략을 적용할 수 있습니다.
application-dev.yml
(개발 프로파일): 로컬 개발용 설정. H2 파일 모드와ddl-auto=update
사용.application-test.yml
(테스트 프로파일): 자동화된 테스트용 설정. H2 인-메모리 모드와ddl-auto=create-drop
사용.application-prod.yml
(운영 프로파일): 실제 서비스용 설정. PostgreSQL이나 MySQL 같은 상용 DB 연결 정보와ddl-auto=validate
또는none
사용.
애플리케이션 실행 시 --spring.profiles.active=dev
와 같은 JVM 옵션을 통해 활성화할 프로파일을 지정할 수 있습니다.
# application-test.yml 예시
spring:
datasource:
# 테스트는 격리된 인-메모리 DB 사용
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
username: sa
password:
jpa:
hibernate:
# 테스트 시작시 생성, 종료시 삭제
ddl-auto: create-drop
운영 환경을 위한 제언: Flyway, Liquibase 도입
다시 한번 강조하지만, ddl-auto=update
는 개발 환경의 편의성을 위한 도구이며 운영 환경에서 사용하는 것은 매우 위험합니다. 예상치 못한 데이터 손실이나 스키마 불일치를 야기할 수 있습니다.
운영 환경에서는 Flyway나 Liquibase와 같은 '데이터베이스 마이그레이션' 도구를 사용하는 것이 표준적인 모범 사례입니다. 이 도구들은 SQL 스크립트에 버전을 부여하여, 애플리케이션 버전과 데이터베이스 스키마 버전을 체계적으로 관리하고 형상 관리 시스템(Git 등)을 통해 변경 이력을 추적할 수 있게 해줍니다. 이를 통해 안전하고 예측 가능한 방식으로 데이터베이스 스키마를 배포하고 롤백할 수 있습니다.
흔히 발생하는 문제와 해결 방안 (Troubleshooting)
- 문제: H2를 파일 모드로 설정했는데도 재시작 시 데이터가 초기화됩니다.
- 원인:
spring.jpa.hibernate.ddl-auto
속성이create
로 설정되어 있을 가능성이 높습니다. 애플리케이션 시작 시마다 테이블을 새로 만들기 때문입니다. - 해결:
ddl-auto
값을update
,validate
, 또는none
으로 변경하세요.
- 원인:
- 문제:
/h2-console
에 접속했지만 테이블이 하나도 보이지 않습니다.- 원인: 콘솔 로그인 화면의 JDBC URL이 실제 애플리케이션에서 사용하는 URL(
jdbc:h2:file:...
)과 일치하지 않습니다. 기본값인 인-메모리 URL(jdbc:h2:mem:...
)로 접속했을 가능성이 큽니다. - 해결: 로그인 화면의 JDBC URL을
application.properties
(또는.yml
)에 정의된 값으로 정확하게 복사하여 붙여넣으세요.
- 원인: 콘솔 로그인 화면의 JDBC URL이 실제 애플리케이션에서 사용하는 URL(
- 문제:
ddl-auto=validate
로 설정하니 애플리케이션이 시작되지 않고 오류가 발생합니다.- 원인: 현재 데이터베이스 스키마와 코드 상의 JPA 엔티티 정의가 일치하지 않기 때문입니다. 예를 들어, 엔티티에 새로운 필드를 추가했지만 DB 테이블에는 해당 컬럼이 없는 경우입니다.
- 해결: 스키마와 엔티티가 불일치하는 부분을 오류 로그를 통해 찾아내고, SQL 쿼리(ALTER TABLE 등)를 직접 실행하여 스키마를 엔티티에 맞게 수정해주세요. 또는 잠시
ddl-auto=update
로 변경하여 실행한 후 다시validate
로 원복하는 방법도 있습니다. (단,update
의 한계를 인지하고 사용해야 합니다.)
결론: 목적에 맞는 데이터베이스 전략 수립의 중요성
Spring Boot 애플리케이션 재시작 시 H2 데이터가 초기화되는 현상은 해결하기 어려운 버그가 아니라, H2의 '인-메모리 모드'라는 강력한 기능이 기본값으로 동작하기 때문입니다. 우리는 이 동작을 이해하고, JDBC URL을 수정하여 '파일 기반 모드'로 전환함으로써 간단히 데이터 영속성을 확보할 수 있습니다.
하지만 진정한 핵심은 단순히 데이터를 유지하는 것을 넘어, '어떤 환경에서, 어떤 목적으로 데이터베이스를 사용하는가?'를 스스로에게 묻는 것입니다. 빠른 피드백과 격리가 중요한 테스트 환경에서는 인-메모리 모드와 create-drop
전략이 최적일 수 있습니다. 점진적인 기능 개발과 데이터 유지가 필요한 로컬 개발 환경에서는 파일 모드와 update
전략이 효율적입니다. 그리고 안정성과 데이터 무결성이 최우선인 운영 환경에서는 상용 데이터베이스와 validate
또는 none
전략, 그리고 Flyway와 같은 마이그레이션 도구를 통한 엄격한 스키마 관리가 필수적입니다.
오늘 다룬 내용들을 바탕으로 여러분의 프로젝트에 맞는 최적의 데이터 관리 전략을 수립하여, 더 안정적이고 생산적인 개발 여정을 이어나가시길 바랍니다.
0 개의 댓글:
Post a Comment