Study/Java

enum class mapping Error

짱이08 2024. 12. 10. 11:10

발단

enum class 로 request요청 시 enum의 code로 매핑하고 싶으나, code 가 아닌 name으로 매핑됨

→ 따라서 400 에러 발생

기존 코드

 @GetMapping("/year-carbon")
    public ApiResponse<?> getYearsCarbon(@ModelAttribute DashboardRequest request) {

        //type 에 따라 서비스 구분
        if (request.getType().equals(FeatureType.road)) {

        }else {
            buildingService.getYearsBuildingCarbon(request.getScenario(), request.getDong());
        }
        return ApiResponse.ok(null);
    }

GetMapping 이므로 query String 형태로 날라오기 때문에 requsetBody 말고 RequestParam을 쓰려 했으나, @ModelAttribute 에서 자동으로 일치하는 필드를 찾아 매핑하기 때문에 @ModelAttribute를 사용

코드로 요청을 위해서는 converter 작성 필요하므로 CodeEnumCoverter Class 와 ConvertConfig 클래스를 생성하여 컨트롤러 실행 후 request에 매핑 할때 convert를 타게 설정

public interface BaseEnum {
    String getCode();
}
@AllArgsConstructor
public class CodeEnumConverter<T extends Enum<T> & BaseEnum> implements Converter<String, T> {

    private final Class<T> enumType;

    @Override
    public T convert(String source) {
        System.out.println("CodeEnumConverter called with source: " + source);
        for (T constant : enumType.getEnumConstants()) {
            if (constant.getCode().equals(source)) {
                return constant;
            }
        }
        throw new IllegalArgumentException("Unknown code: " + source + " for enum " + enumType.getSimpleName());
    }
}
@Slf4j
@Configuration
public class ConvertConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        CodeEnumConverter<FeatureType> converter = new CodeEnumConverter<>(FeatureType.class);
        registry.addConverter(converter);
        log.info("Registered converter {}", converter);
    }
}

제네릭 타입은 타입안정성을 위한 역할을 하고 실제 런타임에서는 없어지기 때문에 private final Class<T> enumType; 에서 에러 발생

CodeEnumConverter 클래스에서는 생성자로 Class<T> 가 필요한데, 제네릭 타입의 경우 런타임시 제거 되므로 동적으로 타입을 지정 되지 않는다. @Component 는 Spring에서 자동으로 의존성을 주입하고 관리하기 때문에 명시적으로 클래스를 지정 해 줘야 하므로 @Component를 붙일 수 없고, 타입이 명시 될 시점에서 Bean으로 등록되어야 한다.

→ 해결

: ConvertConfig를 빈으로 등록하는데,

CodeEnumConverter<FeatureType> converter = new CodeEnumConverter<>(FeatureType.class); registry.addConverter(converter);

여기서 명시적으로 FeatureType 을 타입으로 지정해 주기 때문에 해당 시점에서 @Configuration 어노테이션을 통해 빈으로 등록하면, 명시적으로 CodeEnumConverter가 FeatureType을 생성자로 하여 빈에 등록된다.


RequestBody 와 RequestParam의 다른 매커니즘

RequestBody에서 받아온 Json 데이터의 경우 jackson으로 직렬화/역직렬화가 가능하다.

→ Jackson은 Json 바디나 응답을 처리할 때 사용된다

CodeEnumSerializer 와 CodeEnumDeserializer 의 경우 Jackson에서 Json 데이터를 직렬화/역직렬화 하는 데 사용되는데(StdSerializer를 상속받아 구현)

@RequestParam 은 Json 이 아니라 쿼리스트링으로 처리하기 때문에 변환할때는 Spring에서 Converter 또는 Formatter 를 사용한다.

따라서 Jackson 설정은 쿼리슽트링에서는 영향을 미치지 못한다.

SpringMVC에서는 Enum을 변환 할 경우 기본적을 Enum.valueOf() 를 사용한다.

 public static <T extends Enum<T>> T valueOf(Class<T> enumClass,
                                                String name) {
        T result = enumClass.enumConstantDirectory().get(name);
        if (result != null)
            return result;
        if (name == null)
            throw new NullPointerException("Name is null");
        throw new IllegalArgumentException(
            "No enum constant " + enumClass.getCanonicalName() + "." + name);
    }

Java.lang에 구현되어 있는 valueOf 메서드

여기서 확인 해보면 Enum의 name과 매핑되는 것을 확인 할 수 있다.

코드를 받기 위해서는 코드를 받는 메서드를 사용하여 명시적으로 converter에 등록해야 한다.

static <T extends CodeEnum> T ofCode(Class<T> codeEnumClass, String code) {
        return Arrays.stream(codeEnumClass.getEnumConstants())
                .filter(it -> it.getCode().equals(code))
                .findAny()
                .orElseThrow(() -> new IllegalArgumentException("코드 변환 실패"));
    }

위는 코드로 요청을 할 경우 해당 코드와 일치하는 Enum을 찾아 변환시켜주는 ofCode 메서드이다.

해당 코드를 codeEnum으로 작성하고 이 Enum타입을 상속받아 사용한다면 상속받은 클래스들은 모드 ofCode를 사용 할 수 있다. 따라서 직접 구현하지 않고 공통 처리를 할 수있다.

쿼리스트링 형태로 받을 경우 직접 converter를 설정하는 것에 대해서 알아본다면

@Component
public class StringToFeatureTypeConverter implements Converter<String, FeatureType> {

    @Override
    public FeatureType convert(String source) {
         return CodeEnum.ofCode(FeatureType.class, source); // 코드 값 매핑이 필요한 경우
    }
}

위와 같이 StringToFeatureTypeConverter를 빈으로 등록하여 서비스가 실행될 때 StringToFeatureTypeConverter를 실행시킨다.