JPA책을 보면서 공부하던 중 Hibernate4 에서 5로 넘어가면서 변경된 내용이 있어 기록에 남기기로 했다.
그렇게 어려운 부분은 아니였지만 일부 예제를 사용하면 NullPointerException를 띄어버리기에 해당 글을 찾아보고 정리했다.
만약 동일한 에러가 뜬다면 참고가 되었으면 좋겠다.
Historical NamingStrategy contract
Historically Hibernate defined just a single org.hibernate.cfg.NamingStrategy. That singular NamingStrategy contract actually combined the separate concerns that are now modeled individually as ImplicitNamingStrategy and PhysicalNamingStrategy.
...
Due to these limitation, org.hibernate.cfg.NamingStrategy has been deprecated in favor of ImplicitNamingStrategy and PhysicalNamingStrategy.
출처 : Hibernate ORM 5.4.27.Final User Guide
공식 doc 문서에 따르면 org.hibernate.cfg.NamingStrategy는 더 이상 사용하지 않는다고 알리고 잇다. 또한 해당 대체재로서 ImplicitNamingStrategy(암시적 명칭 전략) 과 PhysicalNamingStrategy(물리적 명칭 전략)을 제시하고 있다.
우리는 이제 상황에 맞추어 이 2가지 Naming Strategy를 사용하면 된다.
이름에서 눈치 챌 수 있듯이 해당 방식은 명시적으로 naming이 지정되지 않은 Entity들의 명칭을 만들어주는 방식이다. 그렇기 @Table, @Column 등의 방법으로 명칭을 미리 지칭한 경우 해당 전략은 적용되지 않는다.
The ImplicitNamingStrategy would only be applied if an explicit name was not given.
출처 : Hibernate ORM 5.4.27.Final User Guide
해당 방법은 기본적으로 ImplicitNamingStrategyJpaCompliantImpl 방식을 따르며, JPA에 정의한 변수, 클래스 명칭 그대로 적용된다.
만약 사용자가 직접 전략을 구상하고 싶다면 ImplicitNamingStrategy 인터페이스나 해당 자식 클래스들을 상속받아 구현하고 적용하면 된다.
예시)
https://riptutorial.com/hibernate/example/10369/creating-and-using-a-custom-implicitnamingstrategy
위의 방법으로도 명칭 전략은 어느정도 충분하다고 생각할 수 있다.
그렇지만 예시를 하나 들어보자.
만약 기존의 개발자가 Entity의 일부 컬럼은 @Column으로 명시했고 그 외의 경우는 따로 만든 CustomImplicitNamingStrategy를 적용했다고 가정하자. 그러던 중 DB변경으로 명칭 정책이 바뀜에 따라 Column과 Table 명칭을 Uppercase, Snake 표기법으로, 변경한다면 어떻게 해야 할까?
개발자는 기존의 암시적 명칭 전략을 변경하는 작업과 동시에, 코드상에 명시적으로 정의한 명칭들을 전부 수정해야 한다.
이는 해당 전략이 명시적으로 naming이 정해진 Entity는 적용하지 않기 때문에 발생하는 문제다. 단순히 몇개가 안되면 상관이 없겠지만 수십, 수백개가 넘는 것들을 일일히 수정한다면 그것만큼 끔찍한 것이 없을 것이다.
물리적 명칭 전략은 위에 대해 훌륭한 대책이 된다.
해당 전략은 명시적 또는 암시적으로 명칭이 결정되든 항상, 마지막으로 적용되기 때문이다.
... But the point here is the separation of concerns. The PhysicalNamingStrategy will be applied regardless of whether the attribute explicitly specified the column name or whether we determined that implicitly. The ImplicitNamingStrategy would only be applied if an explicit name was not given. So, it all depends on needs and intent.
출처 : Hibernate ORM 5.4.27.Final User Guide
해당 전략은 명시적, 암시적 전략이 먼저 적용되고 그 이후에 적용이 된다.
그렇기에 공식 문서에서는 DB의 공통적인 규칙(Upper or lowercase, Snake case, 명칭 약자 등)을 적용할 때 사용하도록 권장하고 있다.
기본값으로는 logical_name 즉, 명시적, 암시적 명칭을 그대로 사용하며 이를 변경하려면 PhysicalNamingStrategy 인터페이스나 해당 자식들을 상속받아 정의하면 된다.
아래의 예제는 Uppercase, snakecase를 적용한 모습이다.
<!-- persistence.xml-->
<property name="hibernate.physical_naming_strategy" value="JPA.UppercaseSnakePhysicalNamingStrategy"/>
//java
package JPA
import org.hibernate.boot.model.naming.Identifier;
import org.hibernate.boot.model.naming.PhysicalNamingStrategy;
import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment;
public class UppercaseSnakePhysicalNamingStrategy implements PhysicalNamingStrategy {
@Override
public Identifier toPhysicalCatalogName(Identifier identifier, JdbcEnvironment jdbcEnvironment) {
if (identifier == null) {
return null;
}
return convertToSnakeUpperCase(identifier);
}
@Override
public Identifier toPhysicalSchemaName(Identifier identifier, JdbcEnvironment jdbcEnvironment) {
if (identifier == null) {
return null;
}
return convertToSnakeUpperCase(identifier);
}
@Override
public Identifier toPhysicalTableName(Identifier identifier, JdbcEnvironment jdbcEnvironment) {
return convertToSnakeUpperCase(identifier);
}
@Override
public Identifier toPhysicalSequenceName(Identifier identifier, JdbcEnvironment jdbcEnvironment) {
return convertToSnakeUpperCase(identifier);
}
@Override
public Identifier toPhysicalColumnName(Identifier identifier, JdbcEnvironment jdbcEnvironment) {
return convertToSnakeUpperCase(identifier);
}
private Identifier convertToSnakeUpperCase(final Identifier identifier) {
final String regex = "([a-z])([A-Z])";
final String replacement = "$1_$2";
final String newName = identifier.getText()
.replaceAll(regex, replacement)
.toUpperCase();
return Identifier.toIdentifier(newName);
}
}
일부 예제 코드를 사용하면 nullpointer이 발생하는 것을 발견했다.
원인은 아래의 예제 Entity를 만들 때 테이블의 catalog, schema를 지정하지 않았기 때문에 toPhysicalCatalogName, toPhysicalSchemaName에서 Identifier 값이 null이 되는 것이었다. 해당 메소드에는 추가로 nullpointer 체크를 포함시켰다.
해당방법을 실제로 적용해보면 명시적, 암시적으로 정의해도 물리적 명칭 전략이 적용 됨을 확인할 수 있다.
//적용할 Entity
@Entity
@Table(name = "MEMBER")
@Getter @Setter
public class Member {
@Id
@Column(name = "id")
private String id;
@Column(name = "name", nullable = false, length = 10)
private String username;
private Integer age;
@Enumerated(EnumType.STRING)
@Column
private RoleType roleType;
@CreationTimestamp
@Column
private LocalDateTime createdDate;
@UpdateTimestamp
@Column
private LocalDateTime lastModifiedDate;
@Lob
@Column
private String description;
}
/*log 결과*/
...
Hibernate:
create table MEMBER (
ID varchar(255) not null,
AGE integer,
CREATED_DATE datetime(6),
DESCRIPTION longtext,
LAST_MODIFIED_DATE datetime(6),
ROLE_TYPE varchar(255),
NAME varchar(10) not null,
primary key (ID)
) engine=InnoDB
...
책에서는 2페이지 남짓하게 짤막하게 설명하는 내용이다.
그냥 버전을 낮추어서 해보면 되지만 내 성격상 그건 또 안되서 디버깅 해보면서 좀 길게 찾아본 내용이다.(그리고 즐겁다!)
혹시 찾아보면서 이해가 안되는 분들이 있다면 한번 읽어보시고 도움이 됬으면 합니다.
https://docs.jboss.org/hibernate/orm/5.4/userguide/html_single/Hibernate_User_Guide.html
https://www.baeldung.com/hibernate-naming-strategy
https://thorben-janssen.com/naming-strategies-in-hibernate-5/
https://riptutorial.com/hibernate/example/10369/creating-and-using-a-custom-implicitnamingstrategy
허허허허 반갑소