지니님이 기본 데이터 타입과 String 등을 enum 으로 바꾸는 작업을 진행하셨다.
대부분은 아래와 같이 @Enumerated 으로 enum의 이름을 사용하도록 작업을 했다.
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
@Entity
@Table(name = "TEST")
@Getter
@Setter
public class TestEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "SEQ")
private Integer id;
@Column(name = "ACTION_TYPE", length = 100)
@Enumerated(value = EnumType.STRING) // ORDINAL 과 STRING 두 가지가 있다.
private ActionType actionType;
문제는 enum에서 매핑하고 있는 특정 코드로 매핑되는 부분이 있어서 위와 같은 방법 대신 커스텀 컨버터를 만들어서 적용을 하셨다.
@Entity
@Table(name = "TEST")
@Getter
@Setter
public class TestEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "SEQ")
private Integer id;
@Column(name = "COMMAND", length = 10)
@Convert(converter = CommandTypeConverter.class)
private CommandType commandType;
}
@Converter 는 어떻게 동작할까?
프레임워크를 사용하다보면 어떤 동작을 할지는 개발자가 정하지만 수행이 되도록 하는 것은 숨겨져있다.
프레임워크에 책임을 위임하기 때문이다.
가끔 프레임워크가 하는 일을 살펴보면 오픈소스 코드를 볼 수 있는 기회도 되고, 개발시 도움이 되는 아이디어를 얻기도 한다.
가장 파악하기 쉬운 방법으로는 통합 테스트 코드를 작성해서 커스텀 컨버터에 break를 걸어놓고 프레임워크가 물기를 기다리는 것이다. (낚시하는 것 같다.)
물린 결과는 아래와 같다.
Spring JPA를 사용해서 일단 CrudRepository 의 findById 가 호출된 것이 보인다.
아래의 빨간줄은 CrudRepository 인터페이스에 매핑된 프록시이고 위의 빨간줄이 실제 구현체인 SimpleJpaRepository이다.
스프링 JPA의 SimpleJpaRepository 는 내부적으로 JPA의 EntityManager 의 find를 호출하게 된다.
package org.springframework.data.jpa.repository.support;
@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
private final EntityManager em;
@Override
public Optional<T> findById(ID id) {
Class<T> domainType = getDomainClass();
if (metadata == null) {
return Optional.ofNullable(em.find(domainType, id));
}
LockModeType type = metadata.getLockModeType();
Map<String, Object> hints = getQueryHints().withFetchGraphs(em).asMap();
return Optional.ofNullable(type == null ? em.find(domainType, id, hints) : em.find(domainType, id, type, hints));
}
위의 호출 스택을 위로 올라가보면 JPA의 구현체로 하이버네이트가 사용되었음을 알 수 있다.
사실 말입니다.
위에서는 아래에서 위로 bottom-up으로 진행했는데 @Converter 는 어떻게 동작할까?를 보려면 top-down으로 진행하는 것이 더 유용하다. (테스트를 하면서 글을 쓰다보니 두서없이 진행되고 있음)
JpaAttributeConverter 인터페이스와 구현체
JpaAttributeConverterImpl 는 JpaAttributeConverter 인터페이스의 표준 구현체이다.
attributeConverterBean 이라는 멤버변수에 AttributeConverter인터페이스를 구현한 커스텀 컨버터들을 가지고 있다가
경우에 따라서 관계형 값을 도메인 값으로, 도메인 값을 관계형 값으로 변환을 해준다.
package org.hibernate.metamodel.model.convert.internal;
public class JpaAttributeConverterImpl<O,R> implements JpaAttributeConverter<O,R> {
private final ManagedBean<AttributeConverter<O,R>> attributeConverterBean;
// ..
@Override
public O toDomainValue(R relationalForm) {
return attributeConverterBean.getBeanInstance().convertToEntityAttribute( relationalForm );
}
@Override
public R toRelationalValue(O domainForm) {
return attributeConverterBean.getBeanInstance().convertToDatabaseColumn( domainForm );
}
이 변환은 값을 추출하는 일을 맡고 있는 ValueExtractor 인터페이스의 extract 과정 안에서 수행된다.
package org.hibernate.type.descriptor;
public interface ValueExtractor<X> {
public X extract(ResultSet rs, String name, WrapperOptions options) throws SQLException;
public X extract(CallableStatement statement, int index, WrapperOptions options) throws SQLException;
public X extract(CallableStatement statement, String[] paramNames, WrapperOptions options) throws SQLException;
}
구현체는 별도로 없이 AttributeConverterSqlTypeDescriptorAdapter 클래스의 익명클래스에 의해 수행되는데 코드의 일부는 아래와 같다.
package org.hibernate.type.descriptor.converter;
public class AttributeConverterSqlTypeDescriptorAdapter implements SqlTypeDescriptor {
// ..
@Override
public <X> ValueExtractor<X> getExtractor(JavaTypeDescriptor<X> javaTypeDescriptor) {
// Get the extractor for the intermediate type representation
final ValueExtractor realExtractor = delegate.getExtractor( intermediateJavaTypeDescriptor );
return new ValueExtractor<X>() {
@Override
public X extract(ResultSet rs, String name, WrapperOptions options) throws SQLException {
return doConversion( realExtractor.extract( rs, name, options ) );
}
@Override
public X extract(CallableStatement statement, int index, WrapperOptions options) throws SQLException {
return doConversion( realExtractor.extract( statement, index, options ) );
}
@Override
public X extract(CallableStatement statement, String[] paramNames, WrapperOptions options) throws SQLException {
if ( paramNames.length > 1 ) {
throw new IllegalArgumentException( "Basic value extraction cannot handle multiple output parameters" );
}
return doConversion( realExtractor.extract( statement, paramNames, options ) );
}
@SuppressWarnings("unchecked")
private X doConversion(Object extractedValue) {
try {
X convertedValue = (X) converter.toDomainValue( extractedValue );
log.debugf( "Converted value on extraction: %s -> %s", extractedValue, convertedValue );
return convertedValue;
}
catch (PersistenceException pe) {
throw pe;
}
catch (RuntimeException re) {
throw new PersistenceException( "Error attempting to apply AttributeConverter", re );
}
}
};
제일 아래에 doConversion에 보면 converter.toDomainValue 부분이 있다.
위의 경우에는 DB -> Object 로 가는 경우라서 ValueExtractor가 사용되는데
반대로 Object -> DB 로 가는 경우라면 ValueBinder 에 의하여 converter.toRelationalValue 가 수행된다.
'Programing > Framework' 카테고리의 다른 글
[JOOQ] MySQL JDBC batch 벤치마킹 (0) | 2020.10.13 |
---|---|
[Spring MVC] HandlerMethodArgumentResolver 구현하기 (0) | 2020.09.18 |
[스프링] 생성자가 private 일때 스프링은 객체는 어떻게 만들까? (0) | 2020.08.25 |
[Spring] FieldError의 계층구조 (0) | 2020.08.20 |
[JPA] H2 테스트 코드에서 createdAt 필드가 없던 이유 (0) | 2020.08.20 |