이전 포스트에서 요청/응답 Body에 포함된 Legacy Enum들을 인터페이스를 이용해 다뤘던 방법을 작성하였다.
아쉽게도, '개발 한 스푼' 서비스에서는 쿼리 파라미터에도 Enum 스타일의 소문자 문자열들이 사용되고 있다. Kotlin에서는 Enum값을 대문자로 작성하는 것을 권장하기 때문에 이에 바인딩하는 작업을 커스텀해야한다.
이번 글에서는 RequestParam, ModelAttribute에 포함된 다수의 Legacy Enum들을 효율적으로 관리하기 위해 적용한 방식을 공유해본다.
본 포스트는 다음과 같은 수순으로 진행됩니다.
1. 기존 Converter 톺아보기
2. 구현 방향성과 방법
1. 기존 Converter 톺아보기
@RequestParam과 @ModelAttribute 를 통해 쿼리 정보들을 받을 수 있다. 이때 우리는 각 필드에 String, Int와 같이 타입을 지정하는데 재밌게도 알아서 쿼리값들이 해당 타입으로 변환되어 주입된다.
이러한 동작이 가능한 이유는 WebMvc에서 Converter를 이용해 바인딩(DataBinder 이용) 작업이 이루어지기 때문이다. 내부적으로 가지고 있는 Converter 중 필드에 할당된 타입으로 전환시킬 수 있는 것을 골라 사용한다.
Enum으로 변환해주는 기본적인 Converter는 StringToEnumConverterFactory로 다음과 같이 구현되어있다.
final class StringToEnumConverterFactory implements ConverterFactory<String, Enum> {
@Override
public <T extends Enum> Converter<String, T> getConverter(Class<T> targetType) {
return new StringToEnum(ConversionUtils.getEnumType(targetType));
}
private static class StringToEnum<T extends Enum> implements Converter<String, T> {
private final Class<T> enumType;
StringToEnum(Class<T> enumType) {
this.enumType = enumType;
}
@Override
@Nullable
public T convert(String source) {
if (source.isEmpty()) {
return null;
}
return (T) Enum.valueOf(this.enumType, source.trim());
}
}
}
일반적인 Converter와 ConverterFactory의 차이는 ConverterFactory는 추상적인 클래스타입을 받아 구체적인 클래스타입으로 체크하고 Converter를 만들어 준다. 즉, 공통 타입에 대해 변환 처리를 할 수 있다.(아쉽지만 소문자를 대문자로 바꿔주지는 않는다.)
2. 구현 방향성과 구현 방법
Legacy Enum들은 공통적으로 소문자 문자열을 대문자형 Enum 값으로 바인딩하는 작업을 수행한다. 위에서 공통 타입에 대한 변환 처리를 하는 ConverterFactory를 이용하면 변환 작업을 공통으로 처리할 수 있다.
서비스를 지속하며 추가될 Enum들도 소문자 문자열이라는 보장은 없다. 때문에 특정 타입(Legacy Enum)만을 타겟하기 위해 인터페이스를 이용해 적용시켰다.
2.1 Legacy Enum을 위한 인터페이스
필자는 이전 포스트에서 Body에 사용하던 인터페이스를 재사용했다.
interface LegacyDtoEnum
이제 인터페이스 타입을 타겟으로 변환로직을 적용시켜보자.
2.2 커스텀 ConverterFactory 구현하기
쿼리는 요청시에만 사용되므로 소문자 문자열을 대문자 Enum 값으로 바인딩하는 로직만 작성하면 된다.
class StringToLegacyDtoEnumConverterFactory<F>: ConverterFactory<String, F> where F: Enum<*>, F: LegacyDtoEnum {
override fun <T : F> getConverter(targetType: Class<T>): Converter<String, T> {
return StringToEnumConverter(targetType)
}
@Suppress("UNCHECKED_CAST")
private class StringToEnumConverter<T : Enum<*>?>(private val enumType: Class<T>) : Converter<String, T> {
override fun convert(source: String): T {
val value = source.uppercase()
// 매칭되는 Enum 값 찾기
val enumValue = (enumType as Class<out Enum<*>>)
.enumConstants
.firstOrNull { it.name == value }
?: throw ApiInvalidEnumException()
return enumValue as T
}
}
}
Converter가 처리할 수 있는 타입을 Enum 클래스 타입이자 LegacyDtoEnum 일 경우에만 가능하도록 제한(where 절)했다. Factory에서 Converter를 만들때 구체적인 Enum 타입을 넘겨 값들을 불러올 수 있도록 만들고, Converter 로직은 소문자 문자열을 대문자로 바꿔 Enum 값과 매칭되는 값으로 선택되도록 만들었다.
2.3 커스텀 ConverterFactory 적용하기
WebMvc 설정에 Converter를 전역적으로 등록할 수 있다. WebMvcConfigurer 인터페이스를 사용하면 스프링 부트가 기존에 제공하는 기능에 추가적인 확장을 할 수 있다.
아래와 같이 ConverterFactory를 등록하기만 하면 원하던대로 잘 동작한다.
@Configuration
class WebMvcConfig: WebMvcConfigurer {
// ...
override fun addFormatters(registry: FormatterRegistry) {
registry.addConverterFactory(StringToLegacyDtoEnumConverterFactory())
}
}
3. 마무리
이전 포스트와 마찬가지로 인터페이스를 통해 Enum에 적용할 공통로직을 분리시켜 재사용해보았다.
전체 코드는 아래 레포에서 확인할 수 있다.
adevspoon-backend/adevspoon-api/src/main/kotlin/com/adevspoon/api/config/controller/converter/StringToLegacyDtoEnumConverterFact
CS 질문 - adevspoon-backend. Contribute to kids-ground/adevspoon-backend development by creating an account on GitHub.
github.com
'Spring' 카테고리의 다른 글
예외 정보를 인터페이스와 infix함수로 확장성, 가독성 높게 관리하기 (0) | 2024.04.18 |
---|---|
API별 인증 해제, 어노테이션으로 효율적으로 처리하기 (2) | 2024.04.09 |
문자열 응답 시 공통 응답형식이 적용되지 않는 문제 해결하기 (0) | 2024.04.05 |
공통 형식의 응답 효율적으로 처리하기 (1) | 2024.04.04 |
요청/응답 Body에 포함된 다수의 Legacy Enum 우아하게 관리하기 (1) | 2024.04.04 |
이전 포스트에서 요청/응답 Body에 포함된 Legacy Enum들을 인터페이스를 이용해 다뤘던 방법을 작성하였다.
아쉽게도, '개발 한 스푼' 서비스에서는 쿼리 파라미터에도 Enum 스타일의 소문자 문자열들이 사용되고 있다. Kotlin에서는 Enum값을 대문자로 작성하는 것을 권장하기 때문에 이에 바인딩하는 작업을 커스텀해야한다.
이번 글에서는 RequestParam, ModelAttribute에 포함된 다수의 Legacy Enum들을 효율적으로 관리하기 위해 적용한 방식을 공유해본다.
본 포스트는 다음과 같은 수순으로 진행됩니다.
1. 기존 Converter 톺아보기
2. 구현 방향성과 방법
1. 기존 Converter 톺아보기
@RequestParam과 @ModelAttribute 를 통해 쿼리 정보들을 받을 수 있다. 이때 우리는 각 필드에 String, Int와 같이 타입을 지정하는데 재밌게도 알아서 쿼리값들이 해당 타입으로 변환되어 주입된다.
이러한 동작이 가능한 이유는 WebMvc에서 Converter를 이용해 바인딩(DataBinder 이용) 작업이 이루어지기 때문이다. 내부적으로 가지고 있는 Converter 중 필드에 할당된 타입으로 전환시킬 수 있는 것을 골라 사용한다.
Enum으로 변환해주는 기본적인 Converter는 StringToEnumConverterFactory로 다음과 같이 구현되어있다.
final class StringToEnumConverterFactory implements ConverterFactory<String, Enum> {
@Override
public <T extends Enum> Converter<String, T> getConverter(Class<T> targetType) {
return new StringToEnum(ConversionUtils.getEnumType(targetType));
}
private static class StringToEnum<T extends Enum> implements Converter<String, T> {
private final Class<T> enumType;
StringToEnum(Class<T> enumType) {
this.enumType = enumType;
}
@Override
@Nullable
public T convert(String source) {
if (source.isEmpty()) {
return null;
}
return (T) Enum.valueOf(this.enumType, source.trim());
}
}
}
일반적인 Converter와 ConverterFactory의 차이는 ConverterFactory는 추상적인 클래스타입을 받아 구체적인 클래스타입으로 체크하고 Converter를 만들어 준다. 즉, 공통 타입에 대해 변환 처리를 할 수 있다.(아쉽지만 소문자를 대문자로 바꿔주지는 않는다.)
2. 구현 방향성과 구현 방법
Legacy Enum들은 공통적으로 소문자 문자열을 대문자형 Enum 값으로 바인딩하는 작업을 수행한다. 위에서 공통 타입에 대한 변환 처리를 하는 ConverterFactory를 이용하면 변환 작업을 공통으로 처리할 수 있다.
서비스를 지속하며 추가될 Enum들도 소문자 문자열이라는 보장은 없다. 때문에 특정 타입(Legacy Enum)만을 타겟하기 위해 인터페이스를 이용해 적용시켰다.
2.1 Legacy Enum을 위한 인터페이스
필자는 이전 포스트에서 Body에 사용하던 인터페이스를 재사용했다.
interface LegacyDtoEnum
이제 인터페이스 타입을 타겟으로 변환로직을 적용시켜보자.
2.2 커스텀 ConverterFactory 구현하기
쿼리는 요청시에만 사용되므로 소문자 문자열을 대문자 Enum 값으로 바인딩하는 로직만 작성하면 된다.
class StringToLegacyDtoEnumConverterFactory<F>: ConverterFactory<String, F> where F: Enum<*>, F: LegacyDtoEnum {
override fun <T : F> getConverter(targetType: Class<T>): Converter<String, T> {
return StringToEnumConverter(targetType)
}
@Suppress("UNCHECKED_CAST")
private class StringToEnumConverter<T : Enum<*>?>(private val enumType: Class<T>) : Converter<String, T> {
override fun convert(source: String): T {
val value = source.uppercase()
// 매칭되는 Enum 값 찾기
val enumValue = (enumType as Class<out Enum<*>>)
.enumConstants
.firstOrNull { it.name == value }
?: throw ApiInvalidEnumException()
return enumValue as T
}
}
}
Converter가 처리할 수 있는 타입을 Enum 클래스 타입이자 LegacyDtoEnum 일 경우에만 가능하도록 제한(where 절)했다. Factory에서 Converter를 만들때 구체적인 Enum 타입을 넘겨 값들을 불러올 수 있도록 만들고, Converter 로직은 소문자 문자열을 대문자로 바꿔 Enum 값과 매칭되는 값으로 선택되도록 만들었다.
2.3 커스텀 ConverterFactory 적용하기
WebMvc 설정에 Converter를 전역적으로 등록할 수 있다. WebMvcConfigurer 인터페이스를 사용하면 스프링 부트가 기존에 제공하는 기능에 추가적인 확장을 할 수 있다.
아래와 같이 ConverterFactory를 등록하기만 하면 원하던대로 잘 동작한다.
@Configuration
class WebMvcConfig: WebMvcConfigurer {
// ...
override fun addFormatters(registry: FormatterRegistry) {
registry.addConverterFactory(StringToLegacyDtoEnumConverterFactory())
}
}
3. 마무리
이전 포스트와 마찬가지로 인터페이스를 통해 Enum에 적용할 공통로직을 분리시켜 재사용해보았다.
전체 코드는 아래 레포에서 확인할 수 있다.
adevspoon-backend/adevspoon-api/src/main/kotlin/com/adevspoon/api/config/controller/converter/StringToLegacyDtoEnumConverterFact
CS 질문 - adevspoon-backend. Contribute to kids-ground/adevspoon-backend development by creating an account on GitHub.
github.com
'Spring' 카테고리의 다른 글
예외 정보를 인터페이스와 infix함수로 확장성, 가독성 높게 관리하기 (0) | 2024.04.18 |
---|---|
API별 인증 해제, 어노테이션으로 효율적으로 처리하기 (2) | 2024.04.09 |
문자열 응답 시 공통 응답형식이 적용되지 않는 문제 해결하기 (0) | 2024.04.05 |
공통 형식의 응답 효율적으로 처리하기 (1) | 2024.04.04 |
요청/응답 Body에 포함된 다수의 Legacy Enum 우아하게 관리하기 (1) | 2024.04.04 |