BeanDeserializerModifier를 이용한 역직렬화 커스터마이징하기
개요 및 예시는 이전글로부터 이어서 진행합니다.
ObjectMapper Enum Control – 1. BeanSerializerModifier (직렬화 커스터마이징)
1. 역직렬화 중복 작업 제거
BeanDeserializerModifier
직렬화 방식을 커스텀하게 변경했던 것처럼 Enum 필드 역직렬화하는 방식도 동일하게 애노테이션을 통해 적용하려 합니다.
public @interface EnumDeserialize {
/* enum 객체를 deserialize 할때 호출할 메서드 명 */
String value();
}
역직렬화할 Enum 타입에 붙여줄 애노테이션을 먼저 생성하였습니다.
class Deposit {
@EnumDeserialize("getByServerCode")
InterestType interestType;
...
}
외부 API 응답값을 Deposit 객체로 역직렬화하려하며 Enum 타입이 존재하는 상황입니다.
직렬화를 위한 메서드는 반환 타입이 String 이었다면, 역직렬화를 위한 메서드의 반환타입은 해당 필드의 Enum 타입이어야 합니다.
응답으로 serverCode 값이 들어올 때 이를 Enum 타입으로 반환해 주기 위한 메서드로 getByServerCode 로 선언하고 이를 애노테이션에 명시하였습니다.
public enum InterestType {
...
public static InterestType getByServerCode(String code) {
var codeValue = Integer.parseInt(code);
for (InterestType value : values()) {
if (value.getServerCode() == codeValue) {
return value;
}
}
return null;
}
}
명시한 메서드는 Enum 클래스 내부에 구현하였습니다. 참고로, Enum 오브젝트를 통해 변환하는 것이 아니라 문자열에 대하여 Enum 클래스로 변경해주어야하기에 static 키워드를 붙여줍니다.
BeanDeserializerModifier
public abstract class BeanDeserializerModifier {
public List<BeanPropertyDefinition> updateProperties(DeserializationConfig config,
BeanDescription beanDesc, List<BeanPropertyDefinition> propDefs) {
return propDefs;
}
public BeanDeserializerBuilder updateBuilder(DeserializationConfig config,
BeanDescription beanDesc, BeanDeserializerBuilder builder) {
return builder;
}
public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config,
BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
return deserializer;
}
...
}
BeanSerailizerModifier와 비슷하게 BeanDeserializerModifier는 Jackson의 역직렬화 과정을 수정하고 변경할 수 있습니다. 더 자세히는 BeanDeserializerFactory가 BeanDeserializer 인스턴스를 구성하는 과정의 라이프사이클마다 동작을 커스터마이징 할 수 있습니다.
저희가 관심 있는 부분은 역직렬화를 수행할 Deserializer를 수정해주는 부분인데요, 역직렬화 부분도 이를 위해 builder를 수정하여야 합니다. updateBuilder 메서드를 구현하여 Enum 필드에 대해 역직렬화 시 사용할 Deserializer를 커스텀하게 지정하는 구현입니다.
public class EnumDeserializerModifier extends BeanDeserializerModifier {
@Override
public BeanDeserializerBuilder updateBuilder(DeserializationConfig config, BeanDescription beanDesc, BeanDeserializerBuilder builder) {
builder.getProperties().forEachRemaining(settableBeanProperty -> {
if (!settableBeanProperty.getType().isEnumType()) {
return;
}
Optional.ofNullable(settableBeanProperty.getAnnotation(EnumDeserialize.class))
// EnumSerialize 애노테이션이 적용되었다면 지정된 메서드를 통하도록 직접 JsonDeserializer 를 구현한다
.ifPresent(annotation -> {
SettableBeanProperty newSettableBeanProperty = settableBeanProperty.withValueDeserializer(new JsonDeserializer<Object>() { // deserializer를 지정한다.
@Override
public Object deserialize(JsonParser parser, DeserializationContext ctxt) throws IOException, JacksonException {
return invokeMethods(settableBeanProperty.getType().getRawClass(), annotation.value(), parser.getText()); // 리플렉션 API를 이용하여 애노테이션에 지정된 메서드를 호출한 결과를 세팅한다
}
});
builder.addOrReplaceProperty(newSettableBeanProperty, true); // 수정한 beanProperty를 builder에 적용한다.
})
});
return builder;
}
private Object invokeMethods(Class<?> enumClass, String method, String argument) {
try {
return enumClass.getMethod(methodName, String.class).invoke(null, argument);
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
logger.error("{} enum deserialization failed unexpectedly", enumClass.getSimpleName(), e);
}
return null;
}
}
EnumDeserializerModifier를 모듈에 등록, 모듈을 ObjectMapper에 등록하면 EnumDeserialize 애노테이션이 달린 필드를 역직렬화할 때 EnumDeserializerModifier의 구현대로 역직렬화하게 됩니다.
@Test
void test_enumDeserializerModifier() throws JsonProcessingException {
SimpleModule module = new SimpleModule("myEnumModule", Version.unknownVersion());
module.setSerializerModifier(new EnumSerializerModifier());
module.setDeserializerModifier(new EnumDeserializerModifier());
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(module);
Deposit deposit = new Deposit(InterestType.SIMPLE);
String serializedString = objectMapper.writeValueAsString(deposit);
System.out.println(serializedString);
Deposit deserializedDeposit = objectMapper.readValue(serializedString, Deposit.class);
Assertions.assertEquals(deposit.getInterestType(), deserializedDeposit.getInterestType());
}
2. 역직렬화 성능 개선
역직렬화 과정도 동일하게 메서드 호출 시 리플렉션 API를 사용합니다. 이 오버헤드를 줄이기 위해 캐시를 적용하여 아래와 같이 개선할 수 있습니다.
private String invokeMethod(Object obj, Class << ? > enumClass, String method) {
var deserializerParams = new DeserializerParams(enumClass, methodName, argument);
try {
if (cache.containsKey(deserializerParams)) {
return cache.get(deserializerParams);
} else {
var result = enumClass.getMethod(methodName, String.class).invoke(null, argument);
cache.put(deserializerParams, result);
return result;
}
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
logger.error("{} enum deserialization failed unexpectedly", enumClass.getSimpleName(), e);
}
return null;
}
static class DeserializerParams {
Class << ? > enumClass;
String methodName;
String argument;
public DeserializerParams(Class << ? > enumClass, String methodName, String argument) {
this.enumClass = enumClass;
this.methodName = methodName;
this.argument = argument;
}
// equals(), hashCode()
}
3. 정리
이번 글에서는 Enum 역직렬화를 위해 일일히 JsonDeserializer를 구현하지 않고 애노테이션과 BeanDeserializerModifier 구현을 통해 지정한 메서드로 역직렬화하는 방법을 살펴보았습니다.
동일한 Enum이지만 다양한 값들로 응답 또는 수신하기 위해 직렬화/역직렬화가 필요한 상황이라면 간단한 애노테이션과 Modifier 설정으로 ObjectMapper 모듈을 만들어 사용하여 최소한의 수정으로 여러 상황에 대응가능할 것입니다