티스토리 뷰

목표

  • Spring Boot 2.1.x 기반의 프로젝트에 HikariCP 커넥션 풀 라이브러리를 적용한 DataSource 빈을 생성한다.
  • 생성된 빈을 이용하여 EntityManagerFactory 빈과 PlatformTransactionManager 빈을 생성하여 트랜잭션 기반의 JPA 개발 환경을 구축한다.

build.gradle

  • /build.gradle 파일에 아래 내용을 추가한다.
dependencies {
    implementation('org.springframework.boot:spring-boot-starter-data-jpa')
    implementation('org.springframework.boot:spring-boot-starter-web')
    compileOnly('org.projectlombok:lombok')
    testImplementation('org.springframework.boot:spring-boot-starter-test')
    compile group: 'com.zaxxer', name: 'HikariCP', version: '3.2.0'
    compile group: 'mysql', name: 'mysql-connector-java', version: '8.0.13'
}
  • HikariCP는 Spring Boot 2.x부터 기본 커넥션 풀 라이브러리로 채택되었을 정도로 현존 최고 성능을 자랑한다.
  • MySQL 데이터베이스 사용을 가정하여 구성하였다.

application.yml

  • /src/main/resources/application.yml 파일에 아래 내용을 추가한다.
spring:
  datasource:
    somedb:
      jdbc-url: jdbc:mysql://localhost:3306/somedb?useUnicode=yes&characterEncoding=UTF-8
      driver-class-name: com.mysql.cj.jdbc.Driver
      username: someuser
      password: somepassword

  jpa:
    database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
    properties:
      hibernate:
        format_sql: true
    generate-ddl: true

logging:
  level:
    org:
      hibernate:
        SQL: DEBUG
        type:
          descriptor:
            sql:
              BasicBinder: TRACE
  • 본래 spring.datasource에 적절히 커넥션 풀 정보를 명시하면 Spring Boot가 자동으로 인식하여 DataSource 빈을 생성해준다. 하지만 프로젝트 내에 2개 이상의 DataSource 빈 설정이 필요할 경우 확장이 불가능하다. 따라서 위와 같이 somedb라는 이름의 데이터베이스 식별자를 추가하였다.
  • spring.jpa.database-platform에는 어떤 데이터베이스에 접속할 것인지에 대한 명시적 설정이 가능하다. MySQL의 경우 위와 같이 org.hibernate.dialect.MySQL5InnoDBDialect로 설정하면 InnoDB 스토리지 엔진의 특성을 활용할 수 있다.
  • spring.jpa.generate-ddl 옵션을 활성화화면 최초 데이터베이스 스키마를 자동으로 생성해준다. 이 옵션은 개발 환경에서만 권장하며 운영 환경에서는 Flyway와 같은 별도의 전문적인 마이그레이션 툴 이용을 권장한다.
  • logging.level.org.hibernate.SQLDEBUG로 설정하면 로그에 JPA가 실행하는 쿼리문을 출력해주는 역할을 한다. 추가적으로 logging.level.org.hibernate.type.descriptor.sql.BasicBinderTRACE로 설정해주면 쿼리문의 파라메터까지 출력해준다. 마지막으로 spring.jpa.properties.hibernate.format_sql 옵션을 활성화하면 쿼리문을 가독성이 좋게 정렬하여 출력해준다.

JpaConfig 작성

  • /src/main/java/JpaConfig.java 파일에 아래 내용을 작성한다.
package com.jsonobject.jpademo;

import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;

@Configuration
@EnableTransactionManagement
public class JpaConfig {

    @Primary
    @Bean(name = "dataSource")
    @ConfigurationProperties("spring.datasource.somedb")
    public DataSource dataSource() {

        return DataSourceBuilder.create().type(HikariDataSource.class).build();
    }

    @Primary
    @Bean(name = "entityManagerFactory")
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(

            EntityManagerFactoryBuilder builder,
            @Qualifier("dataSource") DataSource dataSource) {

        return builder.dataSource(dataSource).packages("com.jsonobject.jpademo").build();
    }

    @Bean(name = "transactionManager")
    public PlatformTransactionManager transactionManager(

            @Qualifier("entityManagerFactory") EntityManagerFactory entityManagerFactory) {

        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(entityManagerFactory);

        return transactionManager;
    }

}

User 엔티티 작성

  • 테이블을 대표할 엔티티를 작성할 차례이다. /src/main/java/User.java 파일에 아래 내용을 작성한다.
package com.jsonobject.jpademo;

import lombok.Data;

import javax.persistence.*;
import java.time.LocalDateTime;

@Data
@Entity
@Table(name = "user")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    private String email;

    private LocalDateTime createdAt;
}
  • @Entity를 클래스 레벨에 명시하여 JPA에 의해 관리되는 엔티티 임을 선언했다. 추가적으로 @Table을 명시하면 실제 맵핑될 테이블의 이름을 명확히 선언할 수 있다.
  • @Id를 필드 레벨에 명시하여 기본키 식별자 역할을 하는 필드 임을 선언했다. 추가적으로 @GeneratedValue(strategy = GenerationType.IDENTITY)을 명시하여 기본키 생성을 데이터베이스가 수행하도록 선언했다.

UserRepository 리파지터리 작성

  • User 엔티티에 대한 CRUD를 수행할 리파지터리를 작성할 차례이다. /src/main/java/UserRepository.java 파일에 아래 내용을 작성한다.
package com.jsonobject.jpademo;

import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {
}
  • 소스 코드를 보면 놀라울 정도로 간단하다. 단지 JpaRepository 인터페이스를 상속했을 뿐이다. 이 것 만으로도 엔티티에 대한 기본적인 CRUD를 위한 메써드를 제공한다.
  • 엔티티가 복잡해질수록 진가는 더욱 발휘된다. Spring Data가 사전 제공하는 네이밍 규칙을 따라 추가적으로 메써드를 선언하면 런타임에서 자동으로 해당 쿼리를 생성해준다. 자세한 네이밍 규칙은 이 글을 참고한다.

Application 작성

  • 이제 애플리케이션의 시작점이 되는 Application을 작성할 차례이다.
package com.jsonobject.jpademo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@SpringBootApplication
@EntityScan(basePackages = {"com.jsonobject.jpademo"})
@EnableJpaRepositories(basePackages = {"com.jsonobject.jpademo"})
public class JpaDemoApplication {

    public static void main(String[] args) {

        SpringApplication.run(JpaDemoApplication.class, args);
    }
}

테스트 케이스 작성

package com.jsonobject.jpademo;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.junit4.SpringRunner;

import javax.transaction.Transactional;
import java.time.LocalDateTime;

@RunWith(SpringRunner.class)
@SpringBootTest
public class JpaDemoApplicationTests {

    @Autowired
    private UserRepository userRepository;

    @Test
    @Transactional
    @Rollback(false)
    public void createUser() {

        User user = new User();
        user.setName("test");
        user.setEmail("test@gmail.com");
        user.setDate(LocalDateTime.now());
        userRepository.save(user);
    }
}
  • 테스트시 기본적으로 케이스를 성공하면 트랜잭션은 롤백된다. @Rollback(false)을 설정하면 트랜잭션 결과를 영구적으로 커밋할 수 있다.

Flyway 적용

  • Flyway는 데이터베이스의 마이그레이션 작업과 형상관리를 자동화해주는 무료 오픈 소스 라이브러리이다. 번거롭고 간과하기 쉬운 데이터베이스 형상관리를 자동으로 작업해주기 때문에 프로젝트 관리가 상당히 편리해진다. 적용 방법은 아래와 같다. 먼저 /build.gradle 파일에 아래 내용을 추가한다.
dependencies {
   compile group: 'org.flywaydb', name: 'flyway-core', version: '5.2.4'
}
  • 다음으로 /src/main/resources/application.yml 파일에 아래 설정을 추가한다.
spring:
  flyway:
    enabled: true
    encoding: UTF-8
  • spring.flyway.enabled 옵션을 true로 설정하면 애플리케이션 시작 시점마다 Flyway가 실행되어 자동으로 데이터베이스 형상관리를 수행한다. (형상관리 이력은 flyway_schema_history 테이블에 기록되어 관리된다. 마이그레이션 중 문제가 생길 경우 해당 테이블을 직접 수정하거나 삭제하면 된다.)

  • 마지막으로 Flyway의 형상관리 대상이 되는 SQL 스크립트를 작성할 차례이다. /src/main/resources/db/migration/V{version}__{description}.sql 파일에 초기 테이블과 인덱스를 생성하는 SQL 스크립트를 작성하면 된다. 형상이 변경되면 기존 스크립트는 유지한 채 위 파일명 컨벤션을 준수하여 새로운 스크립트를 작성하면 Flyway가 자동으로 적용해준다.

  • 애플리케이션 시작 후 형상관리가 성공적으로 완료되면 아래 로그가 출력된다.
2018-12-20 11:37:54.141  INFO 1279 --- [           main] o.f.c.internal.license.VersionPrinter    : Flyway Community Edition 5.2.4 by Boxfuse
2018-12-20 11:37:54.142  INFO 1279 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2018-12-20 11:37:54.170  INFO 1279 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2018-12-20 11:37:54.174  INFO 1279 --- [           main] o.f.c.internal.database.DatabaseFactory  : Database: jdbc:mariadb://localhost:3306/some_db (MySQL 5.6)
2018-12-20 11:37:54.217  INFO 1279 --- [           main] o.f.core.internal.command.DbValidate     : Successfully validated 1 migration (execution time 00:00.018s)
2018-12-20 11:37:54.258  INFO 1279 --- [           main] o.f.c.i.s.JdbcTableSchemaHistory         : Creating Schema History table: `some_db`.`flyway_schema_history`
2018-12-20 11:37:54.328  INFO 1279 --- [           main] o.f.core.internal.command.DbMigrate      : Current version of schema `some_db`: << Empty Schema >>
2018-12-20 11:37:54.329  INFO 1279 --- [           main] o.f.core.internal.command.DbMigrate      : Migrating schema `some_db` to version 20181220 - INIT
2018-12-20 11:37:54.408  INFO 1279 --- [           main] o.f.core.internal.command.DbMigrate      : Successfully applied 1 migration to schema `somedb` (execution time 00:00.150s)

Flyway: java.lang.IllegalStateException 트러블슈팅

  • 형상관리 대상 파일을 저장하는 프로젝트의 /src/main/resources/db/migration 디렉토리가 존재하지 않으면 아래 예외가 발생한다.
Caused by: java.lang.IllegalStateException: Cannot find migrations location in: [classpath:db/migration] (please add migrations or check your Flyway configuration)

Flyway: org.flywaydb.core.api.FlywayException 트러블슈팅

  • 형상관리 대상 파일의 파일명 규칙을 지키지 않을 경우 아래 예외가 발생한다. 파일명은 반드시 V{version}__{description}.sql이 되어야 한다. (대소문자도 지켜야 한다.)
Caused by: org.flywaydb.core.api.FlywayException: Wrong migration name format: xxxxx.sql(It should look like this: V1.2__Description.sql)

참고 글

댓글
댓글쓰기 폼