본문 바로가기

프로젝트/트러블슈팅

ObjectMapper는 스프링 빈으로 주입받기

도입

회사 데이터베이스에 JSON 형태의 데이터를 많이 저장하고 있다. 이 데이터를 String, Map, JsonNode로 받아서 DTO로 변환하는 작업을 수행해야 한다. 

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class Mapper {
    
    private static final ObjectMapper objectMapper = new ObjectMapper();
}

 

따라서 위 코드처럼 new 키워드로 인스턴스화한 ObjectMapper를 이용해 POJO 객체로 변환했다. 하지만 팀원이 ObjectMapper는 인스턴스화 할 때와 스프링 빈으로 주입받아서 사용할 때 설정값이 다르므로 주의해야한다고 말씀해주셨다.

 

이번 글에서 SpringBoot에서 ObjectMapper를 자동 주입할 때 어떤 설정을 하는지 알아보자. Spring Boot 버전은 2.7.18이다.

 

직렬화와 역직렬화 할 때 설정값의 차이

ObjectMapper는 직렬화할 때 그리고 역직렬화할 때 설정값이 다르다. ObjectMapper의 직렬화 설정 구성은

com.fasterxml.jackson.databind 패키지 내 SerializationConfig 클래스를 통해 알 수 있다. 그리고 역직렬화 설정 구성은 같은 패키지 내 DeserializationConfig 클래스를 통해 알 수 있다.

 

먼저 직렬화 설정의 차이는 아래와 같다.

SerializationFeature Spring Bean new 키워드로 인스턴스 화
WRITE_DATES_AS_TIMESTAMPS false true (default)
WRITE_DURATIONS_AS_TIMESTAMPS false true (default)

 

WRITE_DATES_AS_TIMESTAMPS는 true이면 날짜를 숫자형의 타임스탬프로 변환한다. 반대로 false이면 "yyyy-MM-dd'T'HH:mm:ss.SSSX" 형식의 문자열로 변환한다.

WRITE_DURATIONS_AS_TIMESTAMPS는 true이면 기간과 범위를 나타내는 java.time 패키지의 Duration 클래스를 숫자로 표현한다. 반대로 false이면 문자열로 표시한다.

 

예시로 자세히 확인해보자.

@Getter
@NoArgsConstructor
@AllArgsConstructor
public class Vacation {
    private Long id;
    private LocalDateTime startedAt;
    private Duration duration;
}

 

위와 같은 Vacation이란 POJO를 ObjectMapper의 writeValueAsString을 호출하여 JSON 형태의 문자열로 만들면 아래와 같이 생성된다.

// new 키워드로 인스턴스화
{"id":1,"startedAt":[2024,11,24,22,6,18,789141000],"duration":864000.000000000}
// Spring으로 주입받은 Bean
{"id":1,"startedAt":"2024-11-24T22:06:18.789141","duration":"PT240H"}

 

다음으로 역직렬화 설정의 차이는 아래와 같다.

DeserializationFeature Spring Bean new 키워드로 인스턴스 화
FAIL_ON_UNKNOWN_PROPERTIES false true (default)

 

이름 그대로 존재하지 않는 프로퍼티에 대해 에러를 던질지 혹은 무시할지 결정한다. 인스턴스화한 ObjectMapper는 UnrecognizedPropertyException을 던진다.

String json = "{\"id\": 1, \"unknown\": true }";
ObjectMapper objectMapper = new ObjectMapper();
// 🚨🚨 에러 발생 🚨🚨
Vacation vacation = objectMapper.readValue(json, Vacation.class);
com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "unknown" (class com.spring.study.ApplicationTests$Vacation), not marked as ignorable (3 known properties: "id", "startedAt", "duration"])

 

반대로 스프링 빈으로 주입받은 ObjectMapper는 존재하지 않는 프로퍼티에 대해 무시하고 역직렬화를 수행한다.

한 곳에서만 주입받기

날짜의 경우 숫자형의 타임스탬프보다 문자열이 가독성이 높다. 그리고 JSON 데이터의 경우 정형 데이터가 아니기 때문에 역직렬화 대상인 객체와 프로퍼티가 일치하지 않을 수 있다. 따라서 ObjectMapper는 스프링 빈으로 주입받아 사용하기로 결정했다.

 

하지만 JSON 데이터와 POJO를 직렬화 역직렬화할 때마다 ObjectMapper를 매번 주입받아 사용해야하고, JsonProcessingException, IOException이라는 Checked Exception에 대해서 try-catch로 처리해야 했다. 이러한 중복을 없애고자 한 곳에서만 주입받아 Util 처럼 사용하였다.

 

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class ObjectConverter {

    private static ObjectMapper objectMapper;

    private static void init(ObjectMapper objectMapper) {
        ObjectConverter.objectMapper = objectMapper;
    }
    
    @RequiredArgsConstructor
    @Component
    static class InitObjectConverter {

        private final ObjectMapper objectMapper;

        @PostConstruct
        public void setup() {
            ObjectConverter.init(objectMapper);
        }
    }
}