티스토리 뷰
기존에 JSON을 객체로 변경하면서 custom 하게 deserialize/serialize 할 일이 있어서 Object Mapper를 빈 등록하여 사용하였습니다. 이렇게 작성을 하고 나니 Object Mapper가 어떻게 동작하는지 꼼꼼하게 알 필요가 있을 것 같아 해당 내용을 정리하려고 합니다.
Object Mapper 란?
JSON 컨텐츠를 Java 객체로 deserialization 하거나 Java 객체를 JSON으로 serialization 할 때 사용하는 Jackson 라이브러리의 클래스입니다.
Jackson 라이브러리는 다음과 같이 의존성 추가할 수 있습니다.
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.4</version>
</dependency>
Java 객체를 JSON으로 serialization 할 때는 다음과 같이 writeValue() 메서드를 사용합니다.
public class ObjectMapperTest {
public static void main(String[] args) {
ObjectMapper objectMapper = new ObjectMapper();
Member member = new Member("Son", 30);
try {
// Member객체에 저장되어 있는 값을 JSON형식으로 변환 후 member.json파일을 생성합니다.
objectMapper.writeValue(new File("member.json"), member);
// Member객체에 저장되어 있는 값을 JSON형식으로 변환합니다.
String memberInfo = objectMapper.writeValueAsString(member);
System.out.println(memberInfo);
} catch (JsonGenerationException e) {
e.printStackTrace();
} catch (JsonMappingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
이때 주의할 점은 Java 클래스에 Getter가 있어야 한다는 것입니다.
@Getter
@ToString
@NoArgsConstructor
public class Member {
private String name;
private int age;
public Member(String name, int age) {
this.name = name;
this.age = age;
}
}
Jackson 라이브러리는 Java 클래스를 JSON으로 직렬화할 때 Getter, Setter를 사용하여 prefix를 잘라내고 맨 앞 글자를 소문자로 만들어서 필드를 식별하기 때문입니다.
JSON을 Java 객체로 deserialization 할 때는 다음과 같이 readValue() 메서드를 사용합니다.
public class ObjectMapperTest2 {
public static void main(String[] args) throws JsonParseException, JsonMappingException, IOException {
String jsonData = "{ \"name\" : \"son\", \"age\" : \"30\" }";
ObjectMapper objectMapper = new ObjectMapper();
// JSON -> Object
// jsonData의 JSON 데이터를 읽어서 Member 객체에 역직렬화했습니다.
Member member = objectMapper.readValue(jsonData, Member.class);
System.out.println(member.toString()); // Member(name=son, age=30)
// JSON File to Object
// member.json 파일의 JSON 데이터를 읽어서 Member 객체에 역직렬화했습니다.
Member member2 = objectMapper.readValue(new File("member.json"), Member.class);
System.out.println(member2.toString()); // Member(name=son, age=30)
// JSON URL to Object
// file:member.json URL에서 JSON 데이터를 읽어서 Member 객체에 역직렬화했습니다.
Member member3 = objectMapper.readValue(new URL("file:member.json"), Member.class);
System.out.println(member3.toString()); // Member(name=son, age=30)
}
}
이때도 주의할 점은 JSON이 변경될 Java 객체에 기본 생성자가 존재해야 한다는 것입니다. 기본적으로 Jackson 라이브러리의 역직렬화는 기본 생성자로 객체를 생성하고 Getter나 Setter를 통해 필드 찾은 뒤 Reflection 방식을 통해 데이터를 지정하기 때문입니다.
그외 객체 생성을 위해 @JsonCreator 등의 애노테이션을 제공하기도 합니다. (이 부분은 추가로 더 공부하고 정리할 예정입니다.)
잠깐!
xxx라는 필드는 없지만 getXXX(), setXXX() 라는 메서드가 있는 경우는 어떻게 될까요?
이때는 의도치 않게 xxx 라는 값이 포함될 수 있기 때문에 메서드 위에 @JsonIgnore 라는 애노테이션을 추가하여 JSON 변환 과정에 포함되지 않게 처리할 수 있습니다.
SpringBoot 와 Object Mapper
Spring Boot에서는 MessageConverter를 통해 요청, 응답 시 타입 변환을 진행합니다. 이때 JSON -> 객체, 객체 -> JSON 변환 과정에서 MessageConverter가 ObjectMapper를 사용하게 됩니다.
Spring Boot 프로젝트 생성 시 spring-boot-starter-web 의존성을 추가하게 되면 자동으로 Jackson 라이브러리가 같이 추가됩니다.
dependencies {
...
implementation 'org.springframework.boot:spring-boot-starter-web'
...
}
이를 잘 이해하기 위해 Spring MVC의 동작 방식을 알 필요가 있습니다.
- Spring은 Request가 들어오면 HandlerMapping을 통해 사용자의 요청과 해당 요청을 처리하는 Hanller를 매핑합니다.
- 그리고 HandlerMethodArgumentResolver를 사용해 Controller의 Argument(Parameter)에 지정된 변수들을 Annotation이나 객체의 Type에 따라서 적절한 Resolver를 거칩니다.
- HandlerArgumentResolver를 상속한 RequestResponseBodyMethodProcessor 를 보면 @RequestBody를 확인하는 것을 볼 수 있습니다. @RequestBody를 사용한 것이 확인이 된다면 read Or write로 넘어가게 됩니다.
- Content-type이 Json인 것을 확인한 후 Jackson Converter(MappingJackson2HttpMessageConverter)를 통해 Json Data를 직렬화합니다. 이 때 사용되는 것이 ObjectMapper입니다.
Object Mapper 에 custom serialize/deserialize를 적용하려면 어떤 방법이 있을까요?
일단 커스텀한 serialize/deserialize는 다음과 같습니다.
@Slf4j
@Component
public class CustomLocalDateTimeSerializer extends JsonSerializer<LocalDateTime> {
@Override
public Class<LocalDateTime> handledType() {
return LocalDateTime.class;
}
@Override
public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeString(value.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
}
}
@Slf4j
@Component
public class CustomLocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {
private final String[] patterns = {"yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", "yyyy-MM-dd"};
@Override
public Class<?> handledType() {
return LocalDateTime.class;
}
@Override
public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
return LocalDateTime.parse(p.getText(), makeDateTimeFormatter());
}
private DateTimeFormatter makeDateTimeFormatter() {
StringBuilder pattern = new StringBuilder();
for (String p : patterns) {
pattern.append("[").append(p).append("]");
}
return new DateTimeFormatterBuilder()
.appendPattern(pattern.toString())
.parseDefaulting(ChronoField.HOUR_OF_DAY, 0)
.parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0)
.parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0)
.toFormatter();
}
}
1) ObjectMapper 를 직접 빈으로 등록
@Configuration
public class ObjectMapperConfig {
@Bean
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(customLocalDateTimeModule());
return objectMapper;
}
@Bean
public SimpleModule customLocalDateTimeModule() {
SimpleModule module = new SimpleModule();
module.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
module.addSerializer(LocalDateTime.class, new CustomLocalDateTimeSerializer());
module.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
module.addDeserializer(LocalDateTime.class, new CustomLocalDateTimeDeserializer());
return module;
}
}
ObjectMapper를 직접 생성하여 빈으로 등록할 때 Module을 추가해주는 방식입니다.
2) Jackson2ObjectMapperBuilderCustomizer 사용
@Configuration
public class ObjectMapperConfig {
@Bean
public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() {
return builder -> {
builder.simpleDateFormat(LOCAL_DATE_TIME_FORMAT);
builder.serializers(
new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")),
new CustomLocalDateTimeSerializer()
);
builder.deserializers(
new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")),
new CustomLocalDateTimeDeserializer()
);
};
}
}
기존에 제공하던 ObjectMapper에 설정을 변경하는 대신 custom 한 serializer/deserializer만 추가할 수 있습니다.
기존에 이미 ObjectMapper를 사용하여 처리를 하고 있던 경우라고 한다면 새로운 ObjectMapper를 생성하고 Module을 추가하는 방식 대신 두번째 방식을 사용하는 것이 설정 측면에서 더 안정성이 있다고 할 수 있습니다.
지금까지 ObjectMapper를 통해 객체와 JSON 간의 데이터 타입 변환에 대해 알아보았습니다. 이 부분을 공부하다보니 Spring MVC의 동작 방식에 대해 더 깊이 있게 알아야겠다고 생각했고 애노테이션 처리, ArgumentResolver에 대해서도 조금 더 자세히 알아야겠다고 생각했습니다.
다음 글에서는 ArgumentResolver와 함께 @RequestParam, @RequestBody 등이 어떤 식의 변환 과정을 거치는지 조금 더 깊게 살펴보려고 합니다.
참고
- https://www.baeldung.com/jackson-object-mapper-tutorial
- https://da-nyee.github.io/posts/woowacourse-why-the-default-constructor-is-needed/
- https://kim-jong-hyun.tistory.com/60
- https://steady-hello.tistory.com/124
- https://happy-coding-day.tistory.com/entry/SpringMVC-%EC%97%90%EC%84%9C-%EB%A7%90%ED%95%98%EB%8A%94-MessageConverter-%EC%BD%94%EB%93%9C%EB%A1%9C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0
'Spring' 카테고리의 다른 글
[EventListener] 스프링에서 이벤트 발행과 구독 (0) | 2024.03.20 |
---|---|
[MessageSource] messages.properties 를 활용한 에러 메시지 처리 (1) | 2023.11.21 |
[Jackson DataFormat] String To LocalDateTime Serialize (0) | 2023.11.16 |
[Spring Error] SpringBoot 예외 처리 (1) | 2023.10.15 |
[Spring Restdocs] Enum 목록 표시 및 popup 으로 확인 (0) | 2023.01.30 |
- Total
- Today
- Yesterday
- BFS
- 소수
- SWIFT
- permutation
- DFS
- map
- array
- search
- EC2
- 프로그래머스
- ECR
- Algorithm
- spring
- 순열
- CodeCommit
- AWS
- programmers
- java
- sort
- string
- CodePipeline
- 수학
- CodeDeploy
- Combination
- 조합
- 에라토스테네스의 체
- Baekjoon
- Dynamic Programming
- cloudfront
- ionic
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |