개요.

스프링부트 JPA 를 이용하려고 할 때, 실제 사용 DB 에 값을 넣지 않고 인메모리인 H2 를 사용해서 데이터에 대한 CRUD 코드를 작성하는 경우가 종종 있다. 이 때, 스프링부트 실행하는 시점에 H2 디비에 데이터를 넣어보는 것을 해보려고 한다. 더불어서 테스트코드에서 sql 을 실행시키는 방법에 대해서도 부가적으로 이것저것 알아보려고 한다. 

 

환경

  • springboot 2.3.0
  • junit5 ( JUnit Platform + JUnit Jupiter + JUnit Vintage)
    • JUnit Vintage 는 JUnit3 & JUnit4 를 실행할 수 있는 테스트 엔진을 제공한다.

관련소스코드

 

1. 스프링부트 실행 시, sql 을 실행시키기.

  • H2 인메모리 디비를 사용하고, url 접속토록 한다.
  • spring.jpa.hibernate.ddl-auto 를 none 으로 하고 .sql 파일을 통해 table 을 생성하고 data 를 삽입한다.
  • main/java/resources 디렉토리 하위에 schema.sql 과 data.sql 을 삽입한다. 그럼 부트 실행시 script sql 이 실행된다.

프로젝트 구조

  • data.sql 에는 테이블 데이터에 관한 sql 구문만 있다. (ex.INSERT)
  • schema.sql 에는 테이블 생성에 관한 sql 구문이 있다. (ex. CREATE TABLE )
  • data.sql 과 schema.sql 을 classpath 의 특정 디렉토리 경로 하위에 두고 실행시킬 수 있다.
    • spring.datasource.schema
    • spring.datasource.data

application.yml 파일 설정 (테스트 환경에서 동작토록)

spring:
  profiles:
    active: test

server:
  port: 8099

---
## test profile ##
spring:
  profiles: test

  h2:
    console:
      enabled: true
      path: /test_db                  # h2 console url 에 접근하기 위한 값. : `localhost:8099/test_db` 로 접근 가능

  datasource:
    driver-class-name: org.h2.Driver  # h2 드라이버 설정
    url: jdbc:h2:mem:testdb           # jdbc url 설정 (in-memory db 설정)
    username: sa
    password:
    initialization-mode: always       # datasource 를 타입(h2, mysql, oracle, ...) 에 상관없이 항상 초기화한다.
    platform: h2                      # datasource 타입 플랫폼 정의

	# .sql 파일을 classpath 하위로 두고, 특정 디렉토리 아래에서 실행시킬 수 있다.
	# schema: classpath:db/h2/schema.sql  # spring boot startup 시, 특정 경로의 schema.sql 실행
    # data: classpath:db/h2/data.sql      # spring boot startup 시, 특정 경로의 data.sql 실행
    
  jpa:
    database-platform: org.hibernate.dialect.H2Dialect
    hibernate:
      ddl-auto: none                            # ddl 옵션을 무엇으로 할 것인지 (none | create-drop | create | update | validate)
    generate-ddl: true                          # true 설정 시, 해당 데이터를 근거로 서버 시작 시점에 DDL 문 생성하여 DB 에 적용 (ddl 생성옵션 링크)
    show-sql: true                              # true 설정 시, 콘솔에 JPA 쿼리를 보여준다.
    properties.hibernate.format_sql: true       # true 설정 시, 콘솔에 표시되는 쿼리를 가독성있게 보여준다.
    properties.hibernate.use_sql_comments: false # true 설정 시, 콘솔에 표시되는 쿼리문 위에 어떤 실행을 하려는지 hint 를 보여준다.

spring.jpa.properties 하위에는 hibernate 설정들을 키/밸류 쌍으로 추가해줄 수 있다.

https://docs.jboss.org/hibernate/orm/5.5/userguide/html_single/Hibernate_User_Guide.html#configurations

 

 

스프링부트 실행 이후 H2 DB 에 데이터 조회

 

2. 스프링 부트 실행 시, 구동시점에서 초기 코드작업 수행.

  • 스프링부트 구동시점에 초기화작업을 수행할 수 있다.
  • @SpringBootApplication 이 붙은 실행메소드에 CommandLineRunner 인터페이스를 구현하고 메소드 run() 을 오버라이딩한다. 빈들이 컨테이너에 등록된 상태이기 때문에 빈을 주입받아 사용할 수 있다.

스프링부트 구동 Runner 클래스

menu 리스트를 만들고, saveAll() 메소드를 통해서 디비에 삽입한다. 그리고 삽입뒤에 반환된 목록을 System.out.println 으로 출력한다.

@SpringBootApplication
@RequiredArgsConstructor
@Slf4j
public class DemoApplication implements CommandLineRunner {

    private final DishRepository dishRepository;

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

    @Override
    public void run(String... args) throws Exception {

        final List<Dish> menu = Arrays.asList(
                new Dish("pork", false, 800, Dish.Type.MEAT),
                new Dish("beef", false, 700, Dish.Type.MEAT),
                new Dish("chicken", false, 400, Dish.Type.MEAT),
                new Dish("french fries", true, 530, Dish.Type.OTHER),
                new Dish("rice", true, 350, Dish.Type.OTHER),
                new Dish("season fruit", true, 120, Dish.Type.OTHER),
                new Dish("pizza", true, 550, Dish.Type.OTHER),
                new Dish("prawns", false, 300, Dish.Type.FISH),
                new Dish("salmon", false, 450, Dish.Type.FISH));

        List<Dish> savedMenu = dishRepository.saveAll(menu);
        savedMenu.forEach(System.out::println);
    }
}

 

data.sql

milk 만 디비에 넣고 나머지는 주석처리가 되어있다.

INSERT INTO dish (name, vegetarian, calories, type) VALUES ('milk', false, 100, 'OTHER');

// 아래는 주석처리.
-- INSERT INTO dish (name, vegetarian, calories, type) VALUES ('pork', false, 800, 'MEAT');
-- INSERT INTO dish (name, vegetarian, calories, type) VALUES ('beef', false, 700, 'MEAT');
-- INSERT INTO dish (name, vegetarian, calories, type) VALUES ('chicken', false, 400, 'MEAT');
-- INSERT INTO dish (name, vegetarian, calories, type) VALUES ('french fries', true, 530, 'OTHER');
-- INSERT INTO dish (name, vegetarian, calories, type) VALUES ('rice', true, 350, 'OTHER');
-- INSERT INTO dish (name, vegetarian, calories, type) VALUES ('season fruit', true, 120, 'OTHER');
-- INSERT INTO dish (name, vegetarian, calories, type) VALUES ('pizza', false, 550, 'OTHER');
-- INSERT INTO dish (name, vegetarian, calories, type) VALUES ('prawns', false, 300, 'FISH');
-- INSERT INTO dish (name, vegetarian, calories, type) VALUES ('salmon', false, 450, 'FISH');

 

출력결과

// 출력결과
/**
 * id 는 auto-increment 인데, data.sql 에 있는 INSERT 구문을 먼저 실행하고 난 뒤에,
 * CommandLineRunner 가 실행되서 id 찍히는 값이 2부터 찍히기 시작했다.
 **/

Dish(id=2, name=pork, vegetarian=false, calories=800, type=MEAT)
Dish(id=3, name=beef, vegetarian=false, calories=700, type=MEAT)
Dish(id=4, name=chicken, vegetarian=false, calories=400, type=MEAT)
Dish(id=5, name=french fries, vegetarian=true, calories=530, type=OTHER)
Dish(id=6, name=rice, vegetarian=true, calories=350, type=OTHER)
Dish(id=7, name=season fruit, vegetarian=true, calories=120, type=OTHER)
Dish(id=8, name=pizza, vegetarian=true, calories=550, type=OTHER)
Dish(id=9, name=prawns, vegetarian=false, calories=300, type=FISH)
Dish(id=10, name=salmon, vegetarian=false, calories=450, type=FISH)

 

3. 스프링부트 테스트 코드 작성 시, sql 스크립트 파일 실행

  • 스프링부트에서 테스트 코드 작성 시, 실디비를 통해서 테스트할 수 있지만 가상으로 데이터를 쌓아놓고 해당 데이터에 대한 테스트를 수행할 수 있다. (나의 경우 쿼리가 잘 나가는지 확인하기 위한 요소로 @DataJpaTest 를 이용한다.) 
  • 이 때, TestEntityManager 빈을 주입받아서 하나씩 코드레벨에서 넣어줄 수 있지만 sql 구문을 작성해서 넣어줄 있다.
  • 테스트코드 내에 @Sql 구문을 넣어주고, 어느 sql 파일을 읽어들일지 작성할 수 있다.

테스트 디렉토리 프로젝트 구조

 

DishRepositoryTest.java 소스

@ExtendWith(SpringExtension.class)
@DataJpaTest
@Sql(scripts = {"classpath:data/dish_insert.sql"})
@ActiveProfiles("test")
@DisplayName("메뉴 레파지토리에 대한 첫번째 방법 테스트는")
class DishRepositoryFirstWayTest {

    @Autowired
    private DishRepository dishRepository;

    @Test
    @DisplayName("전체 요리를 조회한다.")
    public void Should_DishesSizeGreaterThanZero_When_FindAll(){
        // when
        final List<Dish> dishes = dishRepository.findAll();

        // then
        assertThat(dishes).hasSizeGreaterThan(0);
    }
}

위의 코드를 살펴보면, 

  • @ActiveProfiles 를 두고 현재 프로파일을 test 로 명시한다. application.yml 이 test/resources 아래에 위치하게 하고 해당 yml 파일의 spring.profiles 를 test 로 작성해서 datasource 를 명시해준다. 해당 내용은 위의 application.yml 를 참고.
  • @Sql 애노테이션 스프링 4.1 때부터 추가된 것인데, 클래스 레벨 또는 메소드 레벨에 붙일 수 있다. 지정한 스크립트를 직접적으로 수행시킨다. classpath 는 기본적으로 test/resources 위치를 잡고있으니, 그 하위 경로인 data/dish_insert.sql 을 추가해주었다. 이렇게 하면 테스트 코드 실행이전에 sql 구문부터 수행해서 위의 메소드에 대한 테스트를 성공한다.

 

4. 스프링부트 테스트 코드 작성 시, TestEntityManager 를 이용

  • EntityManager 의 테스트격인 TestEntityManager 를 이용하여, 데이터를 삽입 및 영속상태의 엔티티를 db 와 동기화 맞춘 후 (flush) 이후에, Repository 로 조회한다.
@ExtendWith(SpringExtension.class)
@DataJpaTest
@ActiveProfiles("test")
@DisplayName("메뉴 레파지토리에 대한 두번째 방법 테스트는")
public class DishRepositorySecondWayTest {

    @Autowired
    private TestEntityManager testEntityManager;

    @Autowired
    private DishRepository dishRepository;

    @Test
    @DisplayName("하나의 요리를 조회한다.")
    public void Should_DishesSizeGreaterThanZero_When_FindAll() {
        // given
        final Dish dish = new Dish("noodle", false, 330, Dish.Type.OTHER);
        testEntityManager.persistAndFlush(dish);

        // when
        final Dish foundDish = dishRepository.getOne(1L);

        // then
        assertThat(foundDish.getName()).isEqualTo("noodle");
    }
}

 

추가(2020-06-03)

  • 회사업무를 하다가 엔티티상에 설정된 @EnableJpaAuditing 에 대한 엔티티 리스너를 간과하고 테스트 엔티티매니저로 레파지토리 테스트를 하던 중 에러를 발생했다. 디비 상에 데이터를 flush 하는 시점에 엔티티 생성시간/수정시간을 업데이트 해주는 트리거 메소드이다. 

    테스트 코드상에서도 Auditing 기능을 사용하는 엔티티를 테스트할 시, 반드시 @Import 구문을 통해 Auditing 설정 클래스를 임포트 해주어야 한다. 이거 때문에 삽질 2~3 시간 했다.

참고링크

[환경설정 application.yml 설정 및 내용정리]

https://pasudo123.tistory.com/357

 

[TestEntityManager 살펴보기]

https://pasudo123.tistory.com/348

 

[스프링부트 공식문서, 어떻게 데이터베이스 초기화하는지]

https://docs.spring.io/spring-boot/docs/current/reference/html/howto.html#howto-database-initialization

 

[스택오버플로우, 어떻게 SQL 스크립트 파일을 run 시키는지]

https://stackoverflow.com/questions/39280340/how-to-run-sql-scripts-and-get-data-on-application-startup

 

[Quick Guide on Loading intial Data with spring boot]

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

 

Posted by doubler
,