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
이제 이 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 를 구성할 수 있을 것이다.
끝
'📡 백엔드 > 🌱 Spring Boot' 카테고리의 다른 글
[Spring] x-www-form-urlencoded 요청 JSON 으로 변환하여 받기 (0) | 2024.04.01 |
---|---|
[Spring] Swagger 공통 응답 예시 커스터마이징 (0) | 2024.03.17 |
[Spring] spring-data-envers 를 이용한 엔티티 변경 이력 관리 (0) | 2024.03.10 |
[Spring] Redisson 분산락 AOP로 동시성 문제 해결하기 (트랜잭션 전파속성 NEVER 사용) (0) | 2023.07.06 |
[Spring] 스프링 소셜 로그인 OIDC 방식으로 구현하기 (OAuth with OpenID Connect) (1) | 2023.03.25 |