평소 개발을 하다보면 ObjectMapper를 사용하게 될 일이 많은데요. 저와 같은 경우엔 RedisCacheConfiguration 설정 중 ObjectMapper를 커스텀 해서 사용할 일이 있었습니다.
그리고 해당 커스텀한 ObjectMapper를 공통적으로 사용할 일이 필요하여 아래와 같이 ObjectMapper를 Bean으로 등록하여 공통화를 시켰습니다.
@Bean
public ObjectMapper polymorphicObjectMapper() {
PolymorphicTypeValidator typeValidator = BasicPolymorphicTypeValidator.builder()
.allowIfSubType(Object.class)
.build();
ObjectMapper polymorphicObjectMapper = new ObjectMapper();
polymorphicObjectMapper.registerModule(new JavaTimeModule());
polymorphicObjectMapper.activateDefaultTyping(typeValidator, DefaultTyping.EVERYTHING);
return polymorphicObjectMapper;
}
그런데.. 설정 후 SpringBoot를 실행하니 Swagger문서가 제대로 생성이 안됨과 동시에 API호출이 제대로 수행되지 않았습니다. (BadRequest 발생)
찾아보니 SpringBoot에서는 ObjectMapper가 기본적으로 자동 Bean등록이 되고 있었고 Bean으로 등록된 ObjectMapper를 사용하여 RestController, RestTemplate, WebClient 등 Json변환 시 공통으로 사용되고 있었습니다. 제가 멋대로 해당 설정을 변경해버려 오류가 났던것이죠.
따라서 이번 기회에 스프링에서 기본으로 설정해주는 ObjectMapper에 대해서 정확히 알아보고 넘어가야겠다는 마음에 블로그글을 작성하게 되었습니다.
간단하게 ObjectMapper는 아래와 같은 특징을 갖고있습니다.
JacksonAutoConfiguration 클래스를 보면 알 수 있습니다.
@AutoConfiguration
@ConditionalOnClass({ObjectMapper.class})
public class JacksonAutoConfiguration {
...
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({Jackson2ObjectMapperBuilder.class})
static class JacksonObjectMapperConfiguration {
JacksonObjectMapperConfiguration() {
}
@Bean
@Primary
@ConditionalOnMissingBean
ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
return builder.createXmlMapper(false).build();
}
}
...
}
@ConditionalOnMissingBean은 특정 빈이 존재하지 않을 때만 새로운 빈을 등록하는 조건부 어노테이션입니다. 이를 통해 ObjectMapper 타입의 Bean이 등록되어있지 않다면 등록이 될 수 있습니다. 그럼 Jackson2ObjectMapperBuilder를 사용하여 어떤 설정으로 등록되는지도 알아봅시다.
Jackson2ObjectMapperBuilder 클래스를 보면 알 수 있습니다.
createXmlMapper를 false로 설정했기 때문에 if문 안에 설정들은 무시가 되고
public class Jackson2ObjectMapperBuilder {
...
public <T extends ObjectMapper> T build() {
ObjectMapper mapper;
if (this.createXmlMapper) { // 무시
mapper = this.defaultUseWrapper != null ? (new XmlObjectMapperInitializer()).create(this.defaultUseWrapper, this.factory) : (new XmlObjectMapperInitializer()).create(this.factory);
} else {
mapper = this.factory != null ? new ObjectMapper(this.factory) : new ObjectMapper();
}
this.configure(mapper);
return mapper;
}
...
}
아래 configure 메서드를 수행하게 됩니다.
굉장히 장황하지만 저희는 여기서 딱 2개의 메서드만 보면됩니다. findWellKnownModules 필드를 제외하고는 전부 false거나 Null이거나 Empty하기 때문에 아래 메서드에서 어떤 모듈과 설정을 추가하는지 자세히 살펴보겠습니다.
registerWellKnownModulesIfAvailablecustomizeDefaultFeatures public void configure(ObjectMapper objectMapper) {
Assert.notNull(objectMapper, "ObjectMapper must not be null");
MultiValueMap<Object, Module> modulesToRegister = new LinkedMultiValueMap();
if (this.findModulesViaServiceLoader) {
ObjectMapper.findModules(this.moduleClassLoader).forEach((modulex) -> {
this.registerModule(modulex, modulesToRegister);
});
} else if (this.findWellKnownModules) {
this.registerWellKnownModulesIfAvailable(modulesToRegister); // 여기
}
if (this.modules != null) {
this.modules.forEach((modulex) -> {
this.registerModule(modulex, modulesToRegister);
});
}
if (this.moduleClasses != null) {
Class[] var3 = this.moduleClasses;
int var4 = var3.length;
for(int var5 = 0; var5 < var4; ++var5) {
Class<? extends Module> moduleClass = var3[var5];
this.registerModule((Module)BeanUtils.instantiateClass(moduleClass), modulesToRegister);
}
}
List<Module> modules = new ArrayList();
Iterator var8 = modulesToRegister.values().iterator();
while(var8.hasNext()) {
List<Module> nestedModules = (List)var8.next();
modules.addAll(nestedModules);
}
objectMapper.registerModules(modules);
if (this.dateFormat != null) {
objectMapper.setDateFormat(this.dateFormat);
}
if (this.locale != null) {
objectMapper.setLocale(this.locale);
}
if (this.timeZone != null) {
objectMapper.setTimeZone(this.timeZone);
}
if (this.annotationIntrospector != null) {
objectMapper.setAnnotationIntrospector(this.annotationIntrospector);
}
if (this.propertyNamingStrategy != null) {
objectMapper.setPropertyNamingStrategy(this.propertyNamingStrategy);
}
if (this.defaultTyping != null) {
objectMapper.setDefaultTyping(this.defaultTyping);
}
if (this.serializationInclusion != null) {
objectMapper.setDefaultPropertyInclusion(this.serializationInclusion);
}
if (this.filters != null) {
objectMapper.setFilterProvider(this.filters);
}
if (jackson2XmlPresent) {
objectMapper.addMixIn(ProblemDetail.class, ProblemDetailJacksonXmlMixin.class);
} else {
objectMapper.addMixIn(ProblemDetail.class, ProblemDetailJacksonMixin.class);
}
Map var10000 = this.mixIns;
Objects.requireNonNull(objectMapper);
var10000.forEach(objectMapper::addMixIn);
if (!this.serializers.isEmpty() || !this.deserializers.isEmpty()) {
SimpleModule module = new SimpleModule();
this.addSerializers(module);
this.addDeserializers(module);
objectMapper.registerModule(module);
}
var10000 = this.visibilities;
Objects.requireNonNull(objectMapper);
var10000.forEach(objectMapper::setVisibility);
this.customizeDefaultFeatures(objectMapper); // 여기
this.features.forEach((feature, enabled) -> {
this.configureFeature(objectMapper, feature, enabled);
});
if (this.handlerInstantiator != null) {
objectMapper.setHandlerInstantiator(this.handlerInstantiator);
} else if (this.applicationContext != null) {
objectMapper.setHandlerInstantiator(new SpringHandlerInstantiator(this.applicationContext.getAutowireCapableBeanFactory()));
}
if (this.configurer != null) {
this.configurer.accept(objectMapper);
}
}
참고:
Jackson2ObjectMapperBuilder의 많은 필드중에서findWellKnownModules만 true로 설정되어 있습니다.
private boolean findWellKnownModules = true;
private void registerWellKnownModulesIfAvailable(MultiValueMap<Object, Module> modulesToRegister) {
Class kotlinModuleClass;
Module kotlinModule;
try {
kotlinModuleClass = ClassUtils.forName("com.fasterxml.jackson.datatype.jdk8.Jdk8Module", this.moduleClassLoader);
kotlinModule = (Module)BeanUtils.instantiateClass(kotlinModuleClass);
modulesToRegister.set(kotlinModule.getTypeId(), kotlinModule);
} catch (ClassNotFoundException var7) {
}
try {
kotlinModuleClass = ClassUtils.forName("com.fasterxml.jackson.module.paramnames.ParameterNamesModule", this.moduleClassLoader);
kotlinModule = (Module)BeanUtils.instantiateClass(kotlinModuleClass);
modulesToRegister.set(kotlinModule.getTypeId(), kotlinModule);
} catch (ClassNotFoundException var6) {
}
try {
kotlinModuleClass = ClassUtils.forName("com.fasterxml.jackson.datatype.jsr310.JavaTimeModule", this.moduleClassLoader);
kotlinModule = (Module)BeanUtils.instantiateClass(kotlinModuleClass);
modulesToRegister.set(kotlinModule.getTypeId(), kotlinModule);
} catch (ClassNotFoundException var5) {
}
if (KotlinDetector.isKotlinPresent()) {
try {
kotlinModuleClass = ClassUtils.forName("com.fasterxml.jackson.module.kotlin.KotlinModule", this.moduleClassLoader);
kotlinModule = (Module)BeanUtils.instantiateClass(kotlinModuleClass);
modulesToRegister.set(kotlinModule.getTypeId(), kotlinModule);
} catch (ClassNotFoundException var4) {
}
}
}
총 4개의 모듈이 자동으로 등록되는 것을 볼 수 있습니다. 하나하나 살펴보자면~
java.time 패키지(LocalDate, LocalDateTime 등)을 Jackson이 직렬화/역직렬화 할 수 있도록 지원하는 모듈예시: {"name":"Conference","dateTime":"2025-02-16T12:34:56.789"}
private void customizeDefaultFeatures(ObjectMapper objectMapper) {
if (!this.features.containsKey(MapperFeature.DEFAULT_VIEW_INCLUSION)) {
this.configureFeature(objectMapper, MapperFeature.DEFAULT_VIEW_INCLUSION, false);
}
if (!this.features.containsKey(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)) {
this.configureFeature(objectMapper, DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
}
ObjectMapper의 기본 설정을 커스터마이징하는 메서드 입니다. 기본적으로 제공되는 설정값이 features에 존재하지 않으면, 해당 설정을 false 처리하는 것을 볼 수 있습니다.
@JsonView를 사용할 때 기본적으로 모든 속성을 직렬화할지 여부를 결정하는 설정true : 모든 필드가 기본적으로 직렬화됨 (기본값)false : @JsonView가 적용된 필드만 포함됨true : Json에 정의되지 않은 필드가 있다면 예외 발생 (기본값)false : 알 수 없는 필드가 있어도 무시하고 정상적으로 역직렬화 됨@Primay Bean으로 등록한 후 Custom한 Bean을 등록해준다면 스프링에 기본동작에 문제가 없을 것으로 생각됩니다.읽어주셔서 감사합니다!