사건의 발단
문제의 UserLoginReuqst dto
@AllArgsConstructor
@Getter
public class UserLoginRequest {
private String userName;
private String password;
}
사건 내용
@RequestBody로 데이터를 불러오던 중
Type definition error: [simple type, class com.study.domain.dto.UserLoginRequest]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `com.study.domain.dto.UserLoginRequest` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)\n at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 2, column: 3]
와 같은 에러가 발생하면서, UserLoginRequest가 바인딩이 되지 않는 상황이 발생하였다.
에러 문구 중 (no Creators, like default constructor, exist)의 문구가 눈에 들어와 UserLoginRequset에 NoArgsConstructor를 추가하여 에러를 해결하였다.
궁금증은 여기에서 시작된다.
- 왜 기본 생성자가 있어야 하는 것인가.
- 같이 수업받는 사람들은 해당 어노테이션을 붙이지 않고도(또는 기본 생성자를 만들어주지 않아도) 에러 없이 바인딩이 되었는가?
궁금증 해결 과정 첫 번째
키워드 획득
같은 조원이 찾아온 okky넷의 Q&A로부터 실마리를 풀어나갔다.
나와 같은 상황에서 @NoArgsConstructor를 붙여 문제를 해결한 사람의 질문으로, 해당 상황이 역직렬화와 관계가 있는지 묻고 있었다.
@NoArgsConstructor을 붙여야 하는 이유는
- 역 직렬화를 할 때 대상 클래스의 기본 생성자로 객체를 생성한다.
- 해당 객체의 setter 메서드를 활용하여 json 값을 객체에 설정한다.
- 만약 setter 메서드가 없다면 java reflection 패키지를 활용하여 객체에 값을 설정한다.
이곳에서 "역직렬화"라는 키워드와, 답변에서 "JackSon Objectmapper", "java reflection"이라는 키워드를 얻고 조금 더 깊이 알아보기 위해 검색을 진행했다.
Object Mapper
Object Mapper는 POJO, JSON Tree Model에서 JSON을 읽고 쓰는 기능과 변환 수행 기능을 제공합니다.
여기 ObejctMapper 클래스 안에, serialize, deserialize라는 단어가 보이는데
- Java -> JSON으로 파싱 : 직렬화(Serialize)
- JSON -> JAVA로 파싱 : 역직렬화(Deserialize)
아래의 @RequestBody에 기본 생성자가 필요한 이유에 적힌 블로그를 확인하여 해당 글(2번 글)과 다음 3번 글을 확인하면 왜 기본 생성자가 필요한지에 대해 설명하고 있다.
- RestController에서 @RequestBody를 바인딩을 하기 위해 ObjectMapper를 사용하는데 기본 생성자로 DTO를 생성하기 때문에, 일반적인 상황에서는 기본 생성자가 필요하다.
Java Reflection
- 컴파일을 할 때, 클래스, 인터페이스, 필드, 메서드들의 타입을 몰라도 런타임 특성을 검사, 수정할 수 있도록 해주는 자바 API
- Reflection에서 확인해야 할 것은, Reflection은 무조건 기본 생성자가 필요하다
- Reflection은 생성자의 인자 정보들을 가지고 올 수 없다.
- 기본 생성자 없이 파라미터가 있는 생성자만 존재한다면 java Reflection은 객체를 생성할 수 없다.
- 기본 생성자가 존재해야 객체를 생성할 수 있고, 기본 생성자로 객체를 생성만 하면 필드 값 등은 Reflection API로 넣어줄 수 있다.
이러한 검색을 진행하면서
"그래, 일단 @NoArgsContructor, 기본 생성자가 필요한 건 알겠어. 근데 왜 누군 되고 안되는 건데?"
라는 의문이 끝나지 않았고, 회고팀 모두 위의 글들을 보면서 디버깅을 돌리기 시작했다.
궁금증 해결 과정 두 번째
디버깅을 돌리면서 차이가 생기는 세 변수에 시선이 집중되었다.
- _propertyBasedCreator
- If the bean needs to be instantiated using constructor or factory method that takes one or more named properties as argument(s), this creator is used for instantiation.
- 하나 이상의 명명된 속성을 인수로 사용하는 생성자 또는 팩토리 메서드를 사용하여 Bean을 인스턴스화 해야 하는 경우 생성자가 인스턴스화에 사용된다.
- creatorProps
- _valueInstantiator : 인스턴스가 전달되지 않는다면, 초기 빈 값(데이터를 바인딩할 대상)을 구성하는 세부 사항을 처리하는 객체
- _withArgsCreator
- StdValueInstantiator.Class 내부에 있는 protected AnnotatedWithParams _withArgsCreator; 매개변수
나의 경우(기본 생성자 없음, 실패)
- _propertyBasedCreator = null
- creatorProps = Cannot find local variable 'creatorProps'
- _withArgsCreator = null
나의 경우에는 위와 상태를 계속 유지하지만, 바인딩이 되는 다른 조원들은 해당 변수들에 값들이 다 할당되어 있거나 과정중에 초기화 되었다.
null값일 때, 디버깅
- _PropertyBasedCreator = null 초기화
- _withArgsCreator == null인 상태로, creatorProps = null 반환
- deserializeFromObjectUsingNonDefault에서 msg 반환
if문의 조건에 부합하지 못하고 맨 마지막 return으로
"cannot deserialize from Object value (no delegate- or property-based Creator)"
반환
- 끝까지 _propertyBasedCreator, creatorProps는 null이거나 존재하지 않았다.
바인딩이 성공하는 조건의 디버깅(기본 생성자 없음, 성공)
- _withArgsCreator 초기화
- _PropertyBasedCreator = null 초기화
- _withArgsCreator == true인 상태로, creatorProps = null
- 조건에 부합하면서 creatorProps = _valueInstantiator.getFromObjectArguments(ctxt.getConfig()) 반환
- creatorProps가 null이 아니기 때문에 _propertyBasedCreator에도 값이 생긴다.
- _propertyBasedCreator가 null 이 아니므로 _deserializeUsingPropertyBased 메서드가 반환된다.
실패했을 때와는 달리 무사히 값들이 반환되기 시작한다.
결국 디버깅을 쭉 이어나가다 보면
성공
이유는?
@NoArgsConstructor가 없이 무사히 바인딩이 된 사람들은 Build and run의 설정이 'Gradle'이었으며,
@NoArgsConstructor가 있어야만 바인딩이 된 사람들은 설정이 'IntelliJ IDEA'로 되어있었다.
디버깅을 함께 공유하던 팀원의 Run 속도가 느려 Gradle에서 IntelliJ IDEA로 변경하자, _withArgsCreator의 값이 null로 바뀌었고 에러를 반환해버렸다.
이유를 찾은 상황은 정말 뜻밖이었고, 설마 이 차이가 이렇게 중요한 결과를 결정 낸다는 것에 놀라웠다.
추측?
두 빌드에서 차이가 발생한다는 점은 두 빌드 과정이 다르다는 것을 의미했고 우리는 자바의 빌드 시스템에 눈을 돌렸다.
참조의 자바의 빌드 시스템, Gradle Build와 IntelliJ Build 블로그를 확인하면 Compier flag 설정으로 인하여 JPA 파라미터 바인딩 이슈가 발생한다는 것을 알 수 있었다.
아쉽게도 우리가 직면한 문제에서는 -parameters를 해봤지만 바인딩이 되지 않았다.
결론
다른 분의 실험 결과 Gradle 설정을 하였어도, Swagger를 추가하면서 같은 바인딩 이슈가 일어난 것을 알 수 있었다.
협업은 물론, 개인적으로 개발할 때도 또 다른 문제를 일으킬 수 있으므로 @NoArgsConstructor를 사용하거나, 기본 생성자를 만들어서 Object Mapper, Reflection에서 잘 바인딩이 될 수 있는 환경을 마련해주어야 할 것 같다.
추가내용
참조
Okky
https://okky.kr/articles/1149157
@RequestBody에 기본 생성자가 필요한 이유, 역직렬화 Debug
https://velog.io/@conatuseus/RequestBody에-왜-기본-생정자는-필요하고-Setter는-필요-없을까-2-ejk5siejhh
Java Reflection 기본 생성자가 있어야 하는 이유
https://da-nyee.github.io/posts/woowacourse-why-the-default-constructor-is-needed/
자바의 빌드 시스템, Gradle Build와 IntelliJ Build
https://haenny.tistory.com/394
BeanDeserializerBase, SettableBeanProperty, ConcreteBeanPropertyBase 문서
'Server > Spring&Spring Boot' 카테고리의 다른 글
[Spring] Custom Response 생성 (0) | 2022.12.21 |
---|---|
[Spring Security] config 설정 2 (0) | 2022.12.04 |
[Spring Security] config 설정 1 (0) | 2022.11.29 |
[Docs] Swagger 도입 (0) | 2022.11.24 |
[Spring Security] 기본 유저, 비밀번호 변경 (0) | 2022.11.19 |