
@Operation(summary = "쓰레기통 북마크 지정")
@PostMapping
public void createBookmark(@Valid @RequestBody CreateBookmarkRequest createBookmarkRequest, @CurrentUser String email) {
bookMarkService.createBookMark(email, createBookmarkRequest.getBinId());
}
이렇게 보내는 별거 아닌 코드에서 계속 입력값 에러가 떠서 뭐지?? 하면서 원인을 발견할 수가 없었다. DTO에 값매핑이 뭔가 잘 안되는 거 같았다.

이렇게 컨트롤러단에 브레이크 포인트를 걸어도 여기서 멈추지 않고 바로 잘못된 입력값이라는 메시지와 400코드가 떴기 때문이다.
혹시나 싶어서 DTO에 값을 맵핑해주는 리졸버를 확인해보니

우리 팀이 만들었던 ConcurrentArguementResolver가 result에 걸렸다.
매개변수의 순서를 DTO를 앞으로 옮겨줬더니

이렇게 정상적으로 받아왔다.
그런데도 여전히 입력값이 잘못됐다는 에러가 떴다.

실제로 어떻게 맵핑을 하고 있는지 확인해봤다.

이런 에러 로그가 발생했다.


결국 기본생성자가 없기 때문에 문제가 된 것이다.
근데 왜 저런식으로 반환이 됐는지 모르겠다. 보통은 스프링 서버에서 기본생성자가 없다는 로그가 뜨는 것으로 알고 있는데...
참고로 리퀘스트바디에서 기본생성자가 필요한 이유는 리플랙션 때문이다. 만약 생성자가 여러 개 있다면, Reflection은 이 중 어떤 생성자를 호출해야 할지 알 수가 없다. 따라서 Reflection은 기본 생성자를 통해 객체를 생성한 후 값을 넣어준다.
RequestBody는 입력으로 들어온 Json 형태의 문자열을 dispatcher servlet 에서 객체로 변환할 때 사용한다. 이때 내부적으로 Jackson 라이브러리의 ObjectMapper를 사용하게 되는데, 이 ObjectMapper 내부에서 Reflection을 통해 Json 문자열을 객체로 변환한다.
AbstractMessageConverterMethodProcessor를 구현한 RequestResponseBodyMethodProcessor를 봐보자.



이렇게 컨버터에서 값을 읽는것을 확인했다.

여기서 드디어 ObjectMapper가 쓰인다.
(1) setter없이 기본 생성자만 있을 때


값이 잘 읽혀진다.
(2) setter있고 기본 생성자가 없을 때

MappingJackson2HttpMessageConverter가 선택돼서 이걸 통해서 read를 했다.
쭉 타고 들어가면

BeanDeserializer에서 prop(binId)를 가져온다.

여기서 deserailze를 해준다.

값을 정상적으로 읽었다.!!
이유를 찾아보니 자바 컴파일러가 생성자 표시가 없으면 기본 생성자를 만들어주기 때문이었다.
그렇다면, 기본생성자가 없다면?

컨버터로는 똑같이 MappingJackson2HttpMessageConverter가 골라졌다.

이런 에러가 떴다.

여길보면 기본적이지 않은 방식으로 생성이라는 말이 나오는데 아마 여기서 기본 생성자가 없기 때문에 deseralizeFromObjectUsingNondefault로 가는게 아닌가 싶다.
저 과정들이 상당히 복잡해서... 그걸 이해하기는 쉽지 않아 보였다. 그러면, 기본 생성자가 있을 떈 로직이 어떻게 흘러가는지 비교해보면 좋을 거 같다.

기본 생성자가 있으면 그 아래로 넘어가서 값이 들어가는 것을 확인했따.
중요한 것은 _nonStandardCreation이 값인 거 같다. 어떻게 값이 맵핑되는지 확인해봤다.
(1) 기본 생성자가 있을 때


_nonStandardCreation가 false로 들어갔다.
(2) 기본 생성자가 없을 때

(1)과 다르게 defaultCreate가 null이다.

_nonStandardCreation가 true이다. 위에서 본 것처럼 _nonStandardCreation가 true인지 false인지 확인하는 분기점을 타고 들어가서, 기본 생성자가 없는 상황에서 값을 바인딩하는 흐름으로 넘어가게 될 것이다.
Jack이라는 라이브러이에 대해서 잘 몰랐는데 이번 기회에 작동 흐름을 공부할 수 있어서 좋았다.
처음 시작할 때 왜 "유효하지 않은 입력값이 존재합니다"라는 예외메시지만 나오는지 모르겠다고 적었는데(보통 이렇게 DTO에 기본 생성자가 없으면 생성자가 없다는 로그가 떴기 때문이다) 알고보니 예외 처리 때문이었다.

이렇게 글로벌 예외처리 핸들러를 만들어 놓았는데
마지막에 여기서 예외가 변환돼서 메시지를 반환하느라 DTO에 기본 생성자가 없다는 문제를 바로 파악하지 못했던 것이다..!
예외 메시지를 변환해서 처리할 때 이렇게 되면 원인 파악이 힘들어진다
(처음에 기본 생성자가 없어서 이 문제가 생겼다는 걸 파악하기 어려웠다. 평소같으면 로그를 통해서 바로 볼 수 있는데 이 경우에는 로그도 스프링이 찍어주지 않았다)
글로벌 핸들러로 처리하는 게 편하고 좋은 방법이라고 생각했는데 주의해서 써야겠다.