티스토리 뷰
이번에는 Spring에서 사용되는 ObjectMapper의 동작 방식에 대한 정리해보도록 하겠습니다.
1. ObjectMapper를 이용한 직렬화(Serialize)
[ ObjectMapper의 직렬화(Serialize) 동작 방식 ]
ObjectMapper는 리플렉션을 활용해서 객체로부터 Json 형태의 문자열을 만들어내는데, 이것을 직렬화(Serialize)라고 한다. 해당 부분은 @ResponseBody나 @RestController 또는 ResponseEntity 등을 사용하는 경우에 처리된다.
Spring에서는 기본적으로 jackson 모듈의 ObjectMapper라는 클래스가 직렬화를 처리한다. 그리고 그 과정에서 ObjectMapper의 writeValueAsString이라는 메소드가 사용된다.
String jsonResult = objectMapper.writeValueAsString(myDTO());
객체로부터 Json 문자열을 만들기 위해서는 필드 값을 알아야 한다. 그래야만 다음과 같은 문자열을 만들 수 있다.
String message = "{\"name\":\"MangKyu\",\"age\":20}"
하지만 ObjectMapper의 기본 설정으로는 public 필드 또는 public 형태의 getter(getX로 시작하는 메소드)만 접근이 가능하다. 물론 ObjectMapper에 추가 설정을 통해 가시성을 높여줄 수 있지만, getter가 없는 경우는 거의 없으므로 기본 설정으로 사용해도 충분하다. 그러므로 ObjectMapper를 이용하는 경우, 직렬화를 위해 기본적으로 getter를 반드시 만들어두는 것이 좋다.
[ ObjectMapper를 이용한 직렬화의 주의 사항 ]
이러한 ObjectMapper의 직렬화 동작 방식 때문에 때론 의도치 않은 json 메세지를 만들어 내기도 한다. 예를 들어 DTO에 다음과 같이 getX로 시작하는 메소드를 만들었다고 하자.
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class MangKyuRequest {
private String name;
private Integer age;
public String getNameWithAge() {
return name + "(" + age + ")";
}
}
위와 같은 클래스의 객체를 ObjectMapper로 직렬화하면 다음과 같은 json 문자열이 만들어진다. getNameWithAge 역시 getX로 시작하는 getter 메소드 규칙을 따르기 때문이다. 만약 이러한 부분을 인지하지 못한다면 잘못된 json 응답을 내려줄 수 있다. 그러므로 ObjectMapper의 역직렬화의 동작 방식을 알고 있는 것은 도움이 될 수 있다.
{"name":"MangKyu","age":20,"nameWithAge":"MangKyu(20)"}
2. ObjectMapper를 이용한 역직렬화(Deserialize)
[ ObjectMapper의 역직렬화(Deserialize) 동작 방식 ]
ObjectMapper는 리플렉션을 활용해서 Json 문자열로부터 객체를 만들어내는데, 이것을 역직렬화(Deserialize)라고 한다. Spring에서 @RequestBody로 json 문자열을 객체로 받아올 때 역직렬화가 처리된다.
역직렬화는 기본적으로 다음과 같은 과정을 거쳐서 처리된다.
- 기본 생성자로 객체를 생성함
- 필드값을 찾아서 값을 바인딩 해줌
가장 먼저 객체를 생성하는데, 기본 생성자가 없다면 에러를 발생시킨다. 기본 생성자로 객체를 생성한 후에는 필드값을 찾아야하는데, 기본적으로 public 필드 또는 public 형태의 getter/setter로 찾을 수 있다. 만약 처리에 실패하면 예외가 발생하게 되므로 기본 생성자와 getter 메소드는 반드시 만들어주는 것이 좋다.
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `com.mang.atdd.membership.objectmapper.MyDTO` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
at [Source: (String)"{"name":"MangKyu","age":20}"; line: 1, column: 2]
at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67)
at com.fasterxml.jackson.databind.DeserializationContext.reportBadDefinition(DeserializationContext.java:1904)
at com.fasterxml.jackson.databind.DatabindContext.reportBadDefinition(DatabindContext.java:400)
at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1349)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1415)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:352)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:185)
at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:323)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4674)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3629)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3597)
[ 우회적으로 역직렬화 처리하기 ]
만약 기본 생성자가 아닌 우회적인 방법으로(전체 생성자 등) 객체를 만드려면 별도의 2가지 작업이 필요하다.
- ObjectMapper에 ParameterNames 모듈 추가
- Java 컴파일의 -parameters 옵션 추가
ObjectMapper를 위한 Jackson 모듈에는 다음과 같은 parameter-names 모듈이 있다.
// <https://mvnrepository.com/artifact/com.fasterxml.jackson.module/jackson-module-parameter-names>
implementation group: 'com.fasterxml.jackson.module', name: 'jackson-module-parameter-names'
해당 모듈을 ObjectMapper에 등록을 하면 ObjectMapper가 우회적인 방법을 사용할 수 있게 된다. 예를 들면 파라미터가 있는 생성자와 같이 파라미터 정보를 기반으로 하는 정보들이 사용 가능해진다.
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new ParameterNamesModule());
그러면 이제 파라미터 정보를 얻어올 수 있어야 ParameterNamesModule을 활용할 수 있는데, 해당 부분은 자바 컴파일의 parameters 옵션을 사용하면 된다. 자바 컴파일에 parameters 옵션을 추가해주면 JDK8 이상에서는 컴파일 시에 Reflection API로 파라미터 정보를 가지고 올 수 있도록 컴파일된 클래스에 정보를 추가해준다.
참고로 IntelliJ의 parameters 옵션은 다음과 같이 추가해줄 수 있으며, 반드시 Build > Rebuild Projects를 해주어야 반영이 된다.
[ SpringBoot가 제공해주는 추가 기능들 ]
하지만 Gradle(그레이들) 기반의 스프링 부트로 개발을 하다 보면 기본 생성자가 없는 경우에도 에러 없이 객체가 정상적으로 만들어지는 경험을 할 수 있다. 왜냐하면 그 이유는 SpringBoot가 추가적인 설정과 플러그인을 제공해주기 때문이다.
먼저 parameters 옵션은 SpringBoot가 제공하는 Gradle의 java 플러그인 때문이 처리해준다. 해당 플러그인을 사용하면 Java 컴파일의 -parameters 옵션이 자동 추가된다. 관련 코드는 여기서 확인할 수 있다.
plugins {
id 'org.springframework.boot' version '2.7.5'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
ParameterNames 모듈을 추가하는 부분은 Jackson을 AutoConfigure(자동 설정)하는 과정에서 처리된다. 다른 모듈들과 마찬가지로 해당 모듈의 의존성이 있으면 자동으로 설정을 추가해준다. 관련 코드는 여기서 확인할 수 있다.
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ParameterNamesModule.class)
static class ParameterNamesModuleConfiguration {
@Bean
@ConditionalOnMissingBean
ParameterNamesModule parameterNamesModule() {
return new ParameterNamesModule(JsonCreator.Mode.DEFAULT);
}
}
3. 결론: DTO 클래스는 항상 다음과 같이 사용하라
[ 결론: DTO 클래스는 항상 다음과 같이 사용하라 ]
위에서 설명한 부분을 항상 머리에 담고 생각하며 개발하는 것은 비효율적이다. 그리고 이러한 것은 뇌에 불필요한 인지 부하를 줄 뿐이다. 그래서 내 나름대로 한 가지 규칙을 만들었는데, DTO에는 다음과 같은 코드를 무지성으로 붙여주는 것이다. 그러면 우리는 부담없이 DTO에 일관된 방식을 제공해줄 수 있다. 만약 @ModelAttribute 사용이 필요하다면 무지성으로 @Setter까지 넣어주면 된다.
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MangKyuRequest {
private String name;
private Integer age;
}
'Spring' 카테고리의 다른 글
[Spring] OpenFeign에 Resilence4J 서킷 브레이커 적용하는 방법과 예시 및 주의사항 (0) | 2023.03.21 |
---|---|
[Spring] Jasypt를 이용한 Yaml 또는 Properties 프로퍼티 설정 파일의 리소스 암호화 (6) | 2023.02.28 |
[Spring] POJO 프로그래밍과 스프링 프레임워크의 탄생 (8) | 2022.11.29 |
[Spring] OpenFeign 타임아웃(Timeout), 재시도(Retry), 로깅(Logging) 등 설정하기 (11) | 2022.11.22 |
[Spring] OpenFeign이란? OpenFeign 소개 및 사용법 (8) | 2022.11.15 |