도입
회사 데이터베이스에 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);
}
}
}
'프로젝트 > 트러블슈팅' 카테고리의 다른 글
@RedisHash 주의해서 사용하기 (2) | 2024.11.10 |
---|---|
[트러블 슈팅] GitHub Actions를 활용해 슬랙봇으로 PR 자동화하기 (1) | 2024.10.13 |
[트러블 슈팅] 인덱스 컨디션 푸시다운, 인덱스를 이용한 정렬, 커버링 인덱스로 슬로우 쿼리 튜닝하기 (1) | 2024.02.13 |
[트러블 슈팅] n + 1 문제를 IN절로 해결하기 (0) | 2024.02.12 |
[트러블 슈팅] 리팩토링을 통한 복잡했던 모듈 구조를 단순화 (0) | 2024.02.06 |