JPA Entity 클래스에 Enum 타입 사용기 AttributeConverter 활용(응용)

지난 포스팅 : https://sas-study.tistory.com/415

 

지난번 포스팅에 이어서 AttributeConverter를 좀더 응용해서 개발하는 방법을 공유하고자 합니다.

 

우선 지난번 포스팅에서는 Entity 클래스에 선언된 Enum 타입의 갯수마다 AttributeConverter를 구현하여 각각 XXConverter 클래스를 생성하고 관리해주어야하는 단점이 존재했습니다. 

 

우선 그 점에 있어서는 Entity 클래스의 멤버로 활용중인 Enum 타입을 하나로 묶을 상위 인터페이스가 필요하다고 생각했습니다.

 

코드로 보여드리자면

public interface EntityEnumerable {

    String getType();

    String getName();
}

public enum StudyType implements EntityEnumerable {
    ONLINE("ONLINE", "온라인"),
    OFFLINE("OFFLINE", "오프라인");
    
    private String type;
    private String name;

    // + type getter
    // + name getter
}

 

이런식으로 Entity 내부 클래스에서 활용할 Enum들을 EntityEnumerable 이라는 인터페이스를 구현하도록 합니다.(모든 Enum이 구현해도 되지만 현재는 Entity 클래스에 활용되는 목적이 있기 때문에..)

 

StudyType의 getType 메소드를 EntityEnumerable의 getType으로 호출할 수 있게 되었습니다. (오버라이딩)

 

Enum들이 EntityEnumerable 타입으로 캐스팅될 수 있기 때문에 EntityEnumerableConverter를 구현해주면...? -> (뭔가 방향을 잡았다고 생각함...)

 

EntityEnumerableConverter
@Converter
public abstract class EntityEnumerableConverter 
				implements AttributeConverter<EntityEnumerable, String> {

    @Override
    public String convertToDatabaseColumn(EntityEnumerable attribute) {
        if (Objects.isNull(attribute)) {
            return null;
        }
        return attribute.getType();
    }

    @Override
    public EntityEnumerable convertToEntityAttribute(String dbData) {
        if (StringUtils.isBlank(dbData)) {
            return null;
        }
        return ...???
   }
}

convertToDatabaseColumn을 구현하면서까지는 신이났지만... convertToEntityAttributes 메소드를 구현할 때, 쿵... 하고 벽을 만난 것 같았다.

 

도무지 저 dbData로 넘어온 String에서 내가 원하는 타입인 StudyType Enum 타입을 어떻게 딱 맞춰서 가져올 수 있을까....??? 라는 생각에 빠졌다.. 

 

그러던 도중 제네릭으로 Type Parameter를 넘기듯이 할 수 있을까?? 하고 조금 생각에 잠기고 폭풍 구글링 및 레퍼런스를 뒤지기 시작했습니다. 분명히 이 정도 고민은 내가 아니라 다른 누군가도 했을 것이라고 생각했어요. 

 

제가 참조한 레퍼런스는 아래의 레퍼런스입니다.

참조 레퍼런스 : https://gist.github.com/iampaul83/f23c913801bf329ff5ed4435d04fec86

 

Spring Data JPA Enum converter

Spring Data JPA Enum converter. GitHub Gist: instantly share code, notes, and snippets.

gist.github.com


일단 해당 내용을 정리하기 위해서는 간단히라도 @Converter 어노테이션이 어떤식으로 동작하는지 확인해야 했습니다.

 

@Converter 어노테이션을 선언한 클래스는 모두 MetadataBuildingProcess 라는 클래스 내의 prepare 메소드에서 scannig 됩니다. 

public static ManagedResources prepare(final MetadataSources sources,
                                       final BootstrapContext bootstrapContext) {
    final ManagedResourcesImpl managedResources = ManagedResourcesImpl.baseline( sources, bootstrapContext );
	final ConfigurationService configService = bootstrapContext.getServiceRegistry().getService( ConfigurationService.class );
	final boolean xmlMappingEnabled = configService.getSetting(
        AvailableSettings.XML_MAPPING_ENABLED,
		StandardConverters.BOOLEAN,
		true
    );
	ScanningCoordinator.INSTANCE.coordinateScan(
	    managedResources,
		bootstrapContext,
		xmlMappingEnabled ? sources.getXmlMappingBinderAccess() : null
    );
	return managedResources;
}

저 ManagedResources 라는 객체에 @Entity, @Converter, @Embeddable, @MappedSuperClass 어노테이션이사용된 클래스들을 담습니다. (실제로 담는 부분은 coordinateScan 이라는 메소드 내부에서 매우 복잡한 처리과정을 거치니 관심 있으신 분들은 한번 ManagedResources 클래스를 찾아서 보시면 좋을 것 같습니다.) 해당 클래스들의 정보중 @Converter 어노테이션을 AttributeConverterManager 라는 클래스의 내부 변수로 담고 있습니다.

private Map<Class, ConverterDescriptor> attributeConverterDescriptorsByClass;

실제로 스프링이 구동되면서 AttributeConverter를 상속받은 클래스의 인스턴스를 생성하고 있습니다.

 

이후 JPA 에서 JpaAttributeConverter 라는 객체가 해당 컨버터 데이터를 가져다가 사용하고 있습니다.

// JpaAttributeConverter 내부 변수
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 );
}

즉 Bean처럼 관리되는 객체에 해당 컨버터의 인스턴스들이 담겨있고 특정 Enum 타입에 적용된 컨버터가 있다면 저장된 객체에서 인스턴스를 불러와서 처리하고 있습니다.

 


이제 실제로 적용을 해보도록 하겠습니다.

 

EntityEnumerableConverter를 다음과 같이 제네릭형태로 구현합니다.

@Converter
public abstract class EntityEnumerableConverter<T extends EntityEnumerable> implements AttributeConverter<T, String> {

    private final Class<T> clazz;

    public EntityEnumerableConverter(Class<T> clazz) {
        this.clazz = clazz;
    }

    @Override
    public String convertToDatabaseColumn(T attribute) {
        if (Objects.isNull(attribute)) {
            return null;
        }
        return attribute.getType();
    }

    @Override
    public T convertToEntityAttribute(String dbData) {
        if (StringUtils.isBlank(dbData)) {
            return null;
        }
        T[] enumConstants = clazz.getEnumConstants();
        for (T constant : enumConstants) {
            if (StringUtils.equals(constant.getType(), dbData)) {
                return constant;
            }
        }

        throw new UnsupportedOperationException(String.format("\'%s\' 지원하지 않는 enum 형식입니다."));
   }
}

 

 

EntityEnumerableConverter의 인스턴스를 생성할 때 Class<T> 타입의 객체를 받도록 하였습니다. 그럼 직접적으로 해당 생성자를 호출해야 할텐데요. 이를 각 Enum을 구현할때 별도의 내부 Converter 클래스를 만들도록 하였습니다.

 

public enum StudyType implements EntityEnumerable {
    ONLINE("ONLINE", "온라인"),
    OFFLINE("OFFLINE", "오프라인");
    
    private String type;
    private String name;

    // + type getter
    // + name getter
    
    @javax.persistence.Converter
    public static class Converter extends EntityEnumerableConverter<StudyType> {
        public Converter() {
            super(StudyType.class);
        }
    }
}

내부적으로 Converter를 강제하고 Entity에 사용되는 Enum이라면 EntityEnumerableConverter 구현한다면 위의 로직에서 @Converter 어노테이션을 스프링이 스캔하고 해당 타입(StudyType.class)을 EntityEnumerableConverter의 생성자(super사용)로 타입 파라미터를 전달하게 됩니다. 따라서 clazz 변수에 StudyType.class 타입의 값이 담기게 되고 해당 StudyType의 constants에 접근이 가능하니 EntityEnumConverter의 convertToEntityAttribute 메소드를 구현할 수 있게 되었습니다.!!

 

 


결론

Enum 클래스 내부적으로 Converter 클래스를 별도로 만들어줘야되는 한계는 외부에 클래스를 만드는 것과 큰 차이는 없다고 생각되지만 Enum을 생성할 때, 팀 내부적으로 공유가 되고 원리만 어느정도 이해가 된다면 관리 측면에서는 오히려 이 부분이 좀더 괜찮다고 생각이 듭니다.

Enum을 이용해서 공통화를 직접 스터디한 적은 처음이었는데 이번 기회에 프레임워크와 라이브러리와의 코드적인 복잡성을 한번더 확인해보았던것 같습니다.

아직까지는 모든 AttributeConverter를 스프링과 하이버네이트가 어떤식으로 가져다가 처리하게 되는지 정확한 프로세스까지는 확인을 못했지만 스프링에 대한 이해를 좀더 쌓고 다시 보게 된다면 전체 프로세스를 훑어볼수 있지 않을까??

일단 이해까지는 못했더라도 레퍼런스를 찾고 내가 원하는 개발방식을 구현해볼 수 있었다는 점에 성공을.. 모든 프로세스를 다 파악하지 못했다는 점에서 실패를 맛보았던 것 같다.

댓글

Designed by JB FACTORY