본문 바로가기
프로그래밍/Spring

[Spring + Jackson] Spring Boot에서 default ObjectMapper의 configuration을 알아보도록 하자

by 사바라다 2022. 9. 20.

안녕하세요. ObjectMapper를 기본적으로 선언해서 사용하면 FAIL_ON_UNKNOWN_PROPERTIES 옵션이 켜져있기 때문에 잘못된 RequestBody이 Fields가 들어오면 에러를 냅니다. 그런데 Spring Boot 기본 ObjectMapper를 사용하면 에러를 내지 않고 정상 동작합니다. 어째서 일까요? Spring Boot AutoConfiguration에 의해서 자동으로 ObjectMapper를 커스터마이징해서 사용하기 때문입니다.

그렇다면 Spring Boot의 ObjectMapper에 대한 기본 세팅은 어떻게 될까요? 오늘은 코드를 따라가보며 그 세팅을 찾아보도록 하겠습니다.

시작점 찾기

intellij IDE를 이용하면 빠르게 어떤 objectMapper를 사용하는지 파악할 수 있습니다. 기본적으로 제공되는 Bean이 어떤 것인지 찾기 위해서 시작점을 빠르게 찾기 위해서 @Configuration 클래스를 만들고 objectMapper bean을 주입받습니다. 그리고 왼쪽에 나오는 초록색 동그라미를 눌러보도록 합시다.

좌측의 녹색 동그라미를 누르면 해당 Bean으로 이동 됨

Default Setting

그러면 JacksonAutoConfiguration 클래스의 inner static class JacksonObjectMapperConfiguration의 아래 Bean으로 이동하는 것을 확인하실 수 있습니다.

@Bean
@Primary
@ConditionalOnMissingBean
ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
    return builder.createXmlMapper(false).build();
}

여기까지 오면 아래의 사실을 알 수 있습니다.

  • Jackson2ObjectMapperBuilder 로 OjbectMapper를 만듬
  • XmlMapper를 사용하지 않음
  • @ConditionalOnMissingBean를 사용하고 있으므로 ObjectMapper를 재정의하면 해당 ObjectMapper를 상요하지 않음

Jackson2ObjectMapperBuilder

위에서 봤을 때 Jackson2ObjectMapperBuilder 클래스의 build 메서드를 사용하고 있기 때문에 해당 클래스로 들어가보도록 하겠습니다. createXmlMapper가 false 이기때문에 ObjecctMapper를 생성하는것을 확인하실 수 있습니다. 그 후 configure 설정하고 반환하기 때문에 우리의 목적은 configure에 있을 것을 예측할 수 있습니다.

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());
    }
    configure(mapper);
    return (T) mapper;
}

Jackson2ObjectMapperBuilder#configure

configure 메서드에서는 실제로 configuration을 진행합니다. objectMapper는 수많은 커스터마이징이 가능한대 아래의 순서대로 세팅을 진행합니다.

  1. ObjectMapper Null 여부 체크
  2. ObjectMapper에 modules(Jackson의 확장을 위한 간단한 인터페이스) 등록
  3. DateFormat 등록
  4. Locale 등록
  5. TimeZone 등록
  6. AnnotationIntrospector(class에 붙어있는 @JsonDeserialize 이런 어노테이션 체커) 등록
  7. propertyNamingStrategy(fields 네이밍 룰) 등록
  8. serializationInclusion(필드 포함 여부, null, empty 등) 등록
  9. Filter와 mixin 등록
  10. Visibility(어떤 접근제한자를 이용하여 직렬화/역직렬화할지 선택) 세팅
  11. customizeDefaultFeatures(objectMapper)

우리가 알고싶어하는 objectMapper의 default Configure 세팅은 11번의 customizeDefaultFeatures 메서드에서 일어나게됩니다. 해당 메서드의 내용은 아래와 같습니다.

// Any change to this method should be also applied to spring-jms and spring-messaging
// MappingJackson2MessageConverter default constructors
private void customizeDefaultFeatures(ObjectMapper objectMapper) {
    if (!this.features.containsKey(MapperFeature.DEFAULT_VIEW_INCLUSION)) {
        configureFeature(objectMapper, MapperFeature.DEFAULT_VIEW_INCLUSION, false);
    }
    if (!this.features.containsKey(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)) {
        configureFeature(objectMapper, DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    }
}

위 메스드에서 우리는 아래 설정들이 false가 되는 것을 화인할 수 있습니다. 아래 필드들은 위 configuration가 true 일 경우에 발생하게 아래의 설명글과 같은 케이스가 발생합니다.

  • DEFAULT_VIEW_INCLUSION : 하나의 POJO를 여러 곳에서 사용할때, 하지만 특정 저장 필드는 특정한 곳에서만 사용하거나, 사용하지 않는 차이를 주기 위해서 사용
  • FAIL_ON_UNKNOWN_PROPERTIES : json String을 Object로 변경하는 Deserialize시에 Object에는 없는 json String field가 있을 경우 ERROR 처리

Jackson2ObjectMapperBuilder Bean

사실 이게 끝이 아닙니다. 우리는 위에서 Jackson2ObjectMapperBuilder를 Bean으로써 가져왔다는 사실을 알고 있습니다. 해당 Bean의 정의도 찾아봐야합니다. 그 정의는 아래와 같다는 것을 알 수 있었습니다. 그리고 customize라는 메서드가 있다는 것을 확인 하실 수 있었습니다.

@Bean
@Scope("prototype")
@ConditionalOnMissingBean
Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder(ApplicationContext applicationContext,
        List<Jackson2ObjectMapperBuilderCustomizer> customizers) {
    Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
    builder.applicationContext(applicationContext);
    customize(builder, customizers);
    return builder;
}

들어가보도록 하겠습니다. 여기서도 위에서 봤던것과 마찬가지로 별도의 configuration 세팅이 이루어지고 있다는 것을 알 수 있었습니다.

@Override
public void customize(Jackson2ObjectMapperBuilder builder) {
    if (this.jacksonProperties.getDefaultPropertyInclusion() != null) {
        builder.serializationInclusion(this.jacksonProperties.getDefaultPropertyInclusion());
    }
    if (this.jacksonProperties.getTimeZone() != null) {
        builder.timeZone(this.jacksonProperties.getTimeZone());
    }
    configureFeatures(builder, FEATURE_DEFAULTS);
    configureVisibility(builder, this.jacksonProperties.getVisibility());
    configureFeatures(builder, this.jacksonProperties.getDeserialization());
    configureFeatures(builder, this.jacksonProperties.getSerialization());
    configureFeatures(builder, this.jacksonProperties.getMapper());
    configureFeatures(builder, this.jacksonProperties.getParser());
    configureFeatures(builder, this.jacksonProperties.getGenerator());
    configureDateFormat(builder);
    configurePropertyNamingStrategy(builder);
    configureModules(builder);
    configureLocale(builder);
    configureDefaultLeniency(builder);
    configureConstructorDetector(builder);
}

그리고 눈여겨보아야 할 라인이 configureFeatures(builder, FEATURE_DEFAULTS); 입니다. DEFAULT 세팅을 해준다는 것인데 해당 DEFAULT 세팅에 대한 코드는 아래와 같습니다. 네, 여기서 시간값 반환에 대해서 TIMESTAMPS 찍는 것을 false 처리하고 있습니다.

private static final Map<?, Boolean> FEATURE_DEFAULTS;

static {
    Map<Object, Boolean> featureDefaults = new HashMap<>();
    featureDefaults.put(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
    featureDefaults.put(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS, false);
    FEATURE_DEFAULTS = Collections.unmodifiableMap(featureDefaults);
}

확인된 Spring Boot의 ObjectMapper의 기본 세팅

위의 과정을 통해서 알게된 Spring Boot의 ObjectMapper 기본 세팅은 아래와 같은것을 알 수 있었습니다.

configureFeature(objectMapper, MapperFeature.DEFAULT_VIEW_INCLUSION, false)
configureFeature(objectMapper, DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
featureDefaults.put(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
featureDefaults.put(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS, false)

테스트

그럼 테스트를 통해서 위의 설정을 확인해보도록 하겠습니다.

@PostMapping("/coffee")
fun getCoffee(
    @RequestParam(required = false) brand: String?,
    @RequestParam(required = false) name: String?,
    @RequestBody coffeeRequest: CoffeeRequest,
): CoffeeResponse {
    return CoffeeResponse(
        name = name,
        brand = brand,
        date = coffeeRequest.date
    )
}
//REQUEST

POST http://localhost:8080/coffee?brand=BRAND
Content-Type: application/json

{
  "date" : "2022-09-19T21:51:32.402883",
  "date2" : "2022-09-19T21:51:32.402883"
}
// RESPONSE
{
  "name": null,
  "brand": "BRAND",
  "date": "2022-09-19T21:51:32.402883"
}

마무리

ObjectMapper를 조사하다가 당연히 Spring Boot 기본세팅으로는 FAIL_ON_UNKNOWN_PROPERTIES가 켜져있을거라고 생각하고 작업을 했는데 안켜져 있길래 어디서 세팅하는지 찾아보다가 여기까지 왔네요.

오늘은 이렇게 ObjectMapper의 Option들에 대해서 알아보는 시간을 가져보았습니다.

감사합니다.

참조

https://www.baeldung.com/spring-boot-customize-jackson-objectmapper

댓글