Programming/Java

Java Enum 활용하기2 - ConverterFactory

Jan92 2022. 10. 8. 13:42

ConverterFactory<S, R>

Java Enum을 활용한 방법 중 두 번째,

ConverterFactory 인터페이스를 구현한 CustomConverter를 만들어 HTTP 요청에서 Enum 값을 받을 때 Enum 값 자체를 받는 것이 아니라 legacyCode를 받아 자동으로 Enum으로 형 변환하는 방법입니다.

 

 

@GetMapping("/user/{idx}")
public ResponseEntity<?> getUserDetail(@PathVariable Long idx) {
    
    ...
}

스프링에서 HTTP Query String을 통해 전달되는 정보는 모두 문자열(String)로 인식됩니다.

(HTTP 요청 파라미터는 모두 문자열로 처리)

 

하지만 위 예시와 같이 컨트롤러에서 Long (또는 Integer, boolean Enum 등)으로 해당 값을 전달받을 수도 있는데요.

이처럼 형 변환이 되기 위해서는 매개변수를 처리하는 과정에서 자동으로 형 변환 기능이 적용되어야 하는데, 그 역할을 하는 것이 바로 스프링 타입 컨버터(Type Converter)입니다.

 

Spring은 문자, 숫자 boolean, Enum 등 일반적인 타입에 대해 대부분의 컨버터를 기본적으로 제공하며, 요청이 왔을 때 Controller 단에서 해당 타입에 따른 변환을 자동으로 지원하는데요.

(@ReqestParam, @ModelAttribute, @PathVariable 어노테이션 대상)

 

하지만 Spring에서 기본적으로 자동 변환을 지원하는 타입이 아닌 경우에도 요청되는 값을 원하는 객체나 특정 타입으로 변환해서 받고 싶은 경우가 있습니다. 그럴 때 CustomConverter 등록을 통해 해당 기능을 적용할 수 있으며, 스프링에서는 CustomConverter를 구현할 때 사용할 수 있는 3가지 인터페이스 Converter, ConverterFactory, GenericConverter를 제공합니다.

 

 

 

Converter, ConverterFactory, GenericConverter(간단한 설명)

1. Converter - 기본 타입에 대한 변환에 사용

 

2. ConverterFactory - 전체 클래스 계층에 대한 변환 로직을 구현할 때 사용

(클래스 계층으로 묶을 수 있는 java.lang.Number 또는 java.lang.Enum과 같은 타입 변환 로직을 한 곳에서 관리하고 싶은 경우에 ConverterFactory를 사용할 수 있습니다.)

 

3. GenericConverter - 두 가지 이상의 타입 변환이 필요할 때 사용

(예를 들면 Integer, Double 또는 String을 BigDecimal 값으로 변환할 때 각각 타입에 따른 3개의 Converter를 작성할 필요 없이 GenericConverter를 통해 하나로 해결할 수 있습니다. 이처럼 여러 개의 소스 및 대상 타입을 지정할 수 있기 때문에 유연하지만 구현하기 어렵고 복잡하기 때문에 사용이 권장되지는 않습니다.)

 

 

이어지는 내용을 통해서 Enum에서 주로 사용되는 ConverterFactory를 Enum 변환에 활용하는 방법을 살펴보겠습니다.

 

 


 

 

ConverterFactory 활용 방법

@Getter
public enum UserState implements EnumMapperType {

    NORMAL("정상", 1),
    SUSPENSION("정지", 2),
    WITHDRAWAL("탈퇴", 3);

    private String desc;
    private Integer legacyCode;

    UserState(String desc, Integer legacyCode) {
        this.desc = desc;
        this.legacyCode =legacyCode;
    }
    
    @Override
    public String getCode() { return name(); }
	
    ...
}

(사용자의 상태 값을 나타내는 Enum인 UserState)

 

이어지는 내용은 HTTP 요청을 받을 때 UserState에 대한 값을 NORMAL, SUSPENDSION, WITHDRAWAL으로 받는 것이 아니라 legacyCode인 1, 2, 3으로 받아도 해당되는 Enum 값으로 매핑시키기 위한 작업입니다.

 

 

public interface EnumMapperType {
    String getCode();
    String getDesc();
    Integer getLegacyCode();
}

(공통으로 사용되는 getter 메서드를 구현하기 위한 EnumMapperType 인터페이스)

 

 

ConverterFactory interface

org.springframework.core.convert.converter 패키지에 포함된 ConverterFactory interface입니다.

여기서 S는 변환하기 전의 타입이며, R은 변환할 클래스의 범위를 정의하는 기본 타입입니다. T는 R의 하위 클래스입니다.

 

 

public class StringToEnumConverterFactory implements ConverterFactory<String, Enum<? extends EnumMapperType>> {

    @Override
    public <T extends Enum<? extends EnumMapperType>> Converter<String, T> getConverter(Class<T> targetType) {
        if (EnumMapperType.class.isAssignableFrom(targetType)) {
            return new LegacyCodeToEnumConverter<>(targetType);
        } else {
            return null;
        }
    }

    private static final class LegacyCodeToEnumConverter<T extends Enum<? extends EnumMapperType>> implements Converter<String, T> {

        private final Map<Integer, T> map;

        public LegacyCodeToEnumConverter(Class<T> targetEnum) {
            T[] enumConstants = targetEnum.getEnumConstants();
            map = Arrays.stream(enumConstants)
                    .collect(Collectors.toMap(enumConstant -> ((EnumMapperType) enumConstant).getLegacyCode(), Function.identity()));
        }

        @Override
        public T convert(String source) {
            //해당 값 존재 여부 확인
            if (!StringUtils.hasText(source)) {
                return null;
            }
            
            //String to Integer
            Integer legacyCode = null;
            try {
                legacyCode = Integer.parseInt(source);
            } catch (Exception e) {
                // 변환 실패시 Exception 처리
                throw new IllegalArgumentException("IllegalArgumentException");
            }

            //해당 값 map 에서 추출
            T enumValue = map.get(legacyCode);
            //해당 값이 map 에 존재하지 않을 경우 Exception 처리
            if (enumValue == null) {
                throw new IllegalArgumentException("IllegalArgumentException");
            }
            return enumValue;
        }
    }
}

변환하기 전 String 타입을 EnumMapperType 인터페이스를 상속받은 Enum 타입으로 변환하기 위한 ConverterFactory이며, 코드를 따라가 보면 내부적으로 Converter interface를 구현하여 convert() 메서드를 통해 변환 작업이 이뤄지는데요.

 

convert() 메서드를 통해 String으로 들어온 legacyCode 값을 Integer로 변환하고 해당 값에 매핑되는 Enum 값을 찾는 과정이 진행됩니다.

 

 

@Configuration
public class WebConfiguration implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        ConverterFactory converterFactory = new StringToEnumConverterFactory();
        registry.addConverterFactory(converterFactory);
    }
}

그리고 최종적으로 WebMvcConfigurer 인터페이스를 구현한 WebConfiguration 클래스에서 addFormatters() 메서드를 통해 위에서 생성한 CustomConverterFactory를 등록시켜주면 HTTP 요청 시 해당 컨버터 팩토리가 자동으로 적용이 되는데요.

 

 

 

legacyCode 값으로 요청

(userState에 대한 value를 legacyCode 값으로 요청)

 

 

UserState Mapping

(UserState에 대한 값이 정상적으로 매핑됨)

 

 

 

***

만약 이렇게 커스텀 컨버터 팩토리를 등록한 상황에서 Enum에 대한 값으로 legacyCode가 아닌 기존과 같은 실제 Enum 값이 들어오면 어떻게 될까요?

 

결론을 먼저 말씀드리면, 해당되는 Enum이 정상적으로 매핑됩니다.

 

코드를 작성하며 예상한 결과로는 convert() 메서드의 Integer 변환 부분에서 Exception이 발생하여 매핑이 되지 않을 것이라고 생각했는데요.

실제로 해당 부분에서 Exception은 발생합니다. 하지만 convert() 내부에서 발생된 Exception은 ConversionUtils 추상 클래스에서 잡히며, 내부적인 처리 과정을 통해 변환 가능한 다른 클래스를 찾게 되는데요. (TypeConverterDelegate 클래스 내부적으로 처리)

이때 attemptToConvertStringToEnum 클래스에서 해당 과정이 처리되며 Enum이 정상적으로 매핑되게 되는 것입니다.

 

 

이 기능 역시 아래 'Java Enum활용1 - AttributeConverter'과 마찬가지로 꼭 필요한 기능은 아니지만 필요시 유용하게 사용할 수 있는 기능입니다.

 

 

(참고자료 및 코드를 참고할 수 있는 github 주소는 아래에 남겨져 있으며, 잘못된 내용이나 로직상 더 좋은 방법이 있으면 댓글 남겨주시면 확인하겠습니다. 감사합니다.)

 

 

 

< 함께 보면 좋은 글 >

 

Java Enum 활용하기1 - AttributeConverter

Java Enum을 활용한 방법 중 첫 번째, AttributeConverter 인터페이스를 구현한 CustomConverter를 만들어 DB에는 Enum의 legacyCode 값으로 데이터를 저장하고, Java에서는 DB에 legacyCode 값으로 저장된 데이..

wildeveloperetrain.tistory.com

 

< github 주소 >

 

GitHub - JianChoi-Kor/SideToon: side_toon_project

side_toon_project. Contribute to JianChoi-Kor/SideToon development by creating an account on GitHub.

github.com

 

< 참고 자료 >

 

Guide to Spring Type Conversions | Baeldung

Take a look at built-in type converters in Spring and how to add custom ones.

www.baeldung.com