JPA 프로젝트를 진행하던 와중, DB 연결 전 h2로 진행하다가 실제 DB와 연결을 했더니 갑자기 테이블이 존재하지 않는다는 에러가 발생했다.
Table 'test.notice_list' doesn't exist
???? 우선 DB를 확인해봤는데 테이블은 정상적으로 존재했다.
구글링을 찾아보니 JPA 네이밍 전략때문이었다.
현 프로젝트의 DB는 모두 UPPER SNAKE CASE를 사용하는데, LOWER SNAKE CASE 전략이 적용되고 있던 것이다..!
전에는 우연찮게 DB와 네이밍 전략이 맞아서 해당 문제가 발생하지 않았었다.
그렇다면 네이밍 전략에는 어떤 것이 있는지 알아보자.
JPA 네이밍 전략은 두가지 방식이 있다.
일명 암시적 네이밍 전략으로, 말 그대로 명시적으로 설정하지 않은 것을 암시적으로 설정해주는 것이다.
우리가 Entity를 구성할 때, 테이블명을 @Table
, 컬럼명을 @Column
이라고 지정하는 경우가 있다. 이렇게 @Table, @Column으로 이름을 지정하면 명시적으로 지정하는 것이 된다. 암시적 네이밍은 이렇게 명시적으로 지정되지 않은 곳에 적용된다.
JPA에서 default
로 설정된 암시적 네이밍 규칙은 ImplicitNamingStrategyJpaCompliantImpl
방식을 사용한다.
이는 이름을 Entity에 설정한 컬럼명 그대로 가는 것이다.
일반적으로 camelCase로 작성하므로 userId
이런 방식일 것이다.
그렇다면 아무 설정도 하지 않은 나는 lower snake 규칙이 적용된것일까??
찾아보니 spring 에서 제공하는 네이밍 규칙이 있었다. org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy
이 방식은 ImplicitNamingStrategyJpaCompliantImpl
방식을 상속받아 구현되어 있었다.
package org.springframework.boot.orm.jpa.hibernate;
import org.hibernate.boot.model.naming.Identifier;
import org.hibernate.boot.model.naming.ImplicitJoinTableNameSource;
import org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl;
public class SpringImplicitNamingStrategy extends ImplicitNamingStrategyJpaCompliantImpl {
public SpringImplicitNamingStrategy() {
}
public Identifier determineJoinTableName(ImplicitJoinTableNameSource source) {
String name = source.getOwningPhysicalTableName() + "_" + source.getAssociationOwningAttributePath().getProperty();
return this.toIdentifier(name, source.getBuildingContext());
}
}
물리적 네이밍 전략으로, 명시적, 암시적 전략 보다 나중에 실행되는 전략이다.
즉, 암시적, 명시적 전략을 설정해도 마지막에 무조건 물리적 네이밍 전략의 설정값으로 적용된다는 것이다.
예를 들어 명시적으로 @Table(name="notice_list")
설정하고, 물리적 네이밍 전략으로 camelCase
를 설정하면 테이블명은 최종적으로 noticeList
가 된다.
DB의 경우 어떤 테이블은 소문자고, 어떤 테이블은 대문자를 쓰진 않을 것이다. 프로젝트에 따라 일괄적인 네이밍을 사용할텐데, 암시적 전략을 사용하면 명시적 전략과 별도로 구분되므로 관리가 용이하지 않다.
그러므로 필자도 관리의 용이성을 위해 물리적 네이밍 전략을 사용하기로 했다.
물리적 네이밍 전략을 보니 이미 구현되어 있는 것들이 있었다.
- CamelCaseToUnderscoresNamingStrategy -> lower snake
- PhysicalNamingStrategyStandardImpl -> Entity 변수명 그대로
위 두가지가 있었는데 대문자로 변환해주는 건 없다..그러므로 커스텀해야한다..
하지만 CamelCaseToUnderscoresNamingStrategy
이 전략에서 대문자로만 변경하면 되니 코드를 조금만고치면 된다.
public class UpperCaseNamingStrategy implements PhysicalNamingStrategy {
public UpperCaseNamingStrategy() {
}
public Identifier toPhysicalCatalogName(Identifier name, JdbcEnvironment jdbcEnvironment) {
return this.apply(name, jdbcEnvironment);
}
public Identifier toPhysicalSchemaName(Identifier name, JdbcEnvironment jdbcEnvironment) {
return this.apply(name, jdbcEnvironment);
}
public Identifier toPhysicalTableName(Identifier name, JdbcEnvironment jdbcEnvironment) {
return this.apply(name, jdbcEnvironment);
}
public Identifier toPhysicalSequenceName(Identifier name, JdbcEnvironment jdbcEnvironment) {
return this.apply(name, jdbcEnvironment);
}
public Identifier toPhysicalColumnName(Identifier name, JdbcEnvironment jdbcEnvironment) {
return this.apply(name, jdbcEnvironment);
}
private Identifier apply(Identifier name, JdbcEnvironment jdbcEnvironment) {
if (name == null) {
return null;
} else {
StringBuilder builder = new StringBuilder(name.getText().replace('.', '_'));
for(int i = 1; i < builder.length() - 1; ++i) {
if (this.isUnderscoreRequired(builder.charAt(i - 1), builder.charAt(i), builder.charAt(i + 1))) {
builder.insert(i++, '_');
}
}
return this.getIdentifier(builder.toString(), name.isQuoted(), jdbcEnvironment);
}
}
protected Identifier getIdentifier(String name, boolean quoted, JdbcEnvironment jdbcEnvironment) {
if (this.isCaseInsensitive(jdbcEnvironment)) {
name = name.toUpperCase(Locale.ROOT);
}
return new Identifier(name, quoted);
}
protected boolean isCaseInsensitive(JdbcEnvironment jdbcEnvironment) {
return true;
}
private boolean isUnderscoreRequired(char before, char current, char after) {
return Character.isLowerCase(before) && Character.isUpperCase(current) && Character.isLowerCase(after);
}
}
physicalNamingStrategy
를 implements 해서 구현하면 된다.CamelCaseToUnderscoresNamingStrategy
를 참고했으므로 getIdentifier
메서드만 수정해서 넣어주었다. protected Identifier getIdentifier(String name, boolean quoted, JdbcEnvironment jdbcEnvironment) {
if (this.isCaseInsensitive(jdbcEnvironment)) {
name = name.toUpperCase(Locale.ROOT);
}
return new Identifier(name, quoted);
}
위와 같이 커스텀 물리적 규칙을 만들어주고 application.yml 에 적용해 주면 된다.
spring:
jpa:
hibernate:
naming:
physical-strategy: {패키지 경로}.UpperCaseNamingStrategy
이렇게 설정하면 원하던 upper snake
형식으로 이름이 정해진다.
최종적으로 위의 방식으로 lower snake -> upper snake 형식을 적용할 수 있었다.
JPA가 Entity에 작성된 이름들을 변환하는 것은 알고 있었지만, default로 왜 이런 규칙이 적용되는지, 물리적 전략, 암시적 전략으로 나뉘어져 있는 건 몰랐다. 역시 에러와 삽질이 지식 쌓기에는 굿,,,
https://www.baeldung.com/hibernate-naming-strategy
https://velog.io/@mumuni/Hibernate5-Naming-Strategy-%EA%B0%84%EB%8B%A8-%EC%A0%95%EB%A6%AC