Spring

Spring Boot - JPA 스키마, 데이터 초기화하기 (SQL script -> ddl-auto)

Cold Bean 2024. 11. 13. 23:35
728x90

개요

이전에 작업했던 프로젝트를 Mybatis에서 JPA, Querydsl로 마이그레이션하는 작업을 하고 있다.

기존 초기화를 script(schema.sql(테이블 생성), data.sql(더미데이터 생성)) 파일을 사용하다가 JPA를 사용하게 되면서 ddl-auto를 사용해 Table 생성하기로 했다.

DDL이란?
DDL(Data Definition Language)은 SQL의 하위 집합으로 데이터베이스의 구조와 테이블, 뷰, 인덱스 프로시저와 같은 개체를 정의하는데 사용된다. DDL문은 데이터 베이스 개체를 생성, 변경 및 삭제 하는데 사용된다. (CREATE, ALTER, DROP, TRUNCATE, RENAME)

 

데이터 초기화 설정

SQL script (기존) 

기존에는 script 기반으로 데이터를 초기화했다.

src/main/resources 디렉토리에 schema.sql, data.sql을 파일을 생성해두면 애플리케이션 시작 시점에 schema.sql, data.sql 파일이 차례대로 실행된다. schema.sql을 통해서 스키마를 생성하고 data.sql을 통해 데이터를 생성한다.

아래는 script로 데이터 초기화하기 위한 application.yml 설정이다. 

# 기존
spring:
  sql:
    init:
      mode: always
      schema-locations: classpath:schema.sql
      data-locations: classpath:data.sql

Spring Boot는 내장된 DataSource의 스키마를 자동으로 생성한다.이 동작을 제어하기 위해서 spring.sql.init.mode 속성을 사용할 수 있다.

  • always: 항상 데이터베이스를 초기화
  • embedded: 임베디드 데이터베이스를 사용중이면 항상 초기화. mode 속성의 기본값이다.
  • never: 데이터베이스를 초기화하지 않는다.

이 속성은 Spring Boot 2.5.0부터 도입되었고 이전 버전에서 해당 기능을 사용하고싶다면 spring.datasource.initialization-mode를 사용해야 한다.

 

SQL script + DDL Generation (수정)

나는 schema.sql 스크립트를 사용하지 않고 ddl-auto로 테이블을 생성한 뒤, data.sql로 더미데이터를 생성하고 싶었다.

schema.sql 파일을 삭제하는 방법도 있지만 아직 마이그레이션이 완료되지 않았기 때문에 schema-locations 옵션을 주석처리 후 schema.sql 파일명을 변경해서 실행하지 않도록 했다. (schema.sql -> no-schema.sql)

# ddl-auto 적용
spring:
  sql:
    init:
      mode: always
      # schema-locations: classpath:no-schema.sql # schema.sql 파일명 수정 -> no-schema.sql
      data-locations: classpath:data.sql

  # JPA 설정 정보
  jpa:
    hibernate:
      ddl-auto: create  # create, create-drop, update, validate, none
    defer-datasource-initialization: true
  • ddl-auto:  Spring은 Hibernate가 DDL 생성에 사용하는 spring.jpa.hibernate.ddl-auto 속성을 제공한다. 속성 값으로는 create, update, create-drop, validate, none이 있다.
    • create: 기존 테이블을 삭제한 후 새로운 테이블을 생성한다. 
    • update: 객체 모델을 기존 스키마와 비교한 후, 수정된 사항에 따라 스키마를 업데이트한다. 사용되지 않는 테이블이나 컬럼이 있더라도 삭제하지 않는다.
    • create-drop: 모든 작업이 완료된 후 데이터베이스를 삭제한다. 일반적으로 단위 테스트에서 사용된다.
    • validate: 테이블과 열이 존재하는지 여부만 검증한다. 만약 검증에 실패하면 예외를 발생시킨다.
    • none: DDL 생성을 하지 않는다.
  • defer-datasource-initialization: 기본적으로 data.sql 스크립트는 Hibernate가 초기화되기 전에 실행된다. 즉, 테이블이 생성되기 전에 더미데이터를 생성하려고 하기 때문에 오류가 발생한다. 이를 해결하기 위해서는 더미데이터 생성을 ddl-auto가 실행된 후에 더미데이터가 생성되도록 해야 한다. defer-datasource-initialization 속성 값을 true로 설정하면 ddl 생성 후 data.sql 스크립트를 통해 더미데이터를 생성해준다.

 

문제 발생

ddl-auto 설정 이후 문제가 발생했다. TASK_EVALUATION 테이블만 생성되지 않았던 것.

없다...

 

콘솔을 확인해보면 task_evaluation 테이블을 생성했다는 걸 확인할 수 있다.

Hibernate: 
    
    create table task_evaluation (
       charge_team_id bigint not null,
        task_id bigint not null,
        ceo_point integer,
        cond_ceo varchar(10),
        cond_officer varchar(10),
        level_ceo varchar(10),
        level_officer varchar(10),
        note varchar(500),
        officer_point integer,
        state varchar(1) default N,
        task_gb varchar(10),
        total_point double,
        weight double,
        ins_date timestamp,
        ins_ip varchar(15),
        ins_user varchar(50),
        mod_date timestamp,
        mod_ip varchar(15),
        mod_user varchar(50),
        primary key (charge_team_id, task_id)
    )
    
    alter table task_evaluation 
       add constraint FK7xru9ioepwekfal3hybwk83l0 
       foreign key (charge_team_id) 
       references organization
       
    alter table task_evaluation 
       add constraint FK8vktactaa3ko3hu0yur1sbsmd 
       foreign key (task_id) 
       references task

 

원인

현재 프로젝트에서는 내장 H2를 사용하고 있었다. H2는 데이터베이스 작업 중 발생한 디버그 로그를 trace.db에 기록해서 자동으로 생성한다. (H2 파일을 저장한 위치에 생성된다.) race.db를 보고 문제를 확인해 볼 수 있었다.

jdbc[3]: exception
org.h2.jdbc.JdbcSQLSyntaxErrorException: Column "N" not found; SQL statement:

    create table public.task_evaluation (
       charge_team_id bigint not null,
        task_id bigint not null,
        ins_date timestamp,
        ins_ip varchar(15),
        ins_user varchar(50),
        mod_date timestamp,
        mod_ip varchar(15),
        mod_user varchar(50),
        ceo_point integer,
        cond_ceo varchar(10),
        cond_officer varchar(10),
        level_ceo varchar(10),
        level_officer varchar(10),
        note varchar(500),
        officer_point integer,
        state varchar(1) default N,
        task_gb varchar(10),
        total_point double,
        weight double,
        primary key (charge_team_id, task_id)
    ) [42122-214]
	at org.h2.message.DbException.getJdbcSQLException(DbException.java:502)
	at org.h2.message.DbException.getJdbcSQLException(DbException.java:477)
	at org.h2.message.DbException.get(DbException.java:223)
	at org.h2.message.DbException.get(DbException.java:199)
	at org.h2.expression.ExpressionColumn.getColumnException(ExpressionColumn.java:244)
	at org.h2.expression.ExpressionColumn.optimizeOther(ExpressionColumn.java:226)
	at org.h2.expression.ExpressionColumn.optimize(ExpressionColumn.java:213)
	at org.h2.table.Column.setDefaultExpression(Column.java:249)
	at org.h2.command.Parser.parseColumnForTable(Parser.java:5965)
	at org.h2.command.Parser.parseTableColumnDefinition(Parser.java:9331)
	at org.h2.command.Parser.parseCreateTable(Parser.java:9271)
	at org.h2.command.Parser.parseCreate(Parser.java:6784)
	at org.h2.command.Parser.parsePrepared(Parser.java:763)
	at org.h2.command.Parser.parse(Parser.java:689)
	at org.h2.command.Parser.parse(Parser.java:661)
	at org.h2.command.Parser.prepareCommand(Parser.java:569)
	at org.h2.engine.SessionLocal.prepareLocal(SessionLocal.java:631)
	at org.h2.engine.SessionLocal.prepareCommand(SessionLocal.java:554)
	at org.h2.jdbc.JdbcConnection.prepareCommand(JdbcConnection.java:1116)
	at org.h2.jdbc.JdbcStatement.executeInternal(JdbcStatement.java:237)
	at org.h2.jdbc.JdbcStatement.execute(JdbcStatement.java:223)
	at com.zaxxer.hikari.pool.ProxyStatement.execute(ProxyStatement.java:94)
	at com.zaxxer.hikari.pool.HikariProxyStatement.execute(HikariProxyStatement.java)
	at org.hibernate.tool.schema.internal.exec.GenerationTargetToDatabase.accept(GenerationTargetToDatabase.java:54)
	at org.hibernate.tool.schema.internal.SchemaCreatorImpl.applySqlString(SchemaCreatorImpl.java:458)
	at org.hibernate.tool.schema.internal.SchemaCreatorImpl.applySqlStrings(SchemaCreatorImpl.java:442)
	at org.hibernate.tool.schema.internal.SchemaCreatorImpl.createFromMetadata(SchemaCreatorImpl.java:325)
	at org.hibernate.tool.schema.internal.SchemaCreatorImpl.performCreation(SchemaCreatorImpl.java:169)
	at org.hibernate.tool.schema.internal.SchemaCreatorImpl.doCreation(SchemaCreatorImpl.java:138)
	at org.hibernate.tool.schema.internal.SchemaCreatorImpl.doCreation(SchemaCreatorImpl.java:124)
	at org.hibernate.tool.schema.spi.SchemaManagementToolCoordinator.performDatabaseAction(SchemaManagementToolCoordinator.java:168)
	at org.hibernate.tool.schema.spi.SchemaManagementToolCoordinator.process(SchemaManagementToolCoordinator.java:85)
	at org.hibernate.internal.SessionFactoryImpl.<init>(SessionFactoryImpl.java:335)
	at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:471)
	at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:1498)
	at org.springframework.orm.jpa.vendor.SpringHibernateJpaPersistenceProvider.createContainerEntityManagerFactory(SpringHibernateJpaPersistenceProvider.java:58)
	at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.createNativeEntityManagerFactory(LocalContainerEntityManagerFactoryBean.java:365)
	at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.buildNativeEntityManagerFactory(AbstractEntityManagerFactoryBean.java:409)
	at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.afterPropertiesSet(AbstractEntityManagerFactoryBean.java:396)
	at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.afterPropertiesSet(LocalContainerEntityManagerFactoryBean.java:341)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1863)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1800)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:620)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542)
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335)
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208)
	at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1157)
	at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:911)
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:583)
	at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:147)
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:731)
	at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:408)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:307)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1303)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1292)
	at co.pes.PesApplication.main(PesApplication.java:18)
2024-11-14 14:36:57 jdbc[3]: exception
org.h2.jdbc.JdbcSQLSyntaxErrorException: Table "TASK_EVALUATION" not found; SQL statement:

    alter table public.task_evaluation 
       add constraint FK7xru9ioepwekfal3hybwk83l0 
       foreign key (charge_team_id) 
       references organiz [42102-214]
2024-11-14 14:36:57 jdbc[3]: exception
org.h2.jdbc.JdbcSQLSyntaxErrorException: Table "TASK_EVALUATION" not found; SQL statement:

    alter table public.task_evaluation 
       add constraint FK8vktactaa3ko3hu0yur1sbsmd 
       foreign key (task_id) 
       references [42102-214]

"N" 컬럼을 찾을 수 없다고 한다. 이 테이블에는 N 컬럼이 없는데,,?

스키마를 state 필드 쪽을 보면 답을 알 수 있다. default로 N이 설정되어 있는데, N이 아닌 'N'으로 나와야 한다.

state varchar(1) default N,

 

TaskEvaluation Entity를 확인해보자

@SuperBuilder
@Getter
@EqualsAndHashCode(callSuper = true)
@Entity(name = "TASK_EVALUATION")
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class TaskEvaluationEntity extends BaseEntity {

    ...

    @Column(length = 1)
    @ColumnDefault("N")
    private String state; // 상태 (N: 임시저장 / F: 최종제출)

    ...
}

@ColumnDefault 애너테이션은 DDL을 생성할 때 해당 컬럼의 default 값을 설정하게 해주는 애너테이션이다.

문제는 "N"이라는 값을 넣었을 때 해당 값이 그대로 N으로 스키마에서 사용된다.

 

해결

스키마에 'N'으로 나올 수 있도록 한 번 더 감싸주면 문제없이 스키마가 생성된다.

@SuperBuilder
@Getter
@EqualsAndHashCode(callSuper = true)
@Entity(name = "TASK_EVALUATION")
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class TaskEvaluationEntity extends BaseEntity {

    ...

    @Column(length = 1)
    @ColumnDefault("'N'")
    private String state; // 상태 (N: 임시저장 / F: 최종제출)

    ...
}
Hibernate: 
    
    create table public.task_evaluation (
       charge_team_id bigint not null,
        task_id bigint not null,
        ins_date timestamp,
        ins_ip varchar(15),
        ins_user varchar(50),
        mod_date timestamp,
        mod_ip varchar(15),
        mod_user varchar(50),
        ceo_point integer,
        cond_ceo varchar(10),
        cond_officer varchar(10),
        level_ceo varchar(10),
        level_officer varchar(10),
        note varchar(500),
        officer_point integer,
        state varchar(1) default 'N',
        task_gb varchar(10),
        total_point double,
        weight double,
        primary key (charge_team_id, task_id)
    )

 

TASK_EVALUATION 테이블 생성!
더미데이터도 잘 생성되서 화면에 노출되고 있다.

참조

https://www.baeldung.com/spring-boot-data-sql-and-schema-sql

https://pravusid.kr/java/2018/10/10/spring-database-initialization.html

 

Spring에서 JPA / Hibernate 초기화 전략 · ID PRAVUS

Spring-data-JPA와 DBMS를 연결해서 사용할 때 간편히 개발환경의 변경사항을 적용하여 테스트 할 수 있다. 특히 테스트를 위한 in-memory Database인 H2 Database를 염두에 둔 DB 초기화 전략에서 신경쓸 점을

pravusid.kr

 

728x90