enum class mapping Error
발단
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를 실행시킨다.