티스토리 뷰

Spring

[Spring] ObjectMapper의 동작 방식과 SpringBoot가 제공하는 추가 기능들

망나니개발자 2023. 1. 10. 10:00
반응형

이번에는 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 문자열을 객체로 받아올 때 역직렬화가 처리된다.

 

 

 

역직렬화는 기본적으로 다음과 같은 과정을 거쳐서 처리된다.

  1. 기본 생성자로 객체를 생성함
  2. 필드값을 찾아서 값을 바인딩 해줌

 

 

가장 먼저 객체를 생성하는데, 기본 생성자가 없다면 에러를 발생시킨다. 기본 생성자로 객체를 생성한 후에는 필드값을 찾아야하는데, 기본적으로 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;

}

 

 

 

 

 

 

 

 

 

 

반응형
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG more
«   2024/12   »
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
글 보관함