Spring에서 @RequestBody로 전달받는 객체의 필드에 final 키워드를 붙일 수 있을까?

2023. 4. 18. 16:55자바/스프링 삽질 기록

728x90
반응형

앞으로는 미션을 진행하며 피드백을 받거나 궁금했던 내용 중 조금 깊게 찾아봤던 내용들에 대해 한번 글을 작성해보려고 한다.(그래서 잘못 작성된 내용도 좀 있을 수 있다. ㅎㅎ) 그 중 첫번째는 바로 @RequestBody 어노테이션에 관한 내용이다. ㅎㅎ

 

이번에 웹 자동차 게임 미션을 진행하고 가장 첫번째로 받았던 질문은 다음과 같다.

 

 

참고로 위에 내가 한 답변 중에 틀린 내용도 있었다. ㅎㅎ

 

질문을 받은 코드는 다음과 같다.

 

 

GameRequest 클래스는 다음과 같이 사용된다. Controller 내부에서 PostMapping을 전달받을 때 사용되는 dto 객체이다.

@RequestBody로 GameRequest 객체를 전달받고 있다.

 

리뷰어께서 질문 주셨던 부분은 파일 전체적으로 필드 내부에 final 키워드를 사용하고 있지만, GameRequest에서만 final 키워드를 사용하지 않고 있는데 그 이유를 물었던 것이다. 

 

 

기본생성자가 있어야 @RequestBody를 사용할 수 있지 않아?


 

우선 final 키워드를 뺀 가장 큰 이유는 GameRequest에서 '기본 생성자를 사용'하기 위함이다. final 키워드가 있는 필드가 있다면 기본 생성자를 사용할 수 없기 때문에 final 키워드를 모두 지웠다. 

 

위에 코드에서 기본생성자를 제거한다면 다음과 같은 예외 메세지가 등장한다.

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `racingcar.controller.GameRequest` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
 at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 1, column: 2]
	at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67) ~[jackson-databind-2.13.5.jar:2.13.5]
	at com.fasterxml.jackson.databind.DeserializationContext.reportBadDefinition(DeserializationContext.java:1904) ~[jackson-databind-2.13.5.jar:2.13.5]
	at com.fasterxml.jackson.databind.DatabindContext.reportBadDefinition(DatabindContext.java:400) ~[jackson-databind-2.13.5.jar:2.13.5]
	at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1349) ~[jackson-databind-2.13.5.jar:2.13.5]
	at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1415) ~[jackson-databind-2.13.5.jar:2.13.5]
...

 

Cannot construct instance of `racingcar.controller.GameRequest` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)

지금 보면 Object value로부터 역직렬화를 할 수 없기 때문에 발생하는 예외 메세지라고 등장한다. 

우선 이 예외 메세지가 왜 발생하는지부터 한번 찾아보도록 하자.

 

가장 먼저 예외가 발생하는 지점을 먼저 찾아봤다.

 

여기에서 가장 아래에 위치한 코드가 호출이 되는 것을 볼 수 있다.

ctxt.handleMissingInstantiator(raw, getValueInstantiator(), p,
"cannot deserialize from Object value (no delegate- or property-based Creator)");

 

그리고 해당 메소드로 들어가면 아래의 드래그된 부분이 호출되는데, 이름만 보더라도 예외를 던진다는 것을 알 수 있다.

 

예외 메세지를 보면 기본 생성자가 없어서 발생하는 에러인 것은 알 수 있다.

 


사실 빌드가 intellij로 설정되어있을 때에만 @RequestBody에 사용되는 객체에는 기본생성자가 필요하다.

빌드가 Intellij로 설정되어있으면 다들 빠르다는 것을 알 것이다. 이는 intellij가 증분 빌드 방식을 사용하기 때문인데, 이때 빠르게 작업하기 위해서 중간 중간에 생략하는 작업이 있다. 이 때문에 별도의 파라미터 플래그를 설정하지 않으면 바인딩이 안돼서 예외가 발생한다.


(23년 4월 21일 업데이트)

 

오늘 에단에게 들었는데 파라미터 플래그를 따로 설정해줄 수 있다고 한다.

Additional command line parameters에 -parameters를 추가했다.

 -parameter라는 파라미터 플래그를 넣어서 컴파일하면 위의 문제가 해결된다. 증분 빌드를 하며 빠진 내용을 추가로 넣어주는 파라미터인 것 같다.


빌드를 gradle로 하면 해결은 된다. 하지만 intellij로 빌드했을 때 에러가 난다고 gradle로 빌드하는 것은 다른 사람들과의 협업을 할 때도 gradle로 환경을 맞춰야한다는 제약 사항이 생기는 것이기에 별로 내키지 않았다. 그래서 이를 소스코드 레벨에서 해결할 수 있는 방법이 없는지 찾아본 것이다.


 

왜 기본생성자가 있어야지 @RequestBody를 사용할 수 있는거야?


결론부터 말하자면 @RequestBody 어노테이션을 사용하면 Jackson의 ObjectMapper를 사용해서 객체를 바인딩하기 때문이다. 

출처: ObjectMapper 공식 문서

 

ObjectMapper는 이름에서도 알 수 있듯이 Object의 Mapper 역할을 하는 클래스이다. 그래서 JSON을 자바의 객체로 바꿔주고 객체를 JSON으로 바꿔주는 역할을 한다.

 

위에서 POJO라는 용어가 등장하는데, 이는 자바에서의 가장 기본적인(?) 형태의 클래스로 특정 기술에 종속되지 않고 '기본 생성자'와 'getter', 'setter'가 있는 클래스를 의미한다. 

 

즉, ObjectMapper는 기본생성자, getter와 setter가 있는 객체를 JSON으로, JSON을 기본생성자, getter, setter가 있는 객체로 바꿔준다는 것을 의미한다. ()

 

그래서 기본 생성자가 있는 경우에는 ObjectMapper가 아래와 같이 기본생성자를 호출해서 새로운 객체를 만들어서 값을 넣어주는 과정을 거친다.

 

이때 ObjectMapper에서 dto를 만든 뒤 reflection을 사용해서 값을 주입해주기 때문에 setter는 필요없다. 뭐... 그렇다고 한다.

 

그런데 사실 여기에는 함정이 숨어있다.

 

 

어라? 기본생성자가 없어도 @RequestBody를 사용할 수 있나본데?


 

가장 먼저 살펴봤던 메서드의 이름을 살펴보면 'deserializeFromObjectUsingNonDefault'라고 되어있다.

 

즉, 기본생성자가 없는 경우에 호출되는 메서드라는 것을 알 수 있는데, 기본 생성자가 없는 경우에 객체를 역직렬화해주는 메서드이다.

 

 

사실 위에서 볼 수 있듯이 기본 생성자가 없더라도

 

(1) delegateDeser가 null이 아니거나

(2) _propertyBasedCreator가 null이 아닌 경우에는 정상적으로 동작한다는 것을 알 수 있다.

 

 

delegateDeser

 

 

먼저 delegateDeser가 null인지 아닌지 확인해보기 위해 _delegateDeserializer 메서드를 먼저 살펴봤다. 

 

위의 메서드에서 deser가 null이 되기 위해서는 _delegateDeserializer가 null이고, _arrayDelegateDeserializer이 null이어야 한다.

 

BeanDeserializerBase

 

BeanDeserializerBase 클래스에서는 위의 두 필드를 위와 같이 설명하고 있는데 솔직히 이해가 가지는 않는다 ㅎㅎ...

 

아무튼 JSON 객체를 역직렬화할 때 사용하는 delegate 기반이거나 array delegate 기반의 무언가 정도로 이해하면 될 것 같다. 결국 delegate를 한다면 예외가 발생하지 않을 것 같은데... 뭘 어떻게 위임해준다는 것인지 이해가 안간다. ㅎㅎㅎㅎㅎㅎ

 

구글링을 해봐도 내용이 많이 나오지도 않고 공식 문서도 들춰봤는데 머리가 아파서 그냥 다시 덮었다.

 

'지금은 스프링을 막 공부하는 단계니까 너무 무리해서 다 공부하려고 하면 금방 포기하지 않을까?'라는 마음가짐으로 일단은 가볍게 공부하려 했던거니 이는 나중으로 미뤄보도록 하겠다! 차후에 내용을 공부했으면 보충해서 설명해야지!

 

핵심은 뭐 delegate를 따로 설정해주지 않았기 때문에 delegateDeser가 null이지 않았을까싶다.

 


찾아보니 JsonDeserialize 어노테이션이 있어서 이를 사용하면 처리가 가능한 것 같다. 나중에 정리 한번 해보도록 하겠다.

 

 

_propertyBasedCreator

 

휴... 이것도 뭐 비슷하게 이해못할 것 같지만 한번 찾아보았다. 

 

 

이는 설명하고 있기를 ... 역시 이해가 안간다.

 

일단 이름에서 유추해보기를 property 기반의 Creator라는 것은 알겠다.

 

이것도 나중으로 미룰까하다가 아쉬워서 내용을 조금 더 찾아봤다. 그러다 @JsonProperty라는 어노테이션이 있다는 것을 알게 되었다. 해당 어노테이션을 사용해서 프로퍼티를 등록해주면 다음과 같이 기본생성자가 없이도 사용 가능하다.

 

@JsonProperty

 

 

그런데 하나하나씩 디버깅을 해보니까 위에서 언급한 propertyBasedCreator를 사용해서 처리하는건 아닌 것 같다. 일단 머리가 아파서 내부적으로 어떤 차이가 있는지는 확인하지 않고 @JsonProperty로 필드를 정의하면 되는구나! 하는 것만 알아뒀다.

 

 

@JsonProperty는 논리적으로 setter와 getter를 정의하는 마커 어노테이션이라고 한다. 그래서 setter 메서드를 호출하여 값을 주입하는 식으로 역직렬화를 하기에, 기본생성자가 없더라도 역직렬화가 가능하게끔 돕는 도구인 것 같다.

 

뭐... 이제 막 배우는데 이 이상은 이해하기도 어렵고, 일단 지금 시점에서 배워야하는 우선순위가 더 높은 내용들이 많아서 이 정도만 이해하고 나중에 정리해야겠다.

 


 

그러면 delegate나 property를 사용하면 기본 생성자가 없어도 처리가 가능한거네!


delegate나 property를 따로 등록해주면 역직렬화가 가능하기 때문에 전달받은 값을 객체로 잘 바인딩할 수 있게 된다. 하지만 이 둘을 따로 등록하지 않고 기본생성자도 만들어주지 않는 경우에는 위에서 언급했던 것처럼 바인딩을 제대로 하지 못해서 예외가 발생한다. 

 

 

 

결론

1. 일반적으로는 기본 생성자를 사용해야 @RequestBody를 사용해 json을 객체로 바인딩할 수 있다.

2. 만약 기본 생성자를 사용하지 않으면 delegate나 property를 등록해야한다.

3. delegate나 property를 등록할 때에는 Jackson의 @JsonDeserialize, JsonProperty를 사용할 수 있다.

 

 

배워야할 더 중요한 내용이 엄청 많은데, 하나에 꽂혀서 파고들다가 애매하게 마무리한 느낌이다. 흐음... 아직 Jackson이 뭐고, objectMapper가 뭐고, JsonDeserialize나 JsonProperty가 무엇인지도 명확하게 모르는 채로 그냥 그렇구나~하고 마무리해서 살짝 찝찝한 상태로 글을 마무리하게 됐다. 그렇다고 아직 모르는 것도 많은데 더 깊게 파는 거는 더 이상 의미가 없을 것 같아서, 일단은 지금 공부해야하는 내용에 좀 더 집중해서 공부해보려고 한다. 나중에 어느 정도 스프링 구조에 대해 익숙해지면 차근차근 다시 공부해봐야겠다!

 

 

 

728x90
반응형