spring data mongo를 사용하면서 MappingInstantiationException 발생

한줄느낀점 : 롬복을 쓰면서 record도입은 아직 시기상조인건가…

일 하다가…

몽고로 신나게 개발을 하다가, 자꾸 MongoRepository.findById() 를 수행하는데 MappingInstantiationException 가 발생한다…

로그 트레이스를 보니까 다음과 같이 뜬다..

(중요한 기업비밀-??- 일 것 같은 클래스명은 한글로 대체함)

org.springframework.data.mapping.model.MappingInstantiationException: 
Failed to instantiate 리턴 클래스명 using constructor 생성자 메소드 with arguments 생성자 값
Caused by: java.lang.IllegalArgumentException: Parameter 필드명 must not be null!
	at org.springframework.util.Assert.notNull(Assert.java:201)
	at 리턴 클래스명_Instantiator_dbpwc5.newInstance(Unknown Source)
	at org.springframework.data.mapping.model.ClassGeneratingEntityInstantiator$EntityInstantiatorAdapter.createInstance(ClassGeneratingEntityInstantiator.java:251)
	... 112 more

라고 하네….

몽고의 도큐먼트 내 필드가 존재하지 않거나 null인게 있는데

자바 클래스에 이를 매핑하려 할 때 에러가 발생한 것이다.

이해하기 쉽게 대충 수도코드 형식의 코드로 보자면..

몽고
{
  _id : "1234",
  name : "zaksal58",
  height : 100,
  weight : 100
}
@Builder // 알지? 롬복
public record MyProfile (
  String name,
  long height,
  long weight,
  long shoeSize //신발 사이즈
)}

이런 상황이다.

  • 몽고의 도큐먼트엔 신발 사이즈가 없다
  • 자바의 MyProfile레코드의 shoeSize 변수는 primitive data type이다.

이 경우, SELECT이후 매핑을 할 때 저 위에 있는 익셉션을 100%의 확률로 뿜뿜한다…..

익셉션을 잘 보면, 몽고로부터 가져오(려다 실패하)는 필드명이 null일 경우에 나는 익셉션이고,

자바의 primitive data type은 null값으로의 assign을 원천적으로 허용하지 않는다.

안돼안돼..

1차원적인 해결방법은 다음과 같다

  • 자바에 null이 assign되지 않게끔 한다.
    • 불가능하다. 가능하게 소스를 어찌어찌 꿰멘다 해도 개뻘짓임. 문제의 클래스에서 필드 하나 추가되는순간 서비스 장애남
  • 자바에서 null도 받아준다
    • Wrapper class가 있지 않은가?

그래서 일단 Wrapper class를 사용해서 1차 해결을 했다


근데 좀 꺼림칙하다…

해결방법이 너무 1차원적이고, 직관적으로 생각했을때 null이 assign되는 NPE의 구멍이 만들어지는걸 내 스스로 남기는 것 같아 양심의 가책도 느껴지고…

그리고 팀 내에서 primitive data type이 존재하는 자료형은 그대로 살리는 방향을 지향하고 있다. 이 부분은 나도 동의하고 있고, 가뜩이나 먼옛날 라떼엔… NPE로 허구헌날 뻑뻑뻑 서비스 터져나갔었던 추억이 있어서, (어쩔 수 없는 경우가 아닌 한)nullable한 자료형을 사용하는 걸 별로 좋아하지 않는다. 부가적으로 박싱-언박싱의 작디작은 시간낭비도 있고..

그런데, 서비스코드 다른 영역에서 findById()를 이렇게 하는 곳이 있는데, 여기선 익셉션이 발생하지 않는다. 음… 그래서 wrpper class를 사용하지 않고 해결할 수 있는 해결책에 대해 방법을 찾아보았다.

@Builder
@NoArgsConstructor // 요거 추가
@AllArgsConstructor // 요거 추가
public class MyProfileClass { // 레코드가 클래스로
  String name;
  long height;
  long weight;
  long shoeSize;
}

요렇게

  • 클래스에다 NoArgs, AllArgs 어노테이션을 붙인 채로
  • 레코드를 클래스로 변경한 채로

FindById() 를 쓰니까 익셉션이 일어나지 않는다.

어… 그럼 현재 문제가 있는 MyProfile 레코드에다가 NoArgs, AllArgs 어노테이션을 추가하면 문제가 해결 되겠구만

하지만 어림도 없다.

음… 롬복이 이걸 지원해주지 않네 아…. (참고로 자바17 + 롬복 6.5버전 -나름 현재기준 최신버전-임…)


왜 이런 문제가 발생할까?

문제의 해결방법에 앞서….더 중요한 ‘왜 이 문제가 발생하는지’ 에 대해서 찾아보았다.

우선…. 생성자 관련한 익셉션이 발생해서 관련된 문서를 찾아보았다.

(‘모르면 무조건 매뉴얼을 봐야 하는’건 동서고금을 막론하고 가장 정확하고 빠른 길이다. 물론 소스코드를 보면 좋겠지만…실력이 없어서…해석이 안됨..)

  • 도대체 스프링 몽고는 쿼리한 결과를 Bean에 매핑을 할 때, 어떤 방법으로 생성자를 찾나

https://docs.spring.io/spring-data/mongodb/docs/current/reference/html/#mapping-chapter

여기 들어가보면… 다음과 같은 문구가 있다.

오호…

대충 해석해보면

  1. @PersistenceCreator 어노테이션이 붙은 팩토리메소드가 있으면 그걸 사용한다.
  2. 생성자 한개가 있으면, 그걸 사용한다.
  3. 생성자가 여러개 있는 경우라면, 그때 @PersistenceCreator어노테이션이 붙은걸 사용한다.
  4. NoArgsConstructor가 있으면, 다른 생성자는 무시된다

그렇군…


레코드 객체생성에서 왜 익셉션이 났냐면..

최초 문제가 발생했던 MyProfile 레코드는 Builder어노테이션에 의해, AllArgs가 자동으로 생성되어있다.

(Builder와 AllArgs의 관계에 대해서는 https://devlog-wjdrbs96.tistory.com/419 에 매우매우 잘 정리되어있음.ㅇㅇ)

그러면서 상기 객체생성 조건 2번에 부합하면서, 스프링은 자동적으로 AllArgs를 통해 객체를 생성하려 한다. 아마 이런 식으로 구현이 되겠지.

몽고 데이터..
{
  _id : "1234",
  name : "zaksal58",
  height : 100,
  weight : 100
}


@Builder // 알지? 롬복
public record MyProfile (
  String name,
  long height,
  long weight,
  long shoeSize //신발 사이즈

  public MyProfile(String name, long height, long weight, long shoeSize) { // 롬복 Builder에 의해 자동 생성된 AllArgs...
    this(name, height, weight, shoeSize);
  }
)}


public static void main() {
  var result = new MyProfile("1234","zaksal58",100,100,null);
}


이래서 shoeSize값에 null이 assign되면서 오류가 났던 것이다….


그래서 최종 해결은…

간단하다… 그냥 레코드 사용을 포기하고 클래스로 가는걸로…

  • 레코드는 NoArgsConstrtuctor를 만들 수가 없는 구조
  • 스프링 몽고의 객체 생성과 프로퍼티 생성 로직에 어울리지 않음..
  • 스프링 몽고의 매핑 챕터에 다음과 같이 되어있음.
    • https://docs.spring.io/spring-data/mongodb/docs/current/reference/html/#mapping.general-recommendations
    • 롬복을 쓰란다… 근데 레코드로는 이 사용이 매우 제한적이다….

Leave a Comment