📡 백엔드/🌱 Spring Boot

[Spring] Jackson Module 을 이용한 Jackson 확장

gengminy 2024. 3. 25. 22:19
Custom Serializer / Deserializer 를 만들어 Jackson Module 에 등록하고,
이를 통해 Jackson 을 확장하여 기본 JSON 처리 방식을 변경하는 방법에 대한 글입니다.

 

들어가며

Java Spring 서버를 개발하다보면 Jackson 이라는 라이브러리를 많이 들어봤을 것이다.

Jackson 은 Java 진영에서 JSON 처리를 담당하는 라이브러리이다.
Java 객체를 JSON 으로 직렬화, 혹은 반대로 역직렬화할 수 있는 데이터 바인딩 기능을 제공한다.

현재 REST API 처리 방식에 JSON 요청, 응답을 가장 많이 사용하기 때문에 중요한 라이브러리라 할 수 있다.
그래서인지 현재 스프링 프레임워크에도 Jackson 이 기본적으로 탑재되어 있기도 하다.

 

Jackson Module

Jackson Module 은 Jackson 의 확장을 위한 추상 클래스로
커스텀 직렬화, 역직렬화, 타입 변환기를 Jackson 에 등록하여 확장할 수 있도록 한다.

public abstract class Module implements Versioned {
	
    ...
    
    public abstract void setupModule(SetupContext var1);

    
    public interface SetupContext {
        void addDeserializers(Deserializers var1);

        void addKeyDeserializers(KeyDeserializers var1);

        void addSerializers(Serializers var1);
    
        void addKeySerializers(Serializers var1);
        
        void setNamingStrategy(PropertyNamingStrategy var1);
        
        ...
    }
}

Jackson 은 이전 버전의 Java 에서 구현되었기 때문에
Java 8 에서 도입된 java.time 패키지를 자동으로 처리하지 못한다.

이를 해결하기 위한 jackson-datatype-jsr310 모듈의 JavaTimeModule 도 Jackson Module 이다.

또한 Kotlin 에서 자주 사용하는 jackson-module-kotlin 도
자주 사용되는 Jackson Module 중 하나이다.

이처럼 Module 을 구현하고 ObjectMapper 에 등록하면 사용할 수 있는데

Jackson 2.10 이상에서는 Jackson Module 을 Bean 으로 등록하기만 하면
ObjectMapper 에서 ServiceLoader 를 사용하여 classpath 상 모든 Module 구현체를 자동으로 탐색하고 등록한다.

이후 ObjectMapper 는 JSON 을 바인딩하는 과정에서
Context 에 등록된 Module 을 탐색하고 적절한 모듈을 찾아 바인딩하는데 사용하게 된다.

 

public JsonDeserializer<?> createEnumDeserializer(DeserializationContext ctxt, JavaType type, BeanDescription beanDesc) throws JsonMappingException {
    DeserializationConfig config = ctxt.getConfig();
    Class<?> enumClass = type.getRawClass();
    JsonDeserializer<?> deser = this._findCustomEnumDeserializer(enumClass, config, beanDesc);
    if (deser == null) {
        if (enumClass == Enum.class) {
            return AbstractDeserializer.constructForNonPOJO(beanDesc);
        }

        ...
    }

    ...

    return (JsonDeserializer)deser;
}

protected JsonDeserializer<?> _findCustomEnumDeserializer(Class<?> type, DeserializationConfig config, BeanDescription beanDesc) throws JsonMappingException {
    Iterator var4 = this._factoryConfig.deserializers().iterator();

    JsonDeserializer deser;
    do {
        if (!var4.hasNext()) {
            return null;
        }

        Deserializers d = (Deserializers)var4.next();
        deser = d.findEnumDeserializer(type, config, beanDesc);
    } while(deser == null);

    return deser;
}

위 코드는 BasicDeserializerFactory.class 의 내부 구현 중 EnumDeserializer 에 대한 코드 일부이다.

사용자 정의 모듈을 최우선으로 선택하며 없을 경우 기본 Enum 처리 모듈을 사용하는 것을 알 수 있다.

 

public <T> MappingIterator<T> readValues(JsonParser p, JavaType valueType) throws IOException {
    this._assertNotNull("p", p);
    DeserializationConfig config = this.getDeserializationConfig();
    DeserializationContext ctxt = this.createDeserializationContext(p, config);
    JsonDeserializer<?> deser = this._findRootDeserializer(ctxt, valueType);
    return new MappingIterator(valueType, p, ctxt, deser, false, (Object)null);
}

ObjectMapper 는 Context 를 가져와 현재 존재하는 Module 을 탐색하고
가장 적절한 모듈을 가져와 JSON 매핑을 처리하게 된다.

이 중 눈여겨봐야 할 점은 Deserializers 클래스이다.

 

public interface Deserializers {
    JsonDeserializer<?> findEnumDeserializer(Class<?> var1, DeserializationConfig var2, BeanDescription var3) throws JsonMappingException;
    
    ...
    
    public abstract static class Base implements Deserializers {
        public Base() {
        }

        public JsonDeserializer<?> findEnumDeserializer(Class<?> type, DeserializationConfig config, BeanDescription beanDesc) throws JsonMappingException {
            return null;
        }
    }
}

 

Deserializers.class 는 JsonDeserializer 를 리턴하여
해당 클래스 타입에 맞는 Deserializaer 를 반환하거나
null 을 리턴하여 처리를 해당 처리기가 아닌 다른 Deserializer 에게 넘길 수 있다.

또한 내부 추상 메서드인 Base.class 를 구현하도록 강력하게 권장하고 있는데
나머지 타입에 대해 기본 처리기 구현체가 이미 만들어져 있기 때문이다.

즉 Deserializers.Base 를 구현하는 것을 통해
커스텀 Deserializer 를 구현하여 Jackson Module 로 등록할 수 있다.

이제 이를 구현해서 실제 Module 로 등록해보자.

 

Deserializer 등록

public class EnumDeserializer extends StdDeserializer<Enum> implements ContextualDeserializer {

    public EnumDeserializer() {
        this(null);
    }

    protected EnumDeserializer(Class<?> vc) {
        super(vc);
    }

    @SuppressWarnings("unchecked")
    @Override
    public Enum deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
        JsonNode jsonNode = jp.getCodec().readTree(jp);
        String text = jsonNode.asText();
        Class<? extends Enum> enumType = (Class<? extends Enum>) this._valueClass;
        return Arrays.stream(enumType.getEnumConstants()).filter(e -> {
            return e.name().equals(text);
        }).findAny().map(it -> Enum.valueOf(enumType, it.name())).orElse(null);
    }

    @Override
    public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException {
        return new EnumDeserializer(property.getType().getRawClass());
    }
}

다음과 같이 Custom Enum Deserializer 를 구현했다.

이 역직렬화기는 기대했던 Enum 타입 내부에 존재하는 값이 아닐 경우
JsonParseError 를 던지지 않고 null 값을 리턴하도록 한다.

Request Body 의 Enum 오류 처리를 위한 역직렬화기이고 프로젝트의 전역 에러 핸들링 설정으로 인해 사용하게 되었다.

자세한 것은 아래 링크에 포함된 3개의 게시글을 참조하면 좋을 것 같다.

https://gengminy.tistory.com/49

 

[Spring] 스프링 Custom Enum Deserializer 구현으로 JSON Enum null 로 파싱하기

https://gengminy.tistory.com/48 [Spring] 스프링 Enum Validator Reflection 으로 개선 및 구현하기 https://gengminy.tistory.com/47 [Spring] 스프링에서 Enum 클래스 Validation 추가하기 (Enum JSON parse error 해결) 스프링에서 일

gengminy.tistory.com

 

이제 이 Deserializer 를 Module 안에 포함시킨다.

@Configuration
public class JacksonConfig {
    @Bean
    public Module customEnumModule() {
        return new Module() {
            @Override
            public String getModuleName() {
                return "customEnumModule";
            }

            @Override
            public Version version() {
                return Version.unknownVersion();
            }

            @Override
            public void setupModule(SetupContext context) {
                context.addDeserializers(new Deserializers.Base() {
                    final Map<ClassKey, JsonDeserializer<?>> cache = new ConcurrentHashMap<>();

                    @Override
                    public JsonDeserializer<?> findEnumDeserializer(Class<?> type, DeserializationConfig config, BeanDescription beanDesc) {
                        if (Enum.class.isAssignableFrom(type)) {
                            JsonDeserializer<?> enumDeserializer = new EnumDeserializer(type);
                            addDeserializer(type, enumDeserializer);
                            return enumDeserializer;
                        }
                        return null;
                    }

                    @Override
                    public boolean hasDeserializerFor(DeserializationConfig config, Class<?> valueType) {
                        return cache.containsKey(new ClassKey(valueType));
                    }

                    public void addDeserializer(Class<?> forClass, JsonDeserializer<?> deserializer) {
                        ClassKey key = new ClassKey(forClass);
                        cache.put(key, deserializer);
                    }
                });
            }
        };
    }
}

Module 을 생성하고 필요한 추상 메서드를 구현한다.
이후 SetupContext 에서 Deserializers.Base() 를 인스턴스화 한다.

내부에는 findEnumDeserializer 를 오버라이딩하여 원하는 타입에 대해 Custom Deserializer 를 반환하도록 한다.

이 과정에서 Deserialzer 는 Lazy 하게 생성된다.

캐시를 두어 매번 역직렬화기를 생성하지 않고 이미 존재하는 Deserializer 를 가져와 처리하도록 할 수도 있다.

 

마치며

Jackson Module 을 알아보며 Custom Deserializer 를 Jackson Module 을 통해 등록하는 방식이
Jackson 이 의도하는 확장 방식에 가깝다고 확실히 느꼈다.

매번 @JsonCreator, @JsonDeserialize 등의 어노테이션을 사용하는 것 보다는
이처럼 전역적인 모듈 설정을 등록하는 것으로 중복을 없애고 깔끔하게 POJO 를 구성할 수 있을 것이다.