Spring/JPA, Hibernate

Spring Boot + JPA/Hibernate + Liquibase Project 구성 (feat. PostgreSQL)

갈색왜성 2019. 5. 24. 14:18
반응형

나이가 들어가면서 기억력이 떨어지는 것이 느껴져서 Spring 프로젝트의 많은 Property들 중 자주 사용하는 것 위주로 내용을 한 번 정리하고자 포스팅을 쓴다

개발 환경

  • OpenJDK v1.8.0_212
  • Spring Boot 2.1.4 (Hibernate 5.3.9, HikariCP 3.2)
  • liquibase 3.6.3
  • lombok-1.18.16
  • PostgreSQL 11 (Postgresql JDBC Driver : postgresql-42.2.5)
  • Gradle

build.gradle

Spring Initializer를 통해 jpa, liquibase, postgresql을 Dependencies로 설정해서 Gardle Project를 생성했다. (이 화면에는 없지만 lombok도 dependancy에 추가되어 있다.

Pic 1. Spring Initialize 페이지에서 Project 구성

만들어진 Project Folder에서 build.gradle를 확인해보면 아래와 같이 Dependency가 설정된다.

dependencies {
	...
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.liquibase:liquibase-core'
    ...
	runtimeOnly 'org.postgresql:postgresql'
	...
}

application.yml

Spring Boot 프로젝트는 기본 환경 설정 파일로 application.properties를 포함하게 된다. 여기에서 설정할 수 있는 내용은 Spring Site에 자세히 나와 있고, 이중에서 JPA/Hibernate, Data-source, Liquibase에 관련된 설정은 아래와 같다. 설정할 수 있는 사항은 방대(spring.jpa.properties나 spring.datasource.hikari.* 같은 경우에는 하위에 별도의 property 설정 항목들이 존재)하지만 모두 설정할 필요는 없고 대부분 Default Value를 사용하면 된다.

# JPA (JpaBaseConfiguration, HibernateJpaAutoConfiguration)
spring.data.jpa.repositories.bootstrap-mode=default # Bootstrap mode for JPA repositories.
spring.data.jpa.repositories.enabled=true # Whether to enable JPA repositories.
spring.jpa.database= # Target database to operate on, auto-detected by default. Can be alternatively set using the "databasePlatform" property.
spring.jpa.database-platform= # Name of the target database to operate on, auto-detected by default. Can be alternatively set using the "Database" enum.
spring.jpa.generate-ddl=false # Whether to initialize the schema on startup.
spring.jpa.hibernate.ddl-auto= # DDL mode. This is actually a shortcut for the "hibernate.hbm2ddl.auto" property. Defaults to "create-drop" when using an embedded database and no schema manager was detected. Otherwise, defaults to "none".
spring.jpa.hibernate.naming.implicit-strategy= # Fully qualified name of the implicit naming strategy.
spring.jpa.hibernate.naming.physical-strategy= # Fully qualified name of the physical naming strategy.
spring.jpa.hibernate.use-new-id-generator-mappings= # Whether to use Hibernate's newer IdentifierGenerator for AUTO, TABLE and SEQUENCE.
spring.jpa.mapping-resources= # Mapping resources (equivalent to "mapping-file" entries in persistence.xml).
spring.jpa.open-in-view=true # Register OpenEntityManagerInViewInterceptor. Binds a JPA EntityManager to the thread for the entire processing of the request.
spring.jpa.properties.*= # Additional native properties to set on the JPA provider.
spring.jpa.show-sql=false # Whether to enable logging of SQL statements.

# DATASOURCE (DataSourceAutoConfiguration & DataSourceProperties)
spring.datasource.continue-on-error=false # Whether to stop if an error occurs while initializing the database.
spring.datasource.data= # Data (DML) script resource references.
spring.datasource.data-username= # Username of the database to execute DML scripts (if different).
spring.datasource.data-password= # Password of the database to execute DML scripts (if different).
spring.datasource.dbcp2.*= # Commons DBCP2 specific settings
spring.datasource.driver-class-name= # Fully qualified name of the JDBC driver. Auto-detected based on the URL by default.
spring.datasource.generate-unique-name=false # Whether to generate a random datasource name.
spring.datasource.hikari.*= # Hikari specific settings
spring.datasource.initialization-mode=embedded # Initialize the datasource with available DDL and DML scripts.
spring.datasource.jmx-enabled=false # Whether to enable JMX support (if provided by the underlying pool).
spring.datasource.jndi-name= # JNDI location of the datasource. Class, url, username & password are ignored when set.
spring.datasource.name= # Name of the datasource. Default to "testdb" when using an embedded database.
spring.datasource.password= # Login password of the database.
spring.datasource.platform=all # Platform to use in the DDL or DML scripts (such as schema-${platform}.sql or data-${platform}.sql).
spring.datasource.schema= # Schema (DDL) script resource references.
spring.datasource.schema-username= # Username of the database to execute DDL scripts (if different).
spring.datasource.schema-password= # Password of the database to execute DDL scripts (if different).
spring.datasource.separator=; # Statement separator in SQL initialization scripts.
spring.datasource.sql-script-encoding= # SQL scripts encoding.
spring.datasource.tomcat.*= # Tomcat datasource specific settings
spring.datasource.type= # Fully qualified name of the connection pool implementation to use. By default, it is auto-detected from the classpath.
spring.datasource.url= # JDBC URL of the database.
spring.datasource.username= # Login username of the database.
spring.datasource.xa.data-source-class-name= # XA datasource fully qualified name.
spring.datasource.xa.properties= # Properties to pass to the XA data source.

# LIQUIBASE (LiquibaseProperties)
spring.liquibase.change-log=classpath:/db/changelog/db.changelog-master.yaml # Change log configuration path.
spring.liquibase.check-change-log-location=true # Whether to check that the change log location exists.
spring.liquibase.contexts= # Comma-separated list of runtime contexts to use.
spring.liquibase.database-change-log-lock-table=DATABASECHANGELOGLOCK # Name of table to use for tracking concurrent Liquibase usage.
spring.liquibase.database-change-log-table=DATABASECHANGELOG # Name of table to use for tracking change history.
spring.liquibase.default-schema= # Default database schema.
spring.liquibase.drop-first=false # Whether to first drop the database schema.
spring.liquibase.enabled=true # Whether to enable Liquibase support.
spring.liquibase.labels= # Comma-separated list of runtime labels to use.
spring.liquibase.liquibase-schema= # Schema to use for Liquibase objects.
spring.liquibase.liquibase-tablespace= # Tablespace to use for Liquibase objects.
spring.liquibase.parameters.*= # Change log parameters.
spring.liquibase.password= # Login password of the database to migrate.
spring.liquibase.rollback-file= # File to which rollback SQL is written when an update is performed.
spring.liquibase.test-rollback-on-update=false # Whether rollback should be tested before update is performed.
spring.liquibase.url= # JDBC URL of the database to migrate. If not set, the primary configured data source is used.
spring.liquibase.user= # Login user of the database to migrate.

application.properties file의 확장자를 yml로 변경하면 yaml형식으로 설정파일이 쓰여지게 되고, 필자는 아래와 같이 작성하였다. (각 항목에 대한 설명이 이어진다.)

#Database
spring:
  datasource:
    url: jdbc:postgresql://192.168.1.104:5432/battlefield
    username: browndwarf
    password: browndwarf
 
#Connection Pool
    hikari:
      connection-timeout: 20000
      maximum-pool-size: 10

#JPA
  jpa:
    database: POSTGRESQL
    database-platform: org.hibernate.dialect.PostgreSQL95Dialect
    show-sql: false
    generate-ddl: true
    hibernate:
      ddl-auto: validate
      naming:
        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl    
    properties:
      hibernate:
        jdbc:
          lob:
            non_contextual: true

#Liquibase
  liquibase:
    change-log: classpath:/db/changelog-master.xml
  • spring.datasource section
    • url : DB 접속 URL과 Port 번호이다. 이 URL을 통해 driverClassName을 자동 감지 하기 때문에 driverClassName을 별도로 명기할 필요는 없다. HikariCP를 사용할 경우 이 인자는 HikariCP의 jdbc-url property로 자동으로 Mapping되게 된다.
    • username, password : DB접속시 사용할 사용자 ID 및 Password 정보이다.
    • hikari.* : Spring Boot 2.x 부터는 JDBC Connection Pool이 Default로 HikariCP가 사용되기 때문에 Connection Pool에 관련된 설정은 여기에 해야한다. 설정 사항들은 Hikari Github를 참조하면 되는데, 원래 정의된 인자가 Camel Case Naming을 사용한데 반해, Spring Boot 설정 파일에서는 Kebob Case Naming으로 변경해서 Mapping 되었다. 예를 들어 connectionTimeout의 경우 connection-timeout으로 설정해야 한다.
  • spring.jpa section
    • database-platform : Project에서 사용할 DB Engine을 명기한다. 명기하지 않아도 Auto Detect되고 database property를 대신 설정해도 된다.
    • show-sql : JPA에 의해 생성되는 Query를 Standard Output으로 확인하려면 이 값을 true로 하면 된다. (좀 더 보기 좋게 출력되길 원하면 spring.jpa.properties.hibernate.format_sql을 true로 설정하는 것을 권장) 이 설정은 Logging 설정에 영향을 받지 않는 다는 점을 주의해야 한다.
    • generate-ddl : Schema 초기화 여부를 설정한다. true이면 spring.jpa.hibernate.ddl-auto 설정 값에 의해 동작하며, false인 경우에는 Schema 초기화 과정에 관여하지 않는다.
    • hibernate.ddl-auto : 'create', 'create-drop', 'update', 'validate', 'none' 중에 하나의 값을 선택해야 한다.  h2, hsqldb, derby와 같은 embedded DB를 사용할 때 Schema Manager가 감지되지 않으면 'create-drop'이 default로 수행되고, 나머지는 'none'이 default 값이다. 각각의 값에 따라 다음과 같이 동작한다.
      • create : 초기화 과정에서 Table 생성, 만약 기존에 있었다면 삭제 후 생성한다.
      • create-drop : 초기화 과정에서 Table을 생성하고, 종료 시점에 만들어진 Table을 삭제한다. Unit Test 용으로 많이 사용된다.
      • update : Coulmn 추가/삭제와 같이 Table 변경 사항에 대해서 조치하게 한다. Table 생성/삭제는 관여하지 않는다.
      • validate : Entity와 Table Mapping 오류만 점검하고, 오류 발견 시에 Exception을 발생시킨다.
      • none : Table 초기화 과정에 관여하지 않게 한다.
    • hibernate.naming.physical-strategy : Java Code내에 Entity와 Attribute를 DB의 Table과 Column으로 Mapping 할 때 Naming을 어떤 전략으로 다루는 지를 설정하는 부분이다. 값을 따로 지정하지 않으면 전체 소문자 + Under Score로 단어가 이어지는 Snake-case가 적용되며 이때 사용되는 구현체는 org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy 이다. 여기에서 설정한  org.hibernate.boot.model.naming.PhysicalNamingStrategyStadardImpl은 Snake-case로 변환 없이 Naming하게 된다. (*)
    • hibernate.use-new-id-generator-mappings : Entity의 PK를 @GeneratedValue anootation을 이용해서 생성할 때 Hibernate가 제공하는 최신 IdentifierGenerator를 사용할 지 여부를 정한다. True로 설정하면 org.hibernate.id.enhanced.SequenceStyleGenerator가 기본으로 사용되고, false로 설정하면 org.hibernate.id.SequenceGenerator가 사용된다. SequenceStyleGenerator의 경우 DB에서 Sequence를 지원하지 않을 때에 최적화된 방법을 제공하는 'enhanced' 구현체이지만, PostgreSQL에서는 Sequence 기능을 제공하기 때문에 별다른 영향은 없을 것으로 예상된다.
    • spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation : Remark 참조.
  • spring.liquibase.change-log : Java Project내에서 DB Schema의 형상 관리 기능을 제공하는 Tool로 liquibase, flyway 가 있는데, liquibase를 사용할 경우 DB Schema의 변경 내역을 정리한 File을 나타낸다.

changelog-master.xml

liquibase에 의해 관리되는 DB Schema 형상 관리를 정의한 파일이다. xml, yml, json, sql 등으로 작성할 수 있으며, 필자는 아래와 같은 xml로 작성했다. (각 Tag들의 설명이 이어진다.)

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
        xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
        xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd
    http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">
    
    <preConditions>
        <runningAs username="browndwarf"/>
    </preConditions>

    <changeSet id="1" author="browndwarf">
        <createTable tableName="job">
            <column name="id" type="BIGINT" autoIncrement="true">
                <constraints primaryKey="true" nullable="false"/>
            </column>
            <column name="name" type="varchar(50)">
            	<constraints nullable="false"/>
            </column>
            <column name="customer" type="varchar(50)"/>
            <column name="description" type="varchar(255)"/>
            <column name="jobStatus" type="varchar(20)"/>
            <column name="dueDate" type="varchar(8)"/>
            <column name="createdTime" type="TIMESTAMP(6) WITHOUT TIME ZONE"/>
            <column name="updatedTime" type="TIMESTAMP(6) WITHOUT TIME ZONE"/>
        </createTable>
        
	    <createIndex indexName="idx_job_status" tableName="job">
	        <column name="jobStatus" type="varchar(20)"/>
	    </createIndex>
	    
	    <createIndex indexName="idx_job_customer" tableName="job">
	        <column name="customer" type="varchar(50)"/>
	    </createIndex>
    </changeSet>  
    
</databaseChangeLog>
  • changeSet : 하나의 변경 이력을 나타낸다. id를 기준으로 Schema 변경 내역이 구분 된다.
  • createTable : Table 생성을 정의한다. 이 예에서 만들어 지는 Table Name은 'job'이다.
  • column : Table을 구성하는 Column을 정의한다. 하위에 constraint tag를 가질 수 있으며, 이를 통해 id field는 PK로 설정하였고, id, name field는 null value를 가질 수 없게 제약을 두었다.
  • createIndex : DB에서 사용되는 Index를 생성한다. Index Name과 대상 Table Name, Index 를 구성하는 Column들을 정의해야 한다. 위의 예에서는 job Table에 2개의 Index를 생성하고 있다.

JobEntity.java

DB의 Table과 Mapping되는 객체가 Entity이고, Spring JPA Framework에서는 '@Entity'를 통해 Entity임을 나타낸다. @Table을 통해 Entity와 Mapping되는 Table을 명시적으로 나타낼 수 있고, 이 예제처럼 @Table을 사용하지 않는 경우에는 Entity Class Name이나 Entity의 명시적인 name parameter을 이용해 Table Name을 정의한다. 평범한 Entity Class이고 lombok Library를 통해 Set/Get 함수와 Argument없는 생성자 함수, 그리고 Builder 함수가 만들어 지게 @NoArgsConstructor, @Getter, @Stter annotation을 사용하였다.

@NoArgsConstructor
@Entity(name="job")
@Getter @Setter
public class JobEntity implements Serializable {

	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private long	id;
	
	private String	name;
	
	private String 	customer;
	
	private String	description;

	private String 	jobStatus;
	
	private String	dueDate;
	
	@Column(updatable=false)
	@CreationTimestamp
	private LocalDateTime	createdTime;
	
	@UpdateTimestamp
	private LocalDateTime	updatedTime;

	@Builder
	public JobEntity(String name, String customer, String description, String jobStatus, String dueDate) {
		this.name = name;
		this.customer = customer;
		this.description = description;
		this.jobStatus = jobStatus;
		this.dueDate = dueDate;
	}
}

실행결과

기본적인 환경 구성만 만든 프로젝트이기 때문에 별다른 실행 결과는 없고, Unit Test할 대상도 없기 때문에 아래와 같이 Log와 Database 내용을 통해 환경 구성 결과를 확인해 보겠다.

 

Application Log

Application Level에서는 아래와 같은 로그를 확인할 수 있다. 최초 실행할 때에는 DB에서 사용할 Schema들이 존재하지 않기 때문에 생성하는 초기화 작업을 진행하게 된다. 제일 먼저 liquibase에서 DB Schema Version 관리를 위해 사용하는 Table인 databasechangelog와 databasechangeloglock가 만들어지고, 그 후에 liquibase changelog 파일에 명시한 순서대로 job table 생성, idx_job_status, idx_job_customer index가 생성되고 있다.

[16:32:01.977][INFO ][main            / HikariDataSource] HikariPool-1 - Starting...
[16:32:02.014][INFO ][main            / HikariDataSource] HikariPool-1 - Start completed.
[16:32:02.670][INFO ][main            / JdbcExecutor   ] SELECT COUNT(*) FROM public.databasechangeloglock
[16:32:02.677][INFO ][main            / JdbcExecutor   ] CREATE TABLE public.databasechangeloglock (ID INTEGER NOT NULL, LOCKED BOOLEAN NOT NULL, LOCKGRANTED TIMESTAMP WITHOUT TIME ZONE, LOCKEDBY VARCHAR(255), CONSTRAINT DATABASECHANGELOGLOCK_PKEY PRIMARY KEY (ID))
[16:32:02.831][INFO ][main            / JdbcExecutor   ] SELECT COUNT(*) FROM public.databasechangeloglock
[16:32:02.858][INFO ][main            / JdbcExecutor   ] DELETE FROM public.databasechangeloglock
[16:32:02.859][INFO ][main            / JdbcExecutor   ] INSERT INTO public.databasechangeloglock (ID, LOCKED) VALUES (1, FALSE)
[16:32:02.892][INFO ][main            / JdbcExecutor   ] SELECT LOCKED FROM public.databasechangeloglock WHERE ID=1
[16:32:02.963][INFO ][main            / StandardLockService] Successfully acquired change log lock
[16:32:03.562][INFO ][main            / StandardChangeLogHistoryService] Creating database history table with name: public.databasechangelog
[16:32:03.563][INFO ][main            / JdbcExecutor   ] CREATE TABLE public.databasechangelog (ID VARCHAR(255) NOT NULL, AUTHOR VARCHAR(255) NOT NULL, FILENAME VARCHAR(255) NOT NULL, DATEEXECUTED TIMESTAMP WITHOUT TIME ZONE NOT NULL, ORDEREXECUTED INTEGER NOT NULL, EXECTYPE VARCHAR(10) NOT NULL, MD5SUM VARCHAR(35), DESCRIPTION VARCHAR(255), COMMENTS VARCHAR(255), TAG VARCHAR(255), LIQUIBASE VARCHAR(20), CONTEXTS VARCHAR(255), LABELS VARCHAR(255), DEPLOYMENT_ID VARCHAR(10))
[16:32:03.630][INFO ][main            / JdbcExecutor   ] SELECT COUNT(*) FROM public.databasechangelog
[16:32:03.632][INFO ][main            / StandardChangeLogHistoryService] Reading from public.databasechangelog
[16:32:03.632][INFO ][main            / JdbcExecutor   ] SELECT * FROM public.databasechangelog ORDER BY DATEEXECUTED ASC, ORDEREXECUTED ASC
[16:32:03.634][INFO ][main            / JdbcExecutor   ] SELECT COUNT(*) FROM public.databasechangeloglock
[16:32:03.644][INFO ][main            / JdbcExecutor   ] CREATE TABLE public.job (id BIGSERIAL NOT NULL, name VARCHAR(50) NOT NULL, customer VARCHAR(50), description VARCHAR(255), "jobStatus" VARCHAR(20), "dueDate" VARCHAR(8), "createdTime" TIMESTAMP(6) WITHOUT TIME ZONE, "updatedTime" TIMESTAMP(6) WITHOUT TIME ZONE, CONSTRAINT JOB_PKEY PRIMARY KEY (id))
[16:32:03.708][INFO ][main            / ChangeSet      ] Table job created
[16:32:03.709][INFO ][main            / JdbcExecutor   ] CREATE INDEX idx_job_status ON public.job("jobStatus")
[16:32:03.742][INFO ][main            / ChangeSet      ] Index idx_job_status created
[16:32:03.742][INFO ][main            / JdbcExecutor   ] CREATE INDEX idx_job_customer ON public.job(customer)
[16:32:03.775][INFO ][main            / ChangeSet      ] Index idx_job_customer created
[16:32:03.792][INFO ][main            / ChangeSet      ] ChangeSet classpath:/db/changelog-localnaws.xml::1::browndwarf ran successfully in 148ms
[16:32:03.792][INFO ][main            / JdbcExecutor   ] SELECT MAX(ORDEREXECUTED) FROM public.databasechangelog
[16:32:03.796][INFO ][main            / JdbcExecutor   ] INSERT INTO public.databasechangelog (ID, AUTHOR, FILENAME, DATEEXECUTED, ORDEREXECUTED, MD5SUM, DESCRIPTION, COMMENTS, EXECTYPE, CONTEXTS, LABELS, LIQUIBASE, DEPLOYMENT_ID) VALUES ('1', 'browndwarf', 'classpath:/db/changelog-localnaws.xml', NOW(), 1, '8:1939885ffadd8d5ba6d7de5c8dfbe065', 'createTable tableName=job; createIndex indexName=idx_job_status, tableName=job; createIndex indexName=idx_job_customer, tableName=job', '', 'EXECUTED', NULL, NULL, '3.6.3', '9287923635')
[16:32:03.826][INFO ][main            / StandardLockService] Successfully released change log lock

 

Database Log

Database Log에서도 유사한 내용을 확인할 수 있다. 다만 아래 발생한 1개의 Error는 최초 실행시 Liquibase 상태를 조회하는 과정에서 해당 Table이 없기 때문에 발생한 Error 이니 신경쓰지 않아도 된다.

< 2019-05-31 16:32:02.119 KST >LOG:  execute <unnamed>: SET extra_float_digits = 3
< 2019-05-31 16:32:02.120 KST >LOG:  execute <unnamed>: SET application_name = 'PostgreSQL JDBC Driver'
< 2019-05-31 16:32:02.124 KST >LOG:  execute <unnamed>: SET extra_float_digits = 3
< 2019-05-31 16:32:02.124 KST >LOG:  execute <unnamed>: SET application_name = 'PostgreSQL JDBC Driver'
< 2019-05-31 16:32:02.128 KST >LOG:  execute <unnamed>: SET extra_float_digits = 3
< 2019-05-31 16:32:02.128 KST >LOG:  execute <unnamed>: SET application_name = 'PostgreSQL JDBC Driver'
...
< 2019-05-31 16:32:02.671 KST >LOG:  execute <unnamed>: BEGIN
< 2019-05-31 16:32:02.671 KST >ERROR:  relation "public.databasechangeloglock" does not exist at character 22
< 2019-05-31 16:32:02.671 KST >STATEMENT:  SELECT COUNT(*) FROM public.databasechangeloglock
< 2019-05-31 16:32:02.674 KST >LOG:  execute S_1: ROLLBACK
< 2019-05-31 16:32:02.679 KST >LOG:  execute <unnamed>: BEGIN
< 2019-05-31 16:32:02.679 KST >LOG:  execute <unnamed>: CREATE TABLE public.databasechangeloglock (ID INTEGER NOT NULL, LOCKED BOOLEAN NOT NULL, LOCKGRANTED TIMESTAMP WITHOUT TIME ZONE, LOCKEDBY VARCHAR(255), CONSTRAINT DATABASECHANGELOGLOCK_PKEY PRIMARY KEY (ID))
< 2019-05-31 16:32:02.815 KST >LOG:  execute S_2: COMMIT
< 2019-05-31 16:32:02.832 KST >LOG:  execute <unnamed>: BEGIN
< 2019-05-31 16:32:02.857 KST >LOG:  execute <unnamed>: SELECT COUNT(*) FROM public.databasechangeloglock
< 2019-05-31 16:32:02.859 KST >LOG:  execute <unnamed>: DELETE FROM public.databasechangeloglock
< 2019-05-31 16:32:02.860 KST >LOG:  execute <unnamed>: INSERT INTO public.databasechangeloglock (ID, LOCKED) VALUES (1, FALSE)
< 2019-05-31 16:32:02.861 KST >LOG:  execute S_2: COMMIT
< 2019-05-31 16:32:02.894 KST >LOG:  execute <unnamed>: BEGIN
< 2019-05-31 16:32:02.894 KST >LOG:  execute <unnamed>: SELECT LOCKED FROM public.databasechangeloglock WHERE ID=1
< 2019-05-31 16:32:02.897 KST >LOG:  execute <unnamed>: UPDATE public.databasechangeloglock SET LOCKED = TRUE, LOCKEDBY = '16.8.35.227 (16.8.35.227)', LOCKGRANTED = '2019-05-31 16:32:02.894' WHERE ID = 1 AND LOCKED = FALSE
...
< 2019-05-31 16:32:03.632 KST >LOG:  execute <unnamed>: BEGIN
< 2019-05-31 16:32:03.632 KST >LOG:  execute <unnamed>: SELECT COUNT(*) FROM public.databasechangelog
< 2019-05-31 16:32:03.633 KST >LOG:  execute <unnamed>: SELECT * FROM public.databasechangelog ORDER BY DATEEXECUTED ASC, ORDEREXECUTED ASC
< 2019-05-31 16:32:03.634 KST >LOG:  execute S_2: COMMIT
< 2019-05-31 16:32:03.635 KST >LOG:  execute <unnamed>: BEGIN
< 2019-05-31 16:32:03.635 KST >LOG:  execute <unnamed>: SELECT COUNT(*) FROM public.databasechangeloglock
< 2019-05-31 16:32:03.637 KST >LOG:  execute S_1: ROLLBACK
< 2019-05-31 16:32:03.646 KST >LOG:  execute <unnamed>: BEGIN
< 2019-05-31 16:32:03.646 KST >LOG:  execute <unnamed>: CREATE TABLE public.job (id BIGSERIAL NOT NULL, name VARCHAR(50) NOT NULL, customer VARCHAR(50), description VARCHAR(255), "jobStatus" VARCHAR(20), "dueDate" VARCHAR(8), "createdTime" TIMESTAMP(6) WITHOUT TIME ZONE, "updatedTime" TIMESTAMP(6) WITHOUT TIME ZONE, CONSTRAINT JOB_PKEY PRIMARY KEY (id))
< 2019-05-31 16:32:03.710 KST >LOG:  execute <unnamed>: CREATE INDEX idx_job_status ON public.job("jobStatus")
< 2019-05-31 16:32:03.743 KST >LOG:  execute <unnamed>: CREATE INDEX idx_job_customer ON public.job(customer)
< 2019-05-31 16:32:03.777 KST >LOG:  execute S_2: COMMIT
< 2019-05-31 16:32:03.794 KST >LOG:  execute <unnamed>: BEGIN
< 2019-05-31 16:32:03.795 KST >LOG:  execute <unnamed>: SELECT MAX(ORDEREXECUTED) FROM public.databasechangelog
< 2019-05-31 16:32:03.798 KST >LOG:  execute <unnamed>: INSERT INTO public.databasechangelog (ID, AUTHOR, FILENAME, DATEEXECUTED, ORDEREXECUTED, MD5SUM, DESCRIPTION, COMMENTS, EXECTYPE, CONTEXTS, LABELS, LIQUIBASE, DEPLOYMENT_ID) VALUES ('1', 'browndwarf', 'classpath:/db/changelog-localnaws.xml', NOW(), 1, '8:1939885ffadd8d5ba6d7de5c8dfbe065', 'createTable tableName=job; createIndex indexName=idx_job_status, tableName=job; createIndex indexName=idx_job_customer, tableName=job', '', 'EXECUTED', NULL, NULL, '3.6.3', '9287923635')
< 2019-05-31 16:32:03.798 KST >LOG:  execute S_2: COMMIT
< 2019-05-31 16:32:03.811 KST >LOG:  execute <unnamed>: BEGIN
< 2019-05-31 16:32:03.811 KST >LOG:  execute <unnamed>: UPDATE public.databasechangeloglock SET LOCKED = FALSE, LOCKEDBY = NULL, LOCKGRANTED = NULL WHERE ID = 1
< 2019-05-31 16:32:03.812 KST >LOG:  execute S_2: COMMIT
...

 

Database Schema

Project를 실행하기 전에 Database를 생성해서 내용을 확인해보면 아무런 Table이 존재하지 않지만, 실행이후 초기화 작업이 끝나게 되면 Table이 만들어져 있는 것을 확인할 수 있다.

postgres=# \c battlefield browndwarf

// Project를 실행하기 전
battlefield=# \dt
No relations found.

...

// 프로젝트 실행 이후
battlefield=# \dt
                  List of relations
 Schema |         Name          | Type  |   Owner    
--------+-----------------------+-------+------------
 public | databasechangelog     | table | browndwarf
 public | databasechangeloglock | table | browndwarf
 public | job                   | table | browndwarf
(3 rows)

Remark

PostgreSQL과 JPA/Hibernate의 조합으로 Application을 개발하는 경우 아래와 같은 오류를 접하게 될 수 있다.

2019-05-31 16:11:27.631  INFO 20368 --- [           main] org.hibernate.dialect.Dialect            : HHH000400: Using dialect: org.hibernate.dialect.PostgreSQL95Dialect
2019-05-31 16:11:28.397  INFO 20368 --- [           main] o.h.e.j.e.i.LobCreatorBuilderImpl        : HHH000424: Disabling contextual LOB creation as createClob() method threw error : java.lang.reflect.InvocationTargetException

java.lang.reflect.InvocationTargetException: null
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_191]
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_191]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_191]
	at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_191]
	at org.hibernate.engine.jdbc.env.internal.LobCreatorBuilderImpl.useContextualLobCreation(LobCreatorBuilderImpl.java:113) [hibernate-core-5.3.10.Final.jar:5.3.10.Final]
...

2019-05-31 16:11:28.409  INFO 20368 --- [           main] org.hibernate.type.BasicTypeRegistry     : HHH000270: Type registration [java.util.UUID] overrides previous : org.hibernate.type.UUIDBinaryType@35636217

이런 오류 메시지가 나오는 원인은 현재 postgresql jdbc library(필자의 경우에는 postgresql-42.2.5.jar)에서 Clob 생성 함수(org.postgresql.jdbc.PgConnection에 포함)가 아래와 같이 Exception을 발생시키게 구현되어 있기 때문이다.

...
  @Override
  public Clob createClob() throws SQLException {
    checkClosed();
    throw org.postgresql.Driver.notImplemented(this.getClass(), "createClob()");
  }
...

여러 곳에서 이 문제를 언급한 적이 있는 것으로 봐서 꽤 오래된 문제로 추측이 되고 언제 이 함수가 구현될 지 모르겠다. 다만 Exception이 발생해도 프로젝트가 실행되는 데는 문제가 없다. (향후 CLOB 필드를 사용하게될 경우 문제가 있을 것으로 추측되는데, 확실하게 어떤 제약이 있는 지는 모르겠다) 그럼에도 불구하고 이렇게 Exception이 발생하는 것에 대해 거부감이 있다면 2 가지 방법을 통해 해결할 수 있다. 하나는 spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation property를 true로 설정하는 것이고, 다른 하나는 spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults property를 false로 설정하는 것이다. 여기에서는 전자의 방법을 사용하였다. 각각 어떤 차이가 있는 지는 다음과 같다. 모두 사용하게 되면 내부 구현 Code 특성상 use_jdbc_metadata_defaults property가 false인 설정에 따라 초기화가 이루어진다.

 

방법 1. spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true

spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation property를 true로 설정하면 hibernate의 jdbc 환경 설정 부분에서 CLOB 객체를 생성하는 함수에서 개별 Database의 구현 함수 부분을 호출하는 것을 아래와 같이 막고 있다. 이 때문에 Log로 Warning Message만 하나 남기고 초기화 과정이 끝나게 된다.

package org.hibernate.engine.jdbc.env.internal;

public class LobCreatorBuilderImpl implements LobCreatorBuilder {
...
	@SuppressWarnings("unchecked")
	private static boolean useContextualLobCreation(Map configValues, Connection jdbcConnection) {
		final boolean isNonContextualLobCreationRequired =
				ConfigurationHelper.getBoolean( Environment.NON_CONTEXTUAL_LOB_CREATION, configValues );
		if ( isNonContextualLobCreationRequired ) {
			LOG.disablingContextualLOBCreation( Environment.NON_CONTEXTUAL_LOB_CREATION );
			return false;
		}
...

 

방법 2. spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults= false;

이 설정을 이용하게 되면 Jdbc 환경구성을 하는 과정에서 Default Metadata를 사용할 지 여부에 따라 분기하게 된다. 분기의 시작점은 JdbcEnvironmentInitiator class의 initiateService 함수에서 시작되게 된다. 이 함수에서 JdbcEnvironmentImpl Class를 Instance화 할 때 다른 생성자 함수를 호출하게 된다.

package org.hibernate.engine.jdbc.env.internal;
...
public class JdbcEnvironmentInitiator implements StandardServiceInitiator<JdbcEnvironment> {
...
	@Override
	public JdbcEnvironment initiateService(Map configurationValues, ServiceRegistryImplementor registry) {
		final DialectFactory dialectFactory = registry.getService( DialectFactory.class );

		// 'hibernate.temp.use_jdbc_metadata_defaults' is a temporary magic value.
		// The need for it is intended to be alleviated with future development, thus it is
		// not defined as an Environment constant...
		//
		// it is used to control whether we should consult the JDBC metadata to determine
		// certain Settings default values; it is useful to *not* do this when the database
		// may not be available (mainly in tools usage).
		boolean useJdbcMetadata = ConfigurationHelper.getBoolean(
				"hibernate.temp.use_jdbc_metadata_defaults",
				configurationValues,
				true
		);

		if ( useJdbcMetadata ) {
        ...
 					return new JdbcEnvironmentImpl(
							registry,
							dialect,
							dbmd
					);       
        ...
        }
		// if we get here, either we were asked to not use JDBC metadata or accessing the JDBC metadata failed.
		return new JdbcEnvironmentImpl( registry, dialectFactory.buildDialect( configurationValues, null ) );
	}
    ...
}

결과적으로는 방법 1과 유사하게 warning message 하나만을 남기고 마무리되지만 JdbcEnvironmentImpl 객체를 생성하는 과정에서 차이점이 많다. 그래서 필자는 use_jdbc_metadata_defaults property보다는 non_contextual_creation property를 이용하고 있다.

package org.hibernate.engine.jdbc.env.internal;
...

public class JdbcEnvironmentImpl implements JdbcEnvironment {
...
	public static LobCreatorBuilderImpl makeLobCreatorBuilder() {
		LOG.disablingContextualLOBCreationSinceConnectionNull();
		return new LobCreatorBuilderImpl( false );
	}​
...
}

 

To do(*)

spring.jpa.hibernate.naming.implicit-strategy property도 Naming 작업에 같이 사용된다고 하는데, PostgreSQL 사용시 이 proeprty가 어떤 영향을 미치는지는 확인하지 못했다. 이 부분은 나중에 확인해 보려고 한다.