h2 인메모리 환경 Database Cleaner 오작동
문제상황
기존 DatabaseCleaner 코드
@Component
public class DatabaseCleaner {
@Autowired private DataSource dataSource;
@Autowired private JdbcTemplate jdbcTemplate;
private List<String> tables;
@PostConstruct
public void init() {
this.tables =
jdbcTemplate.query("show tables", (rs, rowNum) -> rs.getString(1)).stream().toList();
}
public void clean() throws SQLException {
try (Connection connection = dataSource.getConnection()) {
if (!connection.getMetaData().getDriverName().contains("H2")) {
throw new IllegalStateException("인메모리 데이터베이스에서만 사용 가능한 기능입니다.");
}
var preparedStatement = connection.prepareStatement("set foreign_key_checks = 0");
preparedStatement.executeUpdate();
for (String table : tables) {
var statement = connection.prepareStatement("truncate table " + table);
statement.executeUpdate();
}
preparedStatement = connection.prepareStatement("set foreign_key_checks = 1");
preparedStatement.executeUpdate();
}
}
}
원인 1) h2에서 외래키 활성화/비활성화 처리하는 방법이 바뀜
원인 2) h2에서 truncate table 명령어만으로 auto_increment가 초기화되지 않음
💬 발생했던 에러 코드
- org.h2.jdbc.JdbcSQLSyntaxErrorException: Syntax error in SQL statement "set [*]foreign_key_checks = 0"; expected "@, AUTOCOMMIT, EXCLUSIVE, IGNORECASE, PASSWORD, SALT, MODE, DATABASE, COLLATION, CLUSTER, DATABASE_EVENT_LISTENER, ALLOW_LITERALS, DEFAULT_TABLE_TYPE, SCHEMA, CATALOG, SCHEMA_SEARCH_PATH, JAVA_OBJECT_SERIALIZER, IGNORE_CATALOGS, SESSION, TRANSACTION, TIME, NON_KEYWORDS, DEFAULT_NULL_ORDERING"; SQL statement: set foreign_key_checks = 0 [42001-224] at org.h2.message.DbException.getJdbcSQLException(DbException.java:514) at org.h2.message.DbException.getJdbcSQLException(DbException.java:489) at org.h2.message.DbException.getSyntaxError(DbException.java:261) at org.h2.command.ParserBase.getSyntaxError(ParserBase.java:750) at org.h2.command.Parser.parseSet(Parser.java:7725) at org.h2.command.Parser.parsePrepared(Parser.java:635) at org.h2.command.Parser.parse(Parser.java:592) at org.h2.command.Parser.parse(Parser.java:569) at org.h2.command.Parser.prepareCommand(Parser.java:483) at org.h2.engine.SessionLocal.prepareLocal(SessionLocal.java:639) at org.h2.engine.SessionLocal.prepareCommand(SessionLocal.java:559) at org.h2.jdbc.JdbcConnection.prepareCommand(JdbcConnection.java:1166) at org.h2.jdbc.JdbcPreparedStatement.<init>(JdbcPreparedStatement.java:93) at org.h2.jdbc.JdbcConnection.prepareStatement(JdbcConnection.java:316) at net.mgrv.apis.customer.utils.DatabaseCleaner.clean(DatabaseCleaner.java:35) at net.mgrv.apis.customer.utils.DatabaseCleanExtension.beforeEach(DatabaseCleanExtension.java:16) at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeBeforeEachCallbacks$2(TestMethodTestDescriptor.java:167) at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeBeforeMethodsOrCallbacksUntilExceptionOccurs$6(TestMethodTestDescriptor.java:203) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeBeforeMethodsOrCallbacksUntilExceptionOccurs(TestMethodTestDescriptor.java:203) at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeBeforeEachCallbacks(TestMethodTestDescriptor.java:166) at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:133) at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:69) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
문제 해결
해결 1) SET REFERENTIAL_INTEGRITY 명령어로 처리
SET REFERENTIAL_INTEGRITY True ,SET REFERENTIAL_INTEGRITY False로 외래키 활성화/비활성화 처리
SQL 실행 방법
// 1. compile error 발생
var preparedStatement = connection.prepareStatement("SET REFERENTIAL_INTEGRITY FALSE");
// 2. String.format을 통해 처리
var preparedStatement = connection.prepareStatement(String.format(REFERENTIAL, "FALSE"));

"SET REFERENTIAL_INTEGRITY FALSE" 로 작성한 경우 컴파일 에러가 발생하여 formatting 하여 처리하였습니다.
해결 2) truncate table [table] INDENTITY 명령어를 통해 처리
truncate table [tableName] → TRUNCATE TABLE [tableName] RESTART IDENTITY
- RESTART IDENTITY를 붙이면 auto_increment도 함께 초기화됨
- 기존 MySQL 데이터베이스에서는 truncate 명령어를 사용하면 테이블 정보가 모두 초기화되는데, h2 데이터베이스는 데이터만 초기화되어 추가 명령어를 작성해주어야 합니다.
최종 Database Cleaner 코드
@Component
public class DatabaseCleaner {
private static final String TRUNCATE_TABLE = "TRUNCATE TABLE %s RESTART IDENTITY";
private static final String REFERENTIAL = "SET REFERENTIAL_INTEGRITY %s";
@Autowired private DataSource dataSource;
@Autowired private JdbcTemplate jdbcTemplate;
private List<String> tables;
@PostConstruct
public void init() {
this.tables =
jdbcTemplate.query("show tables", (rs, rowNum) -> rs.getString(1)).stream().toList();
}
public void clean() throws SQLException {
try (Connection connection = dataSource.getConnection()) {
if (!connection.getMetaData().getDriverName().contains("H2")) {
throw new IllegalStateException("인메모리 데이터베이스에서만 사용 가능한 기능입니다.");
}
var preparedStatement = connection.prepareStatement(String.format(REFERENTIAL, "FALSE"));
preparedStatement.executeUpdate();
for (String table : tables) {
var statement = connection.prepareStatement(String.format(TRUNCATE_TABLE, table));
statement.executeUpdate();
}
preparedStatement = connection.prepareStatement(String.format(REFERENTIAL, "TRUE"));
preparedStatement.executeUpdate();
}
}
}
삽질한 흔적
IDENTITY 명령어로 간단히 처리되는 줄 모르고, alter 명령어를 사용하려고 했었습니다.
@Profile("test")
@Component
public class DatabaseCleaner {
private static final String RESET_TABLE_ID = "ALTER TABLE %s ALTER COLUMN %s RESTART WITH 1";
@PersistenceContext
private EntityManager entityManager;
@Autowired private DataSource dataSource;
@Autowired private JdbcTemplate jdbcTemplate;
public void clean() throws SQLException {
// 외래 키 제약 조건 비활성화
entityManager.createNativeQuery(String.format(REFERENTIAL, "FALSE")).executeUpdate();
// 모든 엔터티 타입 가져오기 (엔티티의 @Id, @Column 정보를 가져오기 위함)
Set<EntityType<?>> entities = entityManager.getMetamodel().getEntities();
for (EntityType<?> entity : entities) {
String tableName = getTableName(entity); // 해당 부분 생략
// @Id로 설정된 필드 정보 가져오기 (@Column 존재 여부, 카멜케이스 (slotId) ...)
String idColumnName = getIdColumnName(entity);
// 테이블 초기화
entityManager.createNativeQuery(String.format(TRUNCATE_TABLE, tableName)).executeUpdate();
// AUTO_INCREMENT 값 초기화
entityManager.createNativeQuery(
String.format(RESET_TABLE_ID, tableName, idColumnName)
).executeUpdate();
}
// 외래 키 제약 조건 활성화
entityManager.createNativeQuery(String.format(REFERENTIAL, "TRUE")).executeUpdate();
}
private String getIdColumnName(EntityType<?> entity) {
Class<?> entityClass = entity.getJavaType();
// 엔티티 클래스에서 @Id 필드를 찾아서 처리
for (Field field : entityClass.getDeclaredFields()) {
if (field.isAnnotationPresent(Id.class)) {
// @Column 어노테이션이 있는지 확인
Column columnAnnotation = field.getAnnotation(Column.class);
// @Column(name) 어노테이션 값 반환
if (columnAnnotation != null) {
return columnAnnotation.name();
} else {
// @Column 어노테이션이 없으면 필드명 반환 (카멜케이스 형태도 있어서 변환 후 반환)
return CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, field.getName());
}
}
}
return null;
}
}
IDENTITY 명령어로 간단히 처리되는 줄 모르고, alter 명령어를 사용하려고 했었습니다.
하지만, PK로 설정된 id 값이 여러 방법으로 작성되어 있어, alter table ~~ alter column 방법으로 초기화할 수 없었습니다.
// 1
@Id
private Long id;
// 2
@Id
@Column("user_id")
private Long id;
// 3
@Id
private Long userId;
-> 테이블에 설정된 PK 필드 값이 모두 달라 ALTER TABLE [table] ALTER COLUMN ID RESTART WITH 1 로 auto_increment를 처리할 수 없었습니다.
그래서 모든 상황을 반영한 DatabaseCleaner를 만들었는데, 다른 테이블에서 해당 테이블을 찾을 수 없다는 에러가 반환되어 더 찾아보다가 더 간단한 방법으로 처리하였습니다.
참고자료
Spring Service 계층 테스트와 데이터 초기화
JPA는 특성상 실제 쿼리를 날리지 않아도 DB의 값이 변경되는 경우가 많습니다. 그리고 보통 영속성 컨택스트의 생명 주기가 서비스 계층과 비슷하기 때문에 서비스 계층 단위 테스트를 할 때는 J
wlsh44.tistory.com
'Spring Framework > Spring' 카테고리의 다른 글
h2 인메모리 환경 Database Cleaner 오작동
문제상황
기존 DatabaseCleaner 코드
@Component
public class DatabaseCleaner {
@Autowired private DataSource dataSource;
@Autowired private JdbcTemplate jdbcTemplate;
private List<String> tables;
@PostConstruct
public void init() {
this.tables =
jdbcTemplate.query("show tables", (rs, rowNum) -> rs.getString(1)).stream().toList();
}
public void clean() throws SQLException {
try (Connection connection = dataSource.getConnection()) {
if (!connection.getMetaData().getDriverName().contains("H2")) {
throw new IllegalStateException("인메모리 데이터베이스에서만 사용 가능한 기능입니다.");
}
var preparedStatement = connection.prepareStatement("set foreign_key_checks = 0");
preparedStatement.executeUpdate();
for (String table : tables) {
var statement = connection.prepareStatement("truncate table " + table);
statement.executeUpdate();
}
preparedStatement = connection.prepareStatement("set foreign_key_checks = 1");
preparedStatement.executeUpdate();
}
}
}
원인 1) h2에서 외래키 활성화/비활성화 처리하는 방법이 바뀜
원인 2) h2에서 truncate table 명령어만으로 auto_increment가 초기화되지 않음
💬 발생했던 에러 코드
- org.h2.jdbc.JdbcSQLSyntaxErrorException: Syntax error in SQL statement "set [*]foreign_key_checks = 0"; expected "@, AUTOCOMMIT, EXCLUSIVE, IGNORECASE, PASSWORD, SALT, MODE, DATABASE, COLLATION, CLUSTER, DATABASE_EVENT_LISTENER, ALLOW_LITERALS, DEFAULT_TABLE_TYPE, SCHEMA, CATALOG, SCHEMA_SEARCH_PATH, JAVA_OBJECT_SERIALIZER, IGNORE_CATALOGS, SESSION, TRANSACTION, TIME, NON_KEYWORDS, DEFAULT_NULL_ORDERING"; SQL statement: set foreign_key_checks = 0 [42001-224] at org.h2.message.DbException.getJdbcSQLException(DbException.java:514) at org.h2.message.DbException.getJdbcSQLException(DbException.java:489) at org.h2.message.DbException.getSyntaxError(DbException.java:261) at org.h2.command.ParserBase.getSyntaxError(ParserBase.java:750) at org.h2.command.Parser.parseSet(Parser.java:7725) at org.h2.command.Parser.parsePrepared(Parser.java:635) at org.h2.command.Parser.parse(Parser.java:592) at org.h2.command.Parser.parse(Parser.java:569) at org.h2.command.Parser.prepareCommand(Parser.java:483) at org.h2.engine.SessionLocal.prepareLocal(SessionLocal.java:639) at org.h2.engine.SessionLocal.prepareCommand(SessionLocal.java:559) at org.h2.jdbc.JdbcConnection.prepareCommand(JdbcConnection.java:1166) at org.h2.jdbc.JdbcPreparedStatement.<init>(JdbcPreparedStatement.java:93) at org.h2.jdbc.JdbcConnection.prepareStatement(JdbcConnection.java:316) at net.mgrv.apis.customer.utils.DatabaseCleaner.clean(DatabaseCleaner.java:35) at net.mgrv.apis.customer.utils.DatabaseCleanExtension.beforeEach(DatabaseCleanExtension.java:16) at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeBeforeEachCallbacks$2(TestMethodTestDescriptor.java:167) at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeBeforeMethodsOrCallbacksUntilExceptionOccurs$6(TestMethodTestDescriptor.java:203) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeBeforeMethodsOrCallbacksUntilExceptionOccurs(TestMethodTestDescriptor.java:203) at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeBeforeEachCallbacks(TestMethodTestDescriptor.java:166) at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:133) at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:69) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
문제 해결
해결 1) SET REFERENTIAL_INTEGRITY 명령어로 처리
SET REFERENTIAL_INTEGRITY True ,SET REFERENTIAL_INTEGRITY False로 외래키 활성화/비활성화 처리
SQL 실행 방법
// 1. compile error 발생
var preparedStatement = connection.prepareStatement("SET REFERENTIAL_INTEGRITY FALSE");
// 2. String.format을 통해 처리
var preparedStatement = connection.prepareStatement(String.format(REFERENTIAL, "FALSE"));

"SET REFERENTIAL_INTEGRITY FALSE" 로 작성한 경우 컴파일 에러가 발생하여 formatting 하여 처리하였습니다.
해결 2) truncate table [table] INDENTITY 명령어를 통해 처리
truncate table [tableName] → TRUNCATE TABLE [tableName] RESTART IDENTITY
- RESTART IDENTITY를 붙이면 auto_increment도 함께 초기화됨
- 기존 MySQL 데이터베이스에서는 truncate 명령어를 사용하면 테이블 정보가 모두 초기화되는데, h2 데이터베이스는 데이터만 초기화되어 추가 명령어를 작성해주어야 합니다.
최종 Database Cleaner 코드
@Component
public class DatabaseCleaner {
private static final String TRUNCATE_TABLE = "TRUNCATE TABLE %s RESTART IDENTITY";
private static final String REFERENTIAL = "SET REFERENTIAL_INTEGRITY %s";
@Autowired private DataSource dataSource;
@Autowired private JdbcTemplate jdbcTemplate;
private List<String> tables;
@PostConstruct
public void init() {
this.tables =
jdbcTemplate.query("show tables", (rs, rowNum) -> rs.getString(1)).stream().toList();
}
public void clean() throws SQLException {
try (Connection connection = dataSource.getConnection()) {
if (!connection.getMetaData().getDriverName().contains("H2")) {
throw new IllegalStateException("인메모리 데이터베이스에서만 사용 가능한 기능입니다.");
}
var preparedStatement = connection.prepareStatement(String.format(REFERENTIAL, "FALSE"));
preparedStatement.executeUpdate();
for (String table : tables) {
var statement = connection.prepareStatement(String.format(TRUNCATE_TABLE, table));
statement.executeUpdate();
}
preparedStatement = connection.prepareStatement(String.format(REFERENTIAL, "TRUE"));
preparedStatement.executeUpdate();
}
}
}
삽질한 흔적
IDENTITY 명령어로 간단히 처리되는 줄 모르고, alter 명령어를 사용하려고 했었습니다.
@Profile("test")
@Component
public class DatabaseCleaner {
private static final String RESET_TABLE_ID = "ALTER TABLE %s ALTER COLUMN %s RESTART WITH 1";
@PersistenceContext
private EntityManager entityManager;
@Autowired private DataSource dataSource;
@Autowired private JdbcTemplate jdbcTemplate;
public void clean() throws SQLException {
// 외래 키 제약 조건 비활성화
entityManager.createNativeQuery(String.format(REFERENTIAL, "FALSE")).executeUpdate();
// 모든 엔터티 타입 가져오기 (엔티티의 @Id, @Column 정보를 가져오기 위함)
Set<EntityType<?>> entities = entityManager.getMetamodel().getEntities();
for (EntityType<?> entity : entities) {
String tableName = getTableName(entity); // 해당 부분 생략
// @Id로 설정된 필드 정보 가져오기 (@Column 존재 여부, 카멜케이스 (slotId) ...)
String idColumnName = getIdColumnName(entity);
// 테이블 초기화
entityManager.createNativeQuery(String.format(TRUNCATE_TABLE, tableName)).executeUpdate();
// AUTO_INCREMENT 값 초기화
entityManager.createNativeQuery(
String.format(RESET_TABLE_ID, tableName, idColumnName)
).executeUpdate();
}
// 외래 키 제약 조건 활성화
entityManager.createNativeQuery(String.format(REFERENTIAL, "TRUE")).executeUpdate();
}
private String getIdColumnName(EntityType<?> entity) {
Class<?> entityClass = entity.getJavaType();
// 엔티티 클래스에서 @Id 필드를 찾아서 처리
for (Field field : entityClass.getDeclaredFields()) {
if (field.isAnnotationPresent(Id.class)) {
// @Column 어노테이션이 있는지 확인
Column columnAnnotation = field.getAnnotation(Column.class);
// @Column(name) 어노테이션 값 반환
if (columnAnnotation != null) {
return columnAnnotation.name();
} else {
// @Column 어노테이션이 없으면 필드명 반환 (카멜케이스 형태도 있어서 변환 후 반환)
return CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, field.getName());
}
}
}
return null;
}
}
IDENTITY 명령어로 간단히 처리되는 줄 모르고, alter 명령어를 사용하려고 했었습니다.
하지만, PK로 설정된 id 값이 여러 방법으로 작성되어 있어, alter table ~~ alter column 방법으로 초기화할 수 없었습니다.
// 1
@Id
private Long id;
// 2
@Id
@Column("user_id")
private Long id;
// 3
@Id
private Long userId;
-> 테이블에 설정된 PK 필드 값이 모두 달라 ALTER TABLE [table] ALTER COLUMN ID RESTART WITH 1 로 auto_increment를 처리할 수 없었습니다.
그래서 모든 상황을 반영한 DatabaseCleaner를 만들었는데, 다른 테이블에서 해당 테이블을 찾을 수 없다는 에러가 반환되어 더 찾아보다가 더 간단한 방법으로 처리하였습니다.
참고자료
Spring Service 계층 테스트와 데이터 초기화
JPA는 특성상 실제 쿼리를 날리지 않아도 DB의 값이 변경되는 경우가 많습니다. 그리고 보통 영속성 컨택스트의 생명 주기가 서비스 계층과 비슷하기 때문에 서비스 계층 단위 테스트를 할 때는 J
wlsh44.tistory.com