Spring/Jackson

BeanDeserializerModifier를 이용한 역직렬화 커스터마이징하기

긍.응.성 2024. 6. 16. 14:20
반응형

개요 및 예시는 이전글로부터 이어서 진행합니다.

https://ckddn9496.tistory.com/176

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 모듈을 만들어 사용하여 최소한의 수정으로 여러 상황에 대응가능할 것입니다

반응형