#INTERMEDIATE

[Spring] H2로 @SpringBootTest 적용 시 Table not found 에러

피치트리 2022. 4. 12. 23:44

 가끔 그런 날이 있다. 맨날 되다가 갑자기 안될 때 ^^; 평소처럼 Spring Boot 환경에서 H2 DB로 테스트 케이스를 작성하고 있었는데 갑자기 "Table xxx not found" 에러를 만나게 되었다. spring.datasource 설정들을 h2로 완료하고, schema.sql, data.sql을 test/resources폴더에 등록해두었는데.. 왜 안 되는 거지?! 그때부터 구글링을 시작했다. 스프링을 사용하고, 항상 최신 버전으로 업데이트하면서도, 릴리즈 노트를 잘 안 읽었던 나를 반성한다. 오늘은 Spring Boot 2.5 버전에서 DataSource 초기화와 관련된 변경사항을 살펴보려고 한다.

 

Spring Boot 2.5 Release Note

Spring Boot 공식문서

더보기

SQL Script DataSource Initialization

The underlying method used to support schema.sql and data.sql scripts has been redesigned in Spring Boot 2.5. spring.datasource.* properties related to DataSource initialization have been deprecated in favor of new spring.sql.init.* properties. These properties can also be used to initialize an SQL database accessed via R2DBC.

schema.sql and data.sql Files

With Spring Boot 2.5.1 and above, the new SQL initialization properties support detection of embedded datasources for JDBC and R2DBC. By default, SQL database initialization is only performed when using an embedded in-memory database. To always initialize a SQL database, irrespective of its type, set spring.sql.init.mode to always. Similarly, to disable initialization, set spring.sql.init.mode to never.

 현 상황에서 주의 깊게 보아야 할 사항은 가장 윗부분에 있었다. 기존의 spring.datasource.* 설정이 deprecated되고 spring.sql.init.* 설정으로 변경되었으며 R2DBC를 포함한 SQL 데이터베이스 초기화를 해당 설정의 값의 여부에 따라 진행하겠다는 것이다. 즉 해당 초기화를 위해서는 spring.sql.init.mode: always로 적용해야 한다. 2.5.1 버전부터는 embedded db인 경우에는 해당 설정 없이도 적용된다. * AbstractScriptDatabaseInitializer 참고

private boolean isEnabled() {
    if (this.settings.getMode() == DatabaseInitializationMode.NEVER) {
        return false;
    }
    return this.settings.getMode() == DatabaseInitializationMode.ALWAYS || isEmbeddedDatabase();
}

 관련 로직을 찾아보다 보니 autoconfigure.jdbc 패키지의 DataSourceInitializationConfiguration이 @Deprecated 상태였다. 새로운 설정에 따라 autoconfigure.sql.init 패키지의 SqlInitializationAutoConfiguration이 적용된다. script로 초기화되는 부분은 DataSourceInitializationConfiguration -> SqlDataSourceScriptDatabaseInitializer를 참고하면 된다. 실제 구현은 DataSourceScriptDatabaseInitializer에 있으며 runScripts 함수에 디버깅 포인트를 걸어보면 내가 작성한 script가 resources에 잘 담겨서 넘어오는지를 확인해보면 된다.

 

 여기까지 확인해보니 일단 문제는 없었다. 사실 H2 임베디드 디비를 사용했기 때문에 해당 설정을 해주지 않아도 스크립트는 잘 로드가 되었다. 다시 에러 메시지를 보니 h2의 Parser에서 에러가 났고, 해당 위치로 가서 디버깅을 해보았다.

at org.h2.command.Parser.getTableOrViewNotFoundDbException(Parser.java:8379)

 Parser의 getTableOrViewNotFoundDbException에서 database.getFirstUserTable()로 테이블을 조회해 보니 내가 만들려고 하는 테이블이 대문자로 만들어져 있는 것.. sql 문에서는 소문자로 만들었었는데 이상하다 싶어서 검색해보니 H2쪽에 이슈가 있어 보인다. sql문에서 테이블명을 "" quote로 감싸거나  DATABASE_TO_UPPER=false 설정을 해주면 소문자로 생성된다고 했다. (H2 github) 일단 설정이 편해서 설정으로 해보았는데 이번에는 "Column xxx.ID not found" 에러가 발생했다.

 

하.. 테이블 명 뿐만 아니라 컬럼명도 모두 소문자가 되기 때문이다. jpa를 사용하면 hibernate의 NamingStrategy를 변경하면 되는 것 같은데 나는 data-jdbc를 사용하고 있었다. 그래서 entity를 다시 보니, entity를 작성할 때 주로 @Table 어노테이션을 이용하는데, 

@Table("my_table")
data class MyTable(
    @Id
    val id: Long? = null
)

이런 식으로 @Table annotation이 붙은 아이는 소문자로 인식되는데, column은 대문자로 되는 것 같다. 다른 방법을 찾아보니 필드마다 @Column으로 소문자로 지정하라는데.. 사실 이 부분은 테스트를 위해 H2를 사용하는 상황이라 본 로직을 건드리고 싶지는 않았다. 그래서 테이블명은 소문자, 컬럼명은 대문자여야 하는 현재 상황을 해결하기 위해, sql에서 테이블 명만 quote로 감싸주었고 정상적으로 동작했다.

 

정리해보자면 H2 DB였기 때문에 sql.init:always 설정의 문제는 아니었고, H2+Spring Data 조합에서 테이블/컬럼명 대소문자 이슈가 있어서 그걸 회피하는 방법으로 해결했다.

 

0. 현재 버전

 - spring-boot-starter-data-jdbc: 2.6.6

 - com.h2database:h2:2.1.212

 

1. application.yaml 설정

spring:
  datasource:
    url: jdbc:h2:mem:test
    driver-class-name: org.h2.Driver
    username: test
    password: test

2. test/resources 폴더 밑에 schema.sql과 data.sql

 - schema.sql

DROP TABLE "my_table" IF EXISTS;
CREATE TABLE "my_table" (
  id int NOT NULL AUTO_INCREMENT
);

 - data.sql

insert into "my_table" (id) values (1);

 

3. entity

위의 entity 참고

 

이런 상황에서 정상적으로 동작함을 확인했다. :) 비슷한 고민하고 있는 분들께 도움이 되길 바라며.