원래 순서상 2.3.6. Custom BasicTypes을 다뤄야겠지만 동일한 기능을 더 간단하게 구현할 수 있는 JPA 2.1 AttributeConverters 가 있어서 해당 기능을 다루고자 한다.
별도로 필자가 spring boot + kotlin 환경에서 AttributeConverters를 테스트해본 내용도 같이 작성할 예정이다.
2.3.16. JPA 2.1 AttributeConverters
비록 하이버네이트가 custom types를 제공해왔지만, JPA 2.1 구현체로서, AttributeConverter 역시 제공한다.
사용자 정의 AttributeConverter를 이용해 애플리케이션 개발자는 주어진 JDBC type을 엔티티 기본 타입으로 매핑할 수 있다.
아래 예제에서는 java.time.Period가 VARCHAR 타입인 데이터베이스 칼럼으로 매핑될 것 이다.
원문
Although Hibernate has long been offering custom types, as a JPA 2.1 provider, it also supports AttributeConverter as well.
With a custom AttributeConverter, the application developer can map a given JDBC type to an entity basic type.
In the following example, the java.time.Period is going to be mapped to a VARCHAR database column.
Example 58. java.time.Period custom AttributeConverter
@Converter
public class PeriodStringConverter
implements AttributeConverter<Period, String> {
@Override
public String convertToDatabaseColumn(Period attribute) {
return attribute.toString();
}
@Override
public Period convertToEntityAttribute(String dbData) {
return Period.parse( dbData );
}
}
위 사용자정의 AttributeConverter를 사용하기 위해서 @Convert 어노테이션을 Period 타입의 엔티티 속성 위에 위치시켜야한다.
(To make use of this custom converter, the @Convert annotation must decorate the entity attribute.)
Example 59. Entity using the custom java.time.Period AttributeConverter mapping
@Entity(name = "Event")
public static class Event {
@Id
@GeneratedValue
private Long id;
@Convert(converter = PeriodStringConverter.class)
@Column(columnDefinition = "")
private Period span;
//Getters and setters are omitted for brevity
}
Event 엔티티를 영속화할 때, 하이버네이트는 AttributeConverter(PeriodStringConverter) 로직에 근거해 타입 변환을 수행할 것이다.
(When persisting such entity, Hibernate will do the type conversion based on the AttributeConverter logic:)
Example 60. Persisting entity using the custom AttributeConverter
INSERT INTO Event ( span, id )
VALUES ( 'P1Y2M3D', 1 )
AttributeConverter Java and JDBC types
변환의 데이터베이스측을 위한 자바 타입 명세가 알려지지 않았을 때, 하이버네이트는 java.io.Serializable 타입으로 대체한다.
변환 수행 중 데이터베이스측에 명세된 자바 타입(두 번째 AttributeConverter 결합 매개변수)이 알려지지 않은 경우,
하이버네이트는 java.io.Serializable 타입으로 대체한다.
필자 해설
AttributeConverter가 수행하는 변환에서 데이버베이스 칼럼측으로 연결되는 자바 타입(즉, JDBC type으로 변환되는 자바 타입)을 하이버네이트가 어느 데이터베이스 타입으로 변환할 지 모르는 경우를 말한다.
Example 58.의 PeriodStringConverter에서 Period를 String 자바 타입으로 변환하는 것으로 명세하고 있는데.
String 자바 타입은 VARCHAR 타입으로 변환해야하는 것을 하이버네이트는 알고 있다.
(String 타입은 하이버네이트가 제공하는 기본 타입에 속해있기 때문이다.)
그런데 String 타입이 아닌 하이버네이트가 어떤 데이터베이스 타입으로 변환하는지 모르는 타입일 때
문제가 된다는 것이다.
그 때는 새로운 JavaTypeDescriptor를 정의해서 알려지지 않은 타입을 JavaTypeDescriptorRegistry에 추가해야한다.
만일 자바 타입이 하이버네이트에 알려지지 않았다면, 아래 메시지를 보게될 것이다.
"HHH000481: JavaTypeDescriptor를 찾을 수 없고, equals와 hashcode를 구현하지 않은 자바타입이 발견되었습니다.
이 자바 타입은 동등성 확인 / 더티 체킹을 수행할 때 중요한 실행 문제를 야기할 수 있습니다. 사용자 정의 JavaTypeDescriptor나 적어도 equals/hashcode를 구현하는 것을 고려하세요."
자바 타입이 알려졌다는 의미는 JavaTypeDescriptorRegistry에 해당 타입이 등록되어 있다는 의미이다.
하이버네이트는 기본적으로 많은 JDK 타입들을 JavaTypeDescriptorRegistry에서 가져오는데, 애플리케이션은 새로운 JavaTypeDescriptor 항목들을 추가함으로서 JavaTypeDescriptorRegistry를 확장할 수 있다.
이 방법으로, 하이버네이트는 JDBC 레벨에서 특정 자바 객체 타입을 다루는하는 방법을 알 수 있을 것이다.
원문
In cases when the Java type specified for the "database side" of the conversion (the second AttributeConverter bind parameter) is not known, Hibernate will fallback to a java.io.Serializable type.
If the Java type is not known to Hibernate, you will encounter the following message:
HHH000481: Encountered Java type for which we could not locate a JavaTypeDescriptor and which does not appear to implement equals and/or hashCode. This can lead to significant performance problems when performing equality/dirty checking involving this Java type. Consider registering a custom JavaTypeDescriptor or at least implementing equals/hashCode.
Whether a Java type is "known" means it has an entry in the JavaTypeDescriptorRegistry. While by default Hibernate loads many JDK types into the JavaTypeDescriptorRegistry, an application can also expand the JavaTypeDescriptorRegistry by adding new JavaTypeDescriptor entries.
This way, Hibernate will also know how to handle a specific Java Object type at the JDBC level.
JPA 2.1 AttributeConverter Mutability Plan
JPA AttributeConverter에 의해 전환되는 하이버네이트 기본 타입은 기본 자바 타입이 불변하다면 불변하다.
역시 기본 자바타입이 변경 가능하다면 연관된 엔티티 속성 타입도 변경 가능하다.
그러므로, 가변성(변경 가능성)은 연관된 엔티티 속성 타입의 JavaTypeDescriptor#getMutabilityPlan로부터 주어진다.
원문
A basic type that’s converted by a JPA AttributeConverter is immutable if the underlying Java type is immutable and is mutable if the associated attribute type is mutable as well.
Therefore, mutability is given by the JavaTypeDescriptor#getMutabilityPlan of the associated entity attribute type.
Immutable types
만일 엔티티 속성이 String 타입, 자바 래퍼 타입 (즉 Integer, Long 등), enum 타입이거나 어떤 불변한 객체 타입이면, 당신은 새로운 값으로 값을 재할당하는 방법으로만 엔티티 속성값을 변경할 수 있습니다.
JPA 2.1 AttributeConverters 섹션에서 보았던 엔티티 속성과 같은 Period이 있다고 가정해보자.
원문
If the entity attribute is a String, a primitive wrapper (e.g. Integer, Long) an Enum type, or any other immutable Object type, then you can only change the entity attribute value by reassigning it to a new value.
Considering we have the same Period entity attribute as illustrated in the JPA 2.1 AttributeConverters section:
@Entity(name = "Event")
public static class Event {
@Id
@GeneratedValue
private Long id;
@Convert(converter = PeriodStringConverter.class)
@Column(columnDefinition = "")
private Period span;
//Getters and setters are omitted for brevity
}
아래 코드와 같이 span 속성을 변화시키는 유일한 방법은 다른 값을 재할당하는 것이다.
(The only way to change the span attribute is to reassign it to a different value:)
Event event = entityManager.createQuery( "from Event", Event.class ).getSingleResult();
event.setSpan(Period
.ofYears( 3 )
.plusMonths( 2 )
.plusDays( 1 )
);
Mutable types
반면에 아래 예제의 Money 타입이 변경 가능하다고 가정해보자.
(On the other hand, consider the following example where the Money type is a mutable.)
public static class Money {
private long cents;
//Getters and setters are omitted for brevity
}
@Entity(name = "Account")
public static class Account {
@Id
private Long id;
private String owner;
@Convert(converter = MoneyConverter.class)
private Money balance;
//Getters and setters are omitted for brevity
}
public static class MoneyConverter
implements AttributeConverter<Money, Long> {
@Override
public Long convertToDatabaseColumn(Money attribute) {
return attribute == null ? null : attribute.getCents();
}
@Override
public Money convertToEntityAttribute(Long dbData) {
return dbData == null ? null : new Money( dbData );
}
}
변경가능한 객체는 당신이 내부 구조를 수정하는 것을 허용하며, 하이버네이트 더티 체킹 매커니즘이 데이터베이스에 변경 전파를 할 것이다.
(A mutable Object allows you to modify its internal structure, and Hibernate dirty checking mechanism is going to propagate the change to the database:)
Account account = entityManager.find( Account.class, 1L );
account.getBalance().setCents( 150 * 100L );
entityManager.persist( account );
비록 AttributeConverter 타입들이 변경가능하게 될 수 있어서 더티 체킹, 깊은 복사, 2차 캐시이 적절하게 동작할 수 있지만, 이 동작들은 불변하도록 처리할 때 더욱 효과적이다.
이런 이유들로 가능하다면 변경 가능한 타입보다 불변한 타입들을 선호하라.
원문
Although the AttributeConverter types can be mutable so that dirty checking, deep copying, and second-level caching work properly, treating these as immutable (when they really are) is more efficient.
For this reason, prefer immutable types over mutable ones whenever possible.
다음 글은 필자가 spring boot + kotlin 환경에서 AttributeConverters를 테스트해본 내용을 소개하겠다.
'백엔드 개발 > JPA' 카테고리의 다른 글
[하이버네이트 유저 가이드 파헤치기] Generated properties - 2.3.18. (0) | 2021.10.23 |
---|---|
[하이버네이트 유저 가이드 파헤치기] JPA 2.1 AttributeConverters 실습 (0) | 2021.10.11 |
[하이버네이트 유저 가이드 파헤치기] BasicTypeRegistry & Explicit BasicTypes - 2.3.4, 2.3.5 (0) | 2021.09.22 |
[하이버네이트 유저 가이드 파헤치기] @Basic 어노테이션 & @Column 어노테이션 - 2.3.1 ~ 2.3.3 (0) | 2021.08.29 |
[하이버네이트 유저 가이드 파헤치기] 매핑 타입 - 2.1 (2) | 2021.08.11 |