Spring Boot JSON 처리를 위한 자동 구성 및 설정
Spring Boot에서 JSON 데이터 처리는 RESTful API를 구축하거나 데이터 전송을 수행할 때 필수적인 요소라고 할 수 있다.
Spring Boot는 기본적으로 Jackson 라이브러리를 사용하여 JSON 직렬화 및 역직렬화를 자동으로 처리한다. 따라서 개발자는 복잡한 설정 없이도 객체를 JSON으로 변환하고, JSON 데이터를 객체로 손쉽게 맵핑할 수 있다.
이번 포스팅에서는 Spring Boot의 JSON Auto Configuration 방식과 주요 설정 방법에 대해서 정리하고자 한다.
Dependency
JSON 처리를 위한 디펜던시는 다음과 같다.
'org.springframework.boot:spring-boot-starter-json'
위 디펜던시를 통해서 jackson-databind, jackson-core, jackson-annotation 의존성이 함께 추가된다.
org.springframework.boot:spring-boot-starter-web 의존성에 jackson 라이브러리 의존성이 포함되어 있어
spring-boot-starter-web 의존성을 추가한 경우 spring-boot-starter-json 의존성을 명시적으로 추가할 필요는 없다.
Spring Boot Jackson AutoConfiguration
spring-boot-autoconfigure에 의해서 JSON 처리를 위한 Jackson 라이브러리에 대한 자동 구성이 제공된다.
spring-boot-starter 의존성에는 spring-boot-autoconfigure 의존성이 포함되는데 spring-boot-autoconfigure 에는 여러 모듈에 대한 자동 구성이 제공된다.
그중에서 org.springframework.boot.autoconfigure.jackson 패키지에는 jackson 라이브러리에 대한 자동구성 클래스들이 포함되어 있다.
org.springframework.boot.autoconfigure.gson 패키지도 제공되는데 gson 라이브러리 의존성을 추가하게 되면 gson에 대한 자동 구성도 사용할 수 있게 된다.
Jackson 자동 구성을 살펴보자.
Jackson 자동 구성을 담당하는 클래스는 JacksonAutoConfiguration 클래스다.
@AutoConfiguration
@ConditionalOnClass(ObjectMapper.class)
public class JacksonAutoConfiguration {
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);
}
...
...
}
@ConditionalOnClass 어노테이션에 의해서 ObjectMapper 클래스가 클래스패스에 정의되어 있는 경우 자동 구성 클래스가 동작한다.
FEATURE_DEFAULTS 맵에는 SerializationFeature.WRITE_DATES_AS_TIMESTAMPS 속성과 SerializationFeature.WRITE_DURATION_AS_TIMESTAMPS 속성을 모두 false로 지정하는데 Jackson2ObjectMapperBuilder 에 의해서 생성되는 ObjectMapper 빈은 디폴트로 위 속성을 가지고 생성된다.
JacksonAutoConfiguration 클래스에는 ObjectMapper를 구성하기 위한 Jackson2ObjectMapperBuilder를 생성하는데, 이를 위한 몇 가지 주요한 static 클래스가 있다.
JacksonObjectMapperBuilderConfiguration
public class JacksonAutoConfiguration {
...
...
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Jackson2ObjectMapperBuilder.class)
static class JacksonObjectMapperBuilderConfiguration {
@Bean
@Scope("prototype")
@ConditionalOnMissingBean
Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder(ApplicationContext applicationContext,
List<Jackson2ObjectMapperBuilderCustomizer> customizers) {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
builder.applicationContext(applicationContext);
customize(builder, customizers);
return builder;
}
private void customize(Jackson2ObjectMapperBuilder builder,
List<Jackson2ObjectMapperBuilderCustomizer> customizers) {
for (Jackson2ObjectMapperBuilderCustomizer customizer : customizers) {
customizer.customize(builder);
}
}
}
}
jacksonObjectMapperBuilder 빈 생성 메서드를 보면 파라미터로 List<Jackson2ObjectMapperBuilderCustomizer>를 전달받게 되는데 Jackson2ObjectMapperBuilderCustomizer는 다음과 같이 정의되어 있다.
@FunctionalInterface
public interface Jackson2ObjectMapperBuilderCustomizer {
/**
* Customize the JacksonObjectMapperBuilder.
* @param jacksonObjectMapperBuilder the JacksonObjectMapperBuilder to customize
*/
void customize(Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder);
}
customize() 메서드에 Jackson2ObjectMapperBuilder 인스턴스를 전달하는 파라미터가 정의되어 있다.
Jackson2ObjectMapperBuilder 인스턴스는 JacksonObjectMapperBuilderConfiguration 클래스의 customize 메서드 내에서 Jackson2ObjectMapperBuilderCustomizer 클래스의 customize() 메서드를 호출하는데 이때 Jackson2ObjectMapperBuilder 인스턴스를 전달한다.
이는 Jackson2ObjectMapperBuilder 인스턴스를 customize() 메서드 내에서 커스텀하게 설정하도록 하기 위한 사용자 인터페이스를 제공하기 위함이다.
즉 Jackson2ObjectMapperBuilderCustomizer 빈을 직접 생성하여 Jackson2ObjectMapperBuilder에 우리가 원하는 설정들을 하도록 람다식 혹은 함수형 인터페이스 구현 클래스를 제공하게 되면 Jackson2ObjectMapperBuilder에 의해서 생성되는 ObjectMapper에 우리가 원하는 설정들이 반영된다.
다음은 Jackson2ObjectMapperBuilderCustomizer 빈을 생성하는 샘플 코드다.
@Bean
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
//jackson2ObjectMapperBuilder 인스턴스가 전달된다.
//jackson2ObjectMapperBuilder 인스턴스에 여러 설정들을 한다.
return builder -> {
builder
.featuresToDisable( SerializationFeature.WRITE_DATES_AS_TIMESTAMPS )
.featuresToEnable( SerializationFeature.INDENT_OUTPUT )
.featuresToDisable( SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS )
.featuresToDisable( DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS )
.serializationInclusion( JsonInclude.Include.NON_NULL );
};
}
위 Jackson2ObjectMapperBuilderCustomizer 내에서 글로벌하게 적용될 설정들을 하면 된다.
Jackson2ObjectMapperBuilder 빈의 scope는 prototype으로 매번 생성되는데 우리가 정의한 Jackson2ObjectMapperBuilderCustomizer 빈은 생성되는 모든 Jackson2ObjectMapperBuilder 빈에 적용된다.
Jackson2ObjectMapperBuilder 빈의 scope가 prototype인 이유
1. 상태 격리
1) Jackson2ObjectMapperBuilder는 상태를 가지는 객체다. 빌더는 여러 설정을 통해 ObjectMapper를 구성하며, 이 과정에서 내부 상태가 변경될 수 있다.
2) singleton scope로 설정하면 여러 클라이언트가 동일한 빌더 인스턴스를 공유하게 되어 설정이 누락되거나 예상치 못한 동작이 발생할 수 있다.
2. 유연한 커스터마이징
1) 애플리케이션의 다양한 부분에서 각기 다른 ObjectMapper 설정이 필요할 수 있다. 예를 들어, 일부 API에서는 날짜 형식을 다르게 설정하거나, 특정 모듈만 활성화해야 할 수 있다.
2) prototype scope를 사용하면 각 요청시 마다 독립적인 빌더 인스턴스를 생성하여 맞춤형 설정을 적용할 수 있다.
3 멀티 ObjectMapper 지원
1) Spring Boot는 기본적으로 하나의 ObjectMapper를 사용하지만 필요에 따라 여러 ObjectMapper 인스턴스를 사용할 수 있다. 이를 위해 각기 다른 설정을 가진 빌더 인스턴스가 필요하다.
2) prototype scope를 사용하면 각기 다른 설정을 가진 여러 빌더 인스턴스를 쉽게 생성할 수 있다.
4. 스레드 안전성
1) Jackson2ObjectMapperBuilder는 스레드 안전하지 않은 상태를 가질 수 있다. 싱글톤 빈으로 정의할 경우 여러 스레드가 동시에 빌더를 사용하여 설정을 변경할 가능성이 있다.
2) prototype scope는 각기 다른 스레드가 독립적인 빌더 인스턴스를 사용하게 하여 스레드 안전성을 보장한다.
Jackson2ObjectMapperBuilderCustomizerConfiguration
Jackson2ObjectMapperBuilderCustomizerConfiguration 클래스는 JacksonAutoConfiguration 클래스의 static 클래스로 다음과 같이 정의되어 있다.
public class JacksonAutoConfiguration {
...
...
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Jackson2ObjectMapperBuilder.class)
@EnableConfigurationProperties(JacksonProperties.class)
static class Jackson2ObjectMapperBuilderCustomizerConfiguration {
@Bean
StandardJackson2ObjectMapperBuilderCustomizer standardJacksonObjectMapperBuilderCustomizer(
JacksonProperties jacksonProperties, ObjectProvider<Module> modules) {
return new StandardJackson2ObjectMapperBuilderCustomizer(jacksonProperties, modules.stream().toList());
}
static final class StandardJackson2ObjectMapperBuilderCustomizer
implements Jackson2ObjectMapperBuilderCustomizer, Ordered {
private final JacksonProperties jacksonProperties;
private final Collection<Module> modules;
StandardJackson2ObjectMapperBuilderCustomizer(JacksonProperties jacksonProperties,
Collection<Module> modules) {
this.jacksonProperties = jacksonProperties;
this.modules = modules;
}
@Override
public int getOrder() {
return 0;
}
@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());
configureFeatures(builder, this.jacksonProperties.getDatatype().getEnum());
configureFeatures(builder, this.jacksonProperties.getDatatype().getJsonNode());
configureDateFormat(builder);
configurePropertyNamingStrategy(builder);
configureModules(builder);
configureLocale(builder);
configureDefaultLeniency(builder);
configureConstructorDetector(builder);
}
...
...
}
}
}
Jackson2ObjectMapperBuilderCustomizerConfiguration 클래스에는 StandardJackson2ObjectMapperBuilderCustomizer 빈을 생성하는데 이를 통해서 Jackson2ObjectMapperBuilder에 기본적으로 적용될 특성 및 spring.jackson 설정들을 적용한다.
Jackson2ObjectMapperBuilder에 적용되는 customizer는 StandardJackson2ObjectMapperBuilderCustomizer가 우선 적용되고 이후 사용자 정의 customizer가 적용된다. 즉, StandardJackson2ObjectMapperBuilderCustomizer와 사용자 정의 customizer에 설정 항목이 중복되면 최종적으로 사용자 정의 customizer 설정이 적용되고 중복되지 않는 항목에 대해서는 StandardJackson2ObjectMapperBuilderCustomizer에 의해서 spring.jackson 에 설정된 항목들이 적용된다.
StandardJackson2ObjectMapperBuilderCustomizer 빈 생성 메서드에는 JacksonProperties 타입의 jacksonProperties 인스턴스가 전달되는데 jacksonProperties 인스턴스에는 spring.jackson.* 에 지정된 설정들이 바인딩된다.
JacksonObjectMapperConfiguration
최종적으로 ObjectMapper 빈은 JacksonObjectMapperConfiguration 클래스에 의해서 생성되며 클래스 정의는 다음과 같다.
@AutoConfiguration
@ConditionalOnClass( ObjectMapper.class )
public class JacksonAutoConfiguration {
...
...
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Jackson2ObjectMapperBuilder.class)
static class JacksonObjectMapperConfiguration {
@Bean
@Primary
@ConditionalOnMissingBean
ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
return builder.createXmlMapper(false).build();
}
}
...
...
}
jacksonObjectMapper 빈 생성 메서드에 @ConditionalOnMissingBean 어노테이션에 의해서 ObjectMapper 빈이 생성되어 있지 않은 경우에 호출된다. 즉, 직접 ObjectMapper 빈을 생성한 경우에는 호출되지 않는다는 의미다.
결국 지금까지 설명한 AutoConfiguration은 ObjectMapper빈을 직접 생성하지 않은 경우에 동작하게 된다.
ObjectMapper 빈은 다음과 같이 직접 생성할 수 있다.
@Bean
public ObjectMapper objectMapper(JacksonProperties jacksonProperties) {
ObjectMapper objectMapper = new ObjectMapper();
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule
.addSerializer( LocalDateTime.class, new LocalDateTimeSerializer( DateTimeFormatter.ofPattern( "yyyy-MM-dd HH:mm:ss" ) ) )
.addDeserializer( LocalDateTime.class, new LocalDateTimeDeserializer( DateTimeFormatter.ofPattern( "yyyy-MM-dd HH:mm:ss" ) ) );
objectMapper.registerModules( javaTimeModule );
objectMapper.setSerializationInclusion( JsonInclude.Include.NON_NULL );
objectMapper.setTimeZone( jacksonProperties.getTimeZone() != null? jacksonProperties.getTimeZone() : TimeZone.getTimeZone( "Asia/Seoul" ) );
objectMapper.configure( SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false );
objectMapper.configure( SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, false );
objectMapper.configure( SerializationFeature.INDENT_OUTPUT, true );
objectMapper.configure( SerializationFeature.CLOSE_CLOSEABLE, true );
objectMapper.configure( SerializationFeature.WRITE_CHAR_ARRAYS_AS_JSON_ARRAYS, true );
objectMapper.configure( DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, false );
return objectMapper;
}
Features (properties)
JacksonProperties 에는 기본적으로 Enum 타입의
- SerializationFeature - 객체를 JSON 혹은 다른 포맷으로 변환 - jackson databind 관련 속성
- DeserializationFeature - JSON 혹은 다른 포맷을 객체로 변환 - jackson databind 관련 속성
- MapperFeature - jackson databind 관련 속성 - Mapper 별 구성을 정의
- JsonParser.Feature - jackson core 관련 속성 - 스트리밍 API에서 구문 분석이 수행되는 방식을 변경하는 일련의 on/off
- JsonGeneration.Feature - jackson core 관련 속성 - 스트리밍 API에서 구문 분석이 수행되는 방식을 변경하는 일련의 on/off
인스턴스를 생성하여 가지고 있고 이는 디폴트 속성을 적용한다는 것을 의미한다.
MapperFeature, SerializationFeature, DeserializationFeature는 databind 구성요소에 영향을 주는 설정이고 JsonParser.Feature, JsonGenerator.Feature는 Streaming API 동작에 영향을 주는 설정이다.
각 Feature에 대한 애플리케이션 설정은 다음과 같은 prefix로 매핑된다.
- SerializationFeature
- spring.jackson.serialization.<Feature명에 대한 대시 표기법>
- ex. WRAP_ROOT_VALUE -> spring.jackson.serialization.wrap-root-value
- spring.jackson.serialization.<Feature명에 대한 대시 표기법>
- DeserializationFeature
- spring.jackson.deserialization.<Feature명에 대한 대시 표기법>
- ex. USE_BIG_DECIMAL_FOR_FLOATS -> spring.jackson.deserialization.use-big-decimal-for-floats
- spring.jackson.deserialization.<Feature명에 대한 대시 표기법>
- MapperFeature
- spring.jackson.mapper.<Feature명에 대한 대시 표기법>
- ex. USE_ANNOTATIONS -> spring.jackson.mapper.use-annotations
- spring.jackson.mapper.<Feature명에 대한 대시 표기법>
- ParserFeature
- spring.jackson.parser.<Feature명에 대한 대시 표기법>
- ex. ALLOW_COMMENTS -> spring.jackson.parser.allow-comments
- spring.jackson.parser.<Feature명에 대한 대시 표기법>
- GeneratorFeature
- spring.jackson.generator.<Feature명에 대한 대시 표기법>
- ex. AUTO_CLOSE_JSON_CONTENT -> spring.jackson.generator.auto-close-json-content
- spring.jackson.generator.<Feature명에 대한 대시 표기법>
MapperFeature
Jackson은 ObjectMapper를 사용하기 전에만 정의할 수 있는 일련의 매퍼별 구성을 정의한다.
각 구성에 대한 설명은 https://github.com/FasterXML/jackson-databind/wiki/Mapper-Features 페이지를 참고하기 바란다.
혹은 com.fasterxml.jackson.databind 패키지의 MapperFeature 클래스 파일을 참고해도 좋다.
MapperFeature는 보통 디폴트 설정 그대로 사용하면 될 것 같다.
com.fasterxml.jackson.databind.MapperFeature 클래스 파일의 설정 정의는 다음과 같다.
public enum MapperFeature implements ConfigFeature
{
USE_ANNOTATIONS(true),
USE_GETTERS_AS_SETTERS(true),
PROPAGATE_TRANSIENT_MARKER(false),
AUTO_DETECT_CREATORS(true),
AUTO_DETECT_FIELDS(true),
AUTO_DETECT_GETTERS(true),
AUTO_DETECT_IS_GETTERS(true),
AUTO_DETECT_SETTERS(true),
REQUIRE_SETTERS_FOR_GETTERS(false),
ALLOW_FINAL_FIELDS_AS_MUTATORS(true),
INFER_PROPERTY_MUTATORS(true),
INFER_CREATOR_FROM_CONSTRUCTOR_PROPERTIES(true),
ALLOW_VOID_VALUED_PROPERTIES(false),
CAN_OVERRIDE_ACCESS_MODIFIERS(true),
OVERRIDE_PUBLIC_ACCESS_MODIFIERS(true),
USE_STATIC_TYPING(false),
USE_BASE_TYPE_AS_DEFAULT_IMPL(false),
INFER_BUILDER_TYPE_BINDINGS(true),
REQUIRE_TYPE_ID_FOR_SUBTYPES(true),
DEFAULT_VIEW_INCLUSION(true),
SORT_PROPERTIES_ALPHABETICALLY(false),
SORT_CREATOR_PROPERTIES_FIRST(true),
ACCEPT_CASE_INSENSITIVE_PROPERTIES(false),
ACCEPT_CASE_INSENSITIVE_ENUMS(false),
ACCEPT_CASE_INSENSITIVE_VALUES(false),
USE_WRAPPER_NAME_AS_PROPERTY_NAME(false),
USE_STD_BEAN_NAMING(false),
ALLOW_EXPLICIT_PROPERTY_RENAMING(false),
ALLOW_IS_GETTERS_FOR_NON_BOOLEAN(false),
ALLOW_COERCION_OF_SCALARS(true),
IGNORE_DUPLICATE_MODULE_REGISTRATIONS(true),
IGNORE_MERGE_FOR_UNMERGEABLE(true),
BLOCK_UNSAFE_POLYMORPHIC_BASE_TYPES(false),
APPLY_DEFAULT_VALUES(true);
...
...
}
SerializationFeature (직렬화 속성)
ObjectMapper 클래스의 enable(feature), disable(feature), configure(feature, state) 메서드를 사용하여 ObjectMapper의 기본값을 변경할 수 있다.
Jackson2ObjectMapperBuilder 클래스는 featureToEnable(feature), featureToDisable(feature) 메서드를 통해 속성을 설정할 수 있다.
속성 | 설명 | 디폴트 | 속성 매핑 |
WRAP_ROOT_VALUE | 활성화하면 출력을 단일 속성 JSON 객체로 매핑한다. 래퍼 프로퍼티 이름은 다음 중 하나를 기준으로 한다. - @JsonRootName 어노테이션의 값 - 어노테이션이 지정되지 않은 경우 직렬화된 인스턴스의 클래스 이름 (패키지 경로는 붙지 않는다) * XML에는 항상 별도의 루트 요소가 필요하므로 XML을 작성하는 경우에는 이 설정이 적용되지 않는다. |
false | spring.jackson.serialization.wrap-root-value |
INDENT_OUTPUT | 기본 들여쓰기(pretty print) 출력을 할지 여부를 설정한다. - 모든 데이터 형식이 들여쓰기를 지원하지는 않는다. binary 형식은 지원되지 않고 CSV는 평면 구조이므로 pretty 하게 표시할 수 없다. |
false | spring.jackson.serialization.indent-output |
FAIL_ON_EMPTY_BEANS | 타입(클래스)에 대한 직렬화기가 없고 getter 혹은 어노테이션이 지정되지 않은 경우 예외를 발생시킬지 여부를 설정한다. | true | spring.jackson.serialization.fail-on-empty-beans |
WRAP_EXCEPTIONS | 잭슨 코드가 예외를 포착하고 래핑하여 문제의 위치(입력내)에 대한 추가 정보를 추가할지 여부를 결정하는 기능이다. 이 옵션을 활성화하면 대부분의 예외가 포착되어 다시 던져진다. 모든 예외는 검사되므로 더 많은 컨텍스트 정보를 얻을 수 있다. | true | spring.jackson.serialization.wrap-exceptions |
CLOSE_CLOSEABLE | ObjecctMapper는 직렬화 작업이 끝날 때(예외가 발생한 경우 포함) java.io.Closeable를 구현하는 직렬화 객체의 close()를 호출한다. 파일의 내용을 읽어서 해당 파일의 데이터를 직렬화 할 때 리소스 정리에 유용하다. |
false | spring.jackson.serialization.close-closeable |
FLUSH_AFTER_WRITE_VALUE | ObjectMapper의 writeValue(JsonGenerator, ...) 메서드가 호출된 후 JsonGenerator.flush()를 자동으로 호출할지 여부를 결정한다. 이 기능을 비활성 하는 주된 이유는 성능이다. 네트워크 연결의 경우 플러싱이 최적보다 더 일찍 메시지를 전소할 수 있고, 일부 압축 스트림의 경우 압축 블록이 flush로 완료될 수 있기 때문이다. |
true | spring.jackson.serialization-flush-after-write-value |
WRITE_DATES_AS_TIMESTAMP | 날짜/시간 타입의 값을 64bit 타임스탬프로 출력할지 문자열로 출력할지를 설정한다. com.fasterxml.jackson.databind.ser.std 패키지에 있는 DateSerializer에 의해서 변환된다. DateSerializer는 spring.jackson.date-format 설정이 지정된 경우 해당 포맷을 DateFormat으로 사용하고 설정이 없는 경우 com.fasterxml.jackson.databind.util.StdDateFormat에 의해서 변환된다. spring.jackson.date-format은 JDK8 이전의 Date, Calendar와 같은 레거시 날짜/시간 타입에 적용된다. 디폴트는 true로 정의되어 있지만 기본 AutoConfiguration에 의해서 생성되는 ObjectMapper는 false로 설정된다. |
true | spring.jackson.serialization.write-dates-as-timestamp |
WRITE_DATE_KEYS_AS_TIMESTAMP | java.util.Map 키로 사용되는 java.util.Dates가 타임스탬프로 직렬화 되는지 여부를 결정하는 설정이다. false인 경우 텍스트(ISO-8601) 값으로 직렬화 된다. | false | spring.jackson.serialization.write-date-keys-as-timestamp |
WRITE_CHAR_ARRYAS_AS_JSON_ARRAYS | 기본 char[] 값이 직렬화되는 방식을 설정한다. true인 경우 각 char array 요소를 JSON 배열로 직렬화 한다. false인 경우 char array 요소를 합친 하나의 JSON 문자열로 직렬화한다. | false | spring.jackson.serialization.write-char-arrays-as-json-arrays |
WRITE_ENUMS_USING_TO_STRING | Enum 타입의 직렬화를 결정하는데 사용할 메서드를 설정한다. (문자열로 직렬화되는 경우) false면 Enum 타입의 name() 메서드가 true인 경우 Enum 타입의 toString()을 사용한다. true로 지정된 경우 역직렬화시 READ_ENUMS_USING_TO_STRING 이 true로 지정되어야 할 것 같다. |
false | spring.jackson.serialization.write-enums-using-to-string |
WRITE_ENUMS_USING_INDEX | Enum 타입을 인덱스로 직렬화할지 문자열로 직렬화할지 결정한다. true면 Enum.ordinal() 값으로 직렬화 한다. true인 경우 WRITE_ENUMS_USING_TO_STRING 설정과 관계없이 index로 직렬화한다. |
false | spring.jackson.serialization.write-enums-using-index |
WRITE_NULL_MAP_VALUES | Map 인스턴스의 entry의 값이 null인 entry를 직렬화할지 여부를 설정한다. true인 경우 값이 null인 entry도 직렬화를 수행하고 false인 경우 값이 null인 entry는 직렬화를 수행하지 않는다. JsonInclude.Include.NON_NULL 설정으로 null 값을 직렬화하지 않도록 설정된 경우 WRITE_NULL_MAP_VALUES 설정은 동작하지 않는다. |
true | spring.jackson.serialization.write-null-map-values |
WRITE_EMPTY_JSON_ARRAYS | 빈 컬렉션 및 배열에 대한 직렬화 여부를 설정한다. true인 경우 빈 컬렉션 혹은 배열에 대해서도 직렬화를 수행하고 false인 경우 직렬화를 수행하지 않는다. |
true | spring.jackson.serialization.write-empty-json-arrays |
WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED | 배열 요소가 단일인 경우 배열/컬렉션이 JSON 배열이 아닌 언래핑된 요소로 직렬화할지 여부를 설정한다. true인 경우 단일 요소 배열/컬렉션에 대해서 언래핑된 요소로 직렬화한다. true인 경우 역직렬화시 ACCEPT_SINGLE_VALUE_AS_ARRAY 설정이 true로 지정되어야 한다. |
false | spring.jackson.serialization.write-single-elem-arrays-unwrapped |
WRITE_BIGDECIMAL_AS_PLAIN | true인 경우 과학적 표기법을 사용하지 않으며, false인 경우 과학정 표기법을 사용한다. | false | spring.jackson.serialization.write-bigdecimal-as-plain |
WRITE_DATE_TIMESTAMP_AS_NANOSECONDS | 시간을 타임스탬프로 직렬화할 때 나노초 타임스탬프까지 직렬화 할지 여부를 설정한다. false인 경우 밀리초 타임스탬프가 사용된다. Java8 이후에 지원되는 날짜/시간 클래스에 대해서 이 설정을 지원한다. Java8 이전의 날짜/시간 타입(Date, Calendar, Joda date/time)은 이를 지원할 만한 해상도가 없다. |
true | spring.jackson.serialization.write-date-timestamp-as-nanoseconds |
ORDER_MAP_ENTRIES_BY_KEYS | 직렬화 전에 java.util.Map entry를 키별로 먼저 정렬할지 여부를 설정한다. true인 경우 정렬 단계가 수행된 후 (SortedMap 에는 필요하지 않음) 직렬화를 수행한다. |
false | spring.jackson.serialization.order-map-entries-by-keys |
DeserializationFeature (역직렬화 속성)
ObjectMapper 클래스의 enable(feature), disable(feature), configure(feature, state) 메서드를 사용하여 ObjectMapper의 기본값을 변경할 수 있다.
Jackson2ObjectMapperBuilder 클래스는 featureToEnable(feature), featureToDisable(feature) 메서드를 통해서 ObjectMapper에 속성을 설정할 수 있다.
속성 | 설명 | 디폴트 | 속성 매핑 |
USE_BIG_DECIMAL_FOR_FLOATS | JSON의 부동소수점 숫자를 역직렬화 할 때 결과 타입이 일반 타입(java.lang.Object) 혹은 일반 숫자 타입(java.lang.Number) 일 때 java.math.BigDecimal 타입을 사용할지 여부를 제어한다. (false인 경우 java.lang.Double이 됨) | false | spring.jackson.deserialization.use-big-decimal-for-floats |
USE_BIG_INTEGER_FOR_INTS | 매핑할 값이 JSON 정수일 때 사용된다. true인 경우 일반 타입에 바인딩하기 위해 java.math.BigInteger 타입이 사용되며, false인 경우 실제 값을 포함할 수 있는 가장 작은 타입인 java.lang.Integer 혹은 java.lang.Long 타입이 사용된다. |
false | spring.jackson.deserialization.use-big-integer-for-ints |
USE_JAVA_ARRAY_FOR_JSON_ARRAY | JSON 배열을 java.util.List 로 바인딩할지 Object[] 로 바인딩할지 결정한다. false인 경우 java.util.List로 true인 경우 Object[]로 바인딩 한다. | false | spring.jackson.deserialization.use-java-array-for-json-array |
READ_ENUMS_USING_TO_STRING | Enum 타입을 역질렬화하는 방식을 제어하는 옵션이다. false인 경우 Enum.name()을 사용한다. JSON의 문자열 값이 Enum 상수의 이름과 일치해야 한다. true인 경우 Enum.toString() 메서드를 사용한다. Enum 클래스에서 toString() 메서드를 override 해서 사용자 지정 문자열을 반환할 수 있다. JSON 문자열 값이 toString()의 반환값과 일치하면 역직렬화가 성공한다. | false | spring.jackson.deserialization.read-enums-using-to-string |
ACCEPT_SINGLE_VALUE_AS_ARRAY | JSON 배열이 아닌 값을 단일 요소 배열 및 컬렉션으로 자동 변환 하는 옵션이다. 일부 라이브러리 및 프레임워크는 단일 요소 배열을 직렬화할 때 JSON 배열이 아닌 단일 요소로 표현하는 경우가 있어 상호 운용성을 위해 필요한 경우가 있다. | false | spring.jackson.deserialization.accept-single-value-as-array |
UNWRAP_ROOT_VALUE | 직렬화에 사용되는 WRAP_ROOT_VALUE의 설정과 일치하도록 루트 수준 JSON 값을 unwrapping 하는 기능이다. true인 경우 역직렬화시 JSON root 키를 무시하고 그 안의 객체 데이터만 바인딩한다. false인 경우 JSON에 root키가 있으면 예외가 발생할 수 있다. | false | spring.jackson.deserialization.unwrap-root-value |
UNWRAP_SINGLE_VALUE_ARRAYS | JSON 배열에 하나의 값만 포함되어 있을 때 해당 값을 단일 값으로 변환하여 필드에 바인딩할 지여부를 결정한다. 수신되는 JSON 데이터 필드가 단일 값과 배열 형태로 불규칙적으로 수신될 때 역직렬화 객체의 필드 바인딩 타입에 List 타입으로 지정하고 이 속성을 true로 지정하면 모두 처리가 가능하다. | false | spring.jackson.deserialization.unwrap-single-value-arrays |
ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT | JSON 배열 값이 비어있는 경우 NULL 값으로 바인딩할지 여부를 설정한다. true인 경우 빈 배열을 null 로 처리하며, false인 경우 빈 배열을 empty 컬렉션으로 바인딩한다. | false | spring.jackson.deserialization.accept-empty-array-as-null-object |
ACCEPT_EMPTY_STRING_AS_NULL_OBJECT | JSON 문자열 값이 비어있는 경우 NULL 값으로 바인딩할지 여부를 설정한다. true인 경우 빈 문자열을 null로 바인딩하며, false인 경우 빈 문자열을 빈 문자열로 바인딩한다. | false | spring.jackson.deserialization.accept-empty-string-as-null-object |
ACCEPT_FLOAT_AS_INT | 부동 소수점 값을 정수 타입 값으로 바인딩할지 여부를 설정한다. true인 경우 부동 소수점 값을 정수 타입으로 바인딩하며, false인 경우 부동 소수점 값을 부동 소수 타입으로 바인딩한다. | true | spring.jackson.deserialization.accept-float-as-int |
ADJUST_DATES_TO_CONTEXT_TIME_ZONE | JSON 값 자체에 시간대 정보가 포함되어 있더라도 역직렬화시 날짜/시간 값을 조정할 때 java.util.TimeZone (SerializerProvider#getTimeZone())을 사용할지 여부를 지정하는 기능이다. true인 경우 컨텍스트 TimeZone이 기본적으로 다른 모든 시간대 정보보다 우선하며, false인 경우 값 자체에 시간대 정보가 포함되어 있지 않은 경우에만 사용된다. 이 설정을 통해서 JSON에서 제공된 날짜/시간이 특정 시간대 정보를 포함하지 않거나 UTC로 되어 있을 때, 애플리케이션에서 사용하는 기본 시간대에 맞춰 자동 변환할 수 있다. | true | spring.jackson.deserialization.adjust-dates-to-context-time-zone |
READ_DATE_TIMESTAMPS_AS_NANOSECONDS | 타임스탬프 값을 나노초 타임스탬프를 사용하여 바인딩할지 여부를 설정한다. false인 경우 표준 밀리초 타임스탬프가 바인딩 된다. | false | spring.jackson.deserialization.read-date-timestamps-as-nanoseconds |
READ_UNKNOWN_ENUM_VALUES_AS_NULL | 정의되지 않은 Enum 값을 null로 바인딩할지 여부를 설정한다. false인 경우 정의되지 않은 Enum 값은 예외를 발생시킨다. | false | spring.jackson.deserialization.read-unknown-enum-values-as-null |
READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE | 정의되지 않은 Enum 값을 무시하고 미리 디폴트로 지정된 값을 사용할지 여부를 설정한다. (Enum 클래스에 @JsonEnumDefaultValue 어노테이션을 사용하여 디폴트 값을 지정한다.) ex) public enum Status { ACTIVE, INACTIVE, @JsonEnumDefaultValue // 기본값 설정 UNKNOWN } |
false | spring.jackson.deserialization.read-unknown-enum-values-using-default-value |
FAIL_ON_IGNORED_PROPERTIES | 명시적으로 무시되는 프로퍼티가 발견되면 예외를 발생시킬지 여부를 설정한다. true 인 경우 무시된 프로퍼티가 발견되면 JsonMappingException이 발생하고 false면 조용히 바인딩을 건너뛴다. | false | spring.jackson.deserialization.fail-on-ignored-properties |
FAIL_ON_UNKNOWN_PROPERTIES | 알 수 없는 프로퍼티 (setter가 없는 프로퍼티)를 만나면 예외를 발생시킬지 여부를 설정한다. true인 경우 알 수 없는 프로퍼티가 발견되면 JsonMappingException이 발생하고 false면 조용히 무시한다. | true | spring.jackson.deserialization.fail-on-unknown-properties |
FAIL_ON_INVALID_SUBTYPE | 유효하지 않은 subtype이 발견되면 예외를 발생시킬지 여부를 설정한다. true인 경우 JSON 데이터의 타입 정보가 유효하지 않거나 해당 서브타입이 정의되지 않은 경우 예외를 발생시킨다. false인 경우 JSON 데이터의 타입 정보가 유효하지 않거나 해당 서브타입이 정의되지 않은 경우 무시한다. 예외가 발생하지 않지만 데이터가 불안정하게 바인딩 될 수 있다. | true | spring.jackson.deserialization.fail-on-invalid-subtype |
FAIL_ON_NULL_FOR_PRIMITIVES | JSON 값이 null 인 경우 자바 기본 타입(int, boolean, long..) 에 허용되는지 여부를 설정한다. true로 설정하면 JsonProcessingException이 발생하고 false로 설정하면 기본값이 사용된다. | false | spring.jackson.deserialization.fail-on-null-for-primitives |
FAIL_ON_NUMBERS_FOR_ENUMS | JSON 값이 정수인 경우 Java Enum 타입으로 역직렬화 할 수 있는지 여부를 설정한다. true인 경우 JSON 정수 값을 Enum으로 역직렬화 하려고 하면 JsonProcessingException이 발생하고 false인 경우 Enum.ordinal()과 일치하는 값으로 바인딩된다. | false | spring.jackson.deserialization.fail-on-numbers-for-enums |
FAIL_ON_READING_DUP_TREE_KEY | JSON에 중복된 속성 이름이 있을 때 JSON 트리 (JsonNode)로 구문 분석시 발생하는 상황에 대해서 설정한다. true인 경우 예외가 발생하고 false인 경우 예외가 발생하지 않고 마지막 값이 사용된다. | false | spring.jackson.deserialization.fail-on-reading-dup-tree-key |
FAIL_ON_UNRESOLVED_OBJECT_IDS | JSON 데이터를 역직렬화 할 때 참조로 사용하는 Object ID가 해결되지 않은 경우 예외를 발생시킬지 여부를 설정한다. true인 경우 JSON 데이터를 역직렬화 할 때 참조된 Object ID가 해결되지 않으면 예외를 발생시킨다. false인 경우 JSON 데이터를 역직렬화 할 때 참조된 Object ID가 해결되지 않으면 예외를 발생시키지 않지만 불완전한 객체가 생성될 수 있다. | true | spring.jackson.deserialization.fail-on-unresolved-object-ids |
FAIL_ON_MISSING_CREATOR_PROPERTIES | JSON 데이터를 역직렬화 할 때 생성자(@JsonCreator) 에서 요구하는 필수 프로퍼티가 JSON에 없을 경우 예외를 발생시킬지 여부를 설정한다. true인 경우 생성자 또는 팩토리 메서드에 정의된 필수 프로퍼티가 누락되었을 때 예외를 발생시킨다. false인 경우 생성자에서 요구하는 필드가 누락되더라도 예외 없이 객체를 생성한다. | false | spring.jackson.deserialization.fail-on-missing-creator-properties |
WRAP_EXCEPTIONS | 역직렬화 중에 발생하는 예외를 wrapping 하여 더 상위의 예외로 감싸서 처리할지 여부를 제어하는 설정이다. true인 경우 예외가 발생할 경우 JsonMappingException 이나 JsonParseException과 같은 더 상위의 Jackson 예외로 래핑하여 던진다. false인 경우 발생한 예외를 래핑하지 않고 발생한 예외를 그대로 던진다. | true | spring.jackson.deserialization.wrap-exceptions |
JSON 사용 Example
JSON Configuration 클래스
Jackson2ObjectMapperBuilderCustomizer 빈을 통해서 원하는 설정을 지정하였다.
package org.example.spring.json.example.configuration;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.boot.autoconfigure.jackson.JacksonProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.TimeZone;
@Configuration
public class JacksonConfig {
@Bean
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer( JacksonProperties jacksonProperties ) {
return builder -> {
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule
.addSerializer( LocalDateTime.class, new LocalDateTimeSerializer( DateTimeFormatter.ofPattern( "yyyy-MM-dd HH:mm:ss" ) ) )
.addDeserializer( LocalDateTime.class, new LocalDateTimeDeserializer( DateTimeFormatter.ofPattern( "yyyy-MM-dd HH:mm:ss" ) ) );
builder
.modules( javaTimeModule )
//time zone 설정
.timeZone( jacksonProperties.getTimeZone() != null? jacksonProperties.getTimeZone() : TimeZone.getTimeZone( "Asia/Seoul" ) )
//WRITE_DATES_AS_TIMESTAMPS false
.featuresToDisable( SerializationFeature.WRITE_DATES_AS_TIMESTAMPS )
//WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS false
.featuresToDisable( SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS )
//INDENT_OUTPUT true
.featuresToEnable( SerializationFeature.INDENT_OUTPUT )
//CLOSE_CLOSEABLE true
.featuresToEnable( SerializationFeature.CLOSE_CLOSEABLE )
//WRITE_CHAR_ARRAYS_AS_JSON_ARRAYS true
.featuresToEnable( SerializationFeature.WRITE_CHAR_ARRAYS_AS_JSON_ARRAYS )
//READ_DATE_TIMESTAMPS_AS_NANOSECONDS false
.featuresToDisable( DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS )
//값이 null인 필드 제외
.serializationInclusion( JsonInclude.Include.NON_NULL );
};
}
//아래 주석을 풀면 Jackson2ObjectMapperBuilderCustomizer 빈은 적용되지 않는다.
/*
@Bean
public ObjectMapper objectMapper(JacksonProperties jacksonProperties) {
ObjectMapper objectMapper = new ObjectMapper();
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule
.addSerializer( LocalDateTime.class, new LocalDateTimeSerializer( DateTimeFormatter.ofPattern( "yyyy-MM-dd HH:mm:ss" ) ) )
.addDeserializer( LocalDateTime.class, new LocalDateTimeDeserializer( DateTimeFormatter.ofPattern( "yyyy-MM-dd HH:mm:ss" ) ) );
objectMapper.registerModules( javaTimeModule );
objectMapper.setSerializationInclusion( JsonInclude.Include.NON_NULL );
objectMapper.setTimeZone( jacksonProperties.getTimeZone() != null? jacksonProperties.getTimeZone() : TimeZone.getTimeZone( "Asia/Seoul" ) );
objectMapper.configure( SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false );
objectMapper.configure( SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, false );
objectMapper.configure( SerializationFeature.INDENT_OUTPUT, true );
objectMapper.configure( SerializationFeature.CLOSE_CLOSEABLE, true );
objectMapper.configure( SerializationFeature.WRITE_CHAR_ARRAYS_AS_JSON_ARRAYS, true );
objectMapper.configure( DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, false );
return objectMapper;
}
*/
}
JSON serialization, deserialization 대상 클래스 정의
package org.example.spring.json.example.data;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Closeable;
import java.io.IOException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Date;
import java.util.Map;
@Data
@NoArgsConstructor
public class MutableSampleDTO implements Closeable {
private String id;
private String name;
private LocalDateTime localDateTime;
private Instant instantTime;
private Date date;
private char[] chars;
private JavaTypes javaTypes;
private Map<String, String> mapValue;
private Collection<String> collectionValue;
@Builder
public MutableSampleDTO( String id,
String name,
LocalDateTime localDateTime,
Instant instantTime,
Date date,
char[] chars,
JavaTypes javaTypes,
Map<String, String> mapValue,
Collection<String> collectionValue ) {
this.id = id;
this.name = name;
this.localDateTime = localDateTime;
this.instantTime = instantTime;
this.date = date;
this.chars = chars;
this.javaTypes = javaTypes;
this.mapValue = mapValue;
this.collectionValue = collectionValue;
}
@Override
public void close() throws IOException {
System.out.println("called close");
}
}
Enum 타입 정의
package org.example.spring.json.example.data;
public enum JavaTypes {
STRING("string type"),
INTEGER("integer type"),
LONG("long type"),
OTHER("other types");
final String description;
JavaTypes( String description ) {
this.description = description;
}
@Override
public String toString() {
return "JavaTypes{" +
"description='" + description + '\'' +
'}';
}
}
테스트 코드
package org.example.spring.json.example.data;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.example.spring.json.example.configuration.JacksonConfig;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.ConfigDataApplicationContextInitializer;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import java.time.Instant;
import java.time.LocalDateTime;
import java.util.*;
@ExtendWith( SpringExtension.class )
@ContextConfiguration( initializers = ConfigDataApplicationContextInitializer.class, classes = { JacksonConfig.class } )
@EnableAutoConfiguration
class MutableSampleDTOTest {
// Jackson Auto Configuration에 의해서 ObjectMapper 빈을 주입받아 사용할 수 있다.
@Autowired
ObjectMapper objectMapper;
@Test
@DisplayName( "Serialization/Deserialization 테스트" )
void serialization_deserialization_test() throws JsonProcessingException {
Map<String, String> map = new HashMap<>();
map.put( "key1", "value1" );
map.put( "key2", null );
//set name to null
MutableSampleDTO mutableSampleDTO = MutableSampleDTO.builder()
.id( UUID.randomUUID().toString() )
.name( "test name" )
.instantTime( Instant.now() )
.localDateTime( LocalDateTime.now() )
.date( Date.from(Instant.now()) )
.chars( "a".toCharArray() )
.javaTypes( JavaTypes.STRING )
.mapValue( map )
.collectionValue( Collections.singletonList( "single" ) )
.build();
String jsaonData = objectMapper.writeValueAsString( mutableSampleDTO );
System.out.println( jsaonData );
MutableSampleDTO deserializedMutableSampleDTO = objectMapper.readValue( jsaonData, MutableSampleDTO.class );
System.out.println( deserializedMutableSampleDTO );
}
}
실행 결과
//CLOSE_CLOSEABLE true 설정의 의한 close() 호출.
called close
직렬화 결과
{
"id" : "eba4770e-13ba-4161-8332-f8a798603f64",
"name" : "test name",
"localDateTime" : "2025-01-04 23:07:37",
"instantTime" : "2025-01-04T14:07:37.361014Z",
"date" : "2025-01-04T23:07:37.361+09:00",
"chars" : [ "a" ],
"javaTypes" : "STRING",
"mapValue" : {
"key1" : "value1"
},
"collectionValue" : [ "single" ]
}
역직렬화 결과
MutableSampleDTO(id=eba4770e-13ba-4161-8332-f8a798603f64, name=test name, localDateTime=2025-01-04T23:07:37, instantTime=2025-01-04T14:07:37.361014Z, date=Sat Jan 04 23:07:37 KST 2025, chars=[a], javaTypes=JavaTypes{description='string type'}, mapValue={key1=value1}, collectionValue=[single])
감사합니다.
끝.