Spring MVC를 이용해 API를 개발할 때, Http Message body에 있는 Json 데이터를 가져오기 위해 주로 @RequestBody를 사용해 Json을 자바 객체로 변환한다.
이와 관련해서 Json으로 변환되는 객체는 기본 생성자만 있고 Setter가 없어도 변환 가능하다고 외우고 사용했다. 그러다 내부적으로 어떻게 동작하는지 궁금증이 생겼다. 그래서 동작 원리를 여러 글과 코드를 통해 공부했다. 공부한 내용을 공유하면 다른 분들도 재밌게 볼 수 있을 것 같아 글을 쓰게 됐다.
@RequestBody 동작 원리
Spring MVC에서 요청이 들어와 @RequestBody를 처리하는 converter를 찾을 때까지 많은 과정이 있다. 이 글은 @RequestBody가 어떤 원리로 Json을 자바 객체로 변환시키는지가 주제이기 때문에 위의 과정은 생략하겠다. (자세한 내용은 해당 글을 참고하면 도움이 될 것 같다.)
결론은 @RequestBody의 변환 작업은 MappingJackson2HttpMessageConverter가 책임진다. MappingJackson2HttpMessageConverter는 자신의 부모 클래스인 AbstractJackson2HttpMessageConverter의 read 메서드를 이용해 Json을 자바 객체로 변환한다.
AbstractJackson2HttpMessageConverter의 read 메서드에서는 ObjectMapper를 사용해 Json을 자바 객체로 변환한다.
이를 통해 Json이 자바 객체로 변환되는 비밀은 ObjectMapper에 숨어 있다는 사실을 예측할 수 있다.
ObjectMapper의 동작원리
ObjectMapper의 readValue 메서드는 자신의 _readMapAndClose 메서드를 호출한다.
ObjectMapper의 _readMapAndClose 메서드는 무슨 역할을 할까?
ObjectMapper의 _readMapAndClose 메서드는 역직렬화 관련 설정 작업을 수행하고, DefaultDeserializationContextImpl 객체에 Json에서 자바 객체로의 변환 과정을 위임한다.
DefaultDeserializationContext의 readRootValue 메서드에는 무슨 로직이 들어있을까?
DefaultDeserializationContext의 readRootValue 메서드는 Deserializer 객체의 deserialize 메서드를 이용해 역직렬화를 진행한다. 여기서 Deserializer의 구현체로 BeanDeserializer가 사용된다.
거의 다 왔다. 그럼 이제 BeanDeserializer에서 변환 작업이 이뤄질 것을 예측해 볼 수 있다. 한번 자세히 살펴보자.
BeanDeserializer의 deserialize 메서드는 자신의 deserializeFromObject 메서드를 호출한다.
BeanDeserializer의 deserializeFromObject 메서드를 분석하면 드디어 기본 생성자가 왜 필요한지 알 수 있다.
deserializeFromObject 메서드의 로직은 변환하고자 하는 객체의 생성자 여부에 따라 다른 로직이 수행된다. 변환하고자 하는 객체에 기본 생성자가 있으면 아래 그림처럼 기본 생성자를 이용해 빈 객체를 생성한다. (기본 생성자는 없고, 매개변수가 있는 생성자만 존재하면 deserializeFromObjectUsingNonDefault 메서드가 호출된다.)
여기까지 오면 자바 객체의 기본 생성자를 통해 아무 값도 설정되지 않은 기본 객체가 생성됐다는 것을 알 수 있다. 그럼 객체의 프로퍼티에 어떻게 값이 할당될까?
BeanDeserializer 객체의 _beanProperties 필드를 이용해 Json 데이터와 이름이 일치하는 프로퍼티 정보를 가져온다. 만약 자바 객체에 setter가 없다면, FieldProperty 객체가 반환된다. (왜 FieldProperty가 반환되는지는 뒤에 BeanDeserializer가 생성되는 과정을 보면 알 수 있다.)
그리고 FieldProperty 객체의 deserializeAndSet() 메서드를 통해 객체의 프로퍼티에 값이 할당된다.
FieldProperty의 deserializeAndSet 메서드에서는 내부적으로 리플렉션을 이용해 프로퍼티에 값을 할당한다. 150번 라인이 리플렉션을 이용해 객체의 프로퍼티가 설정되는 과정이다.
이런 과정을 거치면 Json의 데이터들이 자바 객체의 프로퍼티로 맵핑된다. 다시 말하면, 변환하고자 하는 객체에 기본 생성자만 있으면 Setter가 없어도 변환 가능하다.
그럼 이제 BeanDeserializer 객체의 _beanProperties 필드가 어떻게 생성되는지 알아보자.
BeanDeserializer 생성 원리
BeanDeserializer는 BeanDeserializerFactory의 buildBeanDeserializer 메서드에 의해 생성된다. 그리고 _beanProperties는 addBeanProps 메서드에서 결정된다.
addBeanProps의 로직은 역직렬화할 자바 객체의 Setter, Field, Getter 순으로 탐색해 프로퍼티 정보를 가져온다. 이때 자바 객체에 Setter가 없으면 Field를 보고 FieldProperty 객체가 만들어진다. (여기서 만들어진 FieldProperty를 프로퍼티 값을 세팅하는 과정에 사용한다.)
정리
나는 @RequestBody를 사용할 때 기본 생성자만 있고, Setter는 없어도 된다고 알고 있었다. 이 비밀은 BeanDeserializer 로직에 숨어있었다.
BeanDeserializer는 변환하고 싶은 객체의 기본 생성자 여부에 따라 역직렬화 로직을 진행한다. 기본 생성자가 있으면 기본 생성자를 이용해 빈 자바 객체를 만든다. 그리고 자신의 필드인 _beanProperties를 이용해 Json의 값과 객체의 프로퍼티를 매핑 시킨다.
이때 Setter가 없어도 되는 이유는 BeanDeserializer가 생성되는 시점에 Setter가 없으면 Field나 Getter를 보고 프로퍼티 정보를 가져올 수 있기 때문이다. 그리고 Field 정보를 보고 프로퍼티를 가져올 경우, 리플렉션을 이용해 Json 값과 객체의 프로퍼티를 매핑 시킨다.
참조
Spring-webmvc:5.3.15
Jackson: 2.13.1
'Spring' 카테고리의 다른 글
Spring's STOMP support 파헤치기 (0) | 2023.06.02 |
---|---|
Spring Websocket Trobleshooting!! (feat. Decorator Pattern) (0) | 2023.05.21 |
Spring Cloud GateWay의 non-blocking server (0) | 2022.12.31 |
@SpringBootTest의 비밀! (0) | 2022.12.11 |
@JdbcTest와 @DataJdbcTest (0) | 2022.05.21 |