Spring Data JPA에서 findById()
같은 API를 사용해 데이터를 조회할 때, No default constructor for entity
라는 에러가 발생하는 이유가 무엇인지 조금 더 디테일하게 분석해보았습니다.
Spring Data JPA에서는 개발자가 JpaRepository 등의 인터페이스를 상속하는 Repository 인터페이스를 구현하는 구현 클래스의 인스턴스를 생성하기 위해 내부적으로 Dynamic Proxy라는 기술과 Reflection을 이용하는데 RepositoryFactorySupport
에서 해당 인스턴스(Repository의 Proxy)를 생성합니다.
그런데, 여기까지는 Spring에서 지원하는 Repository 인터페이스(개발자가 JpaRepository 같은 인터페이스를 상속한 Custom Repository)의 Proxy를 생성하는 역할을 할 뿐이고, 이 Proxy에서 타겟 클래스인 SimpleJpaRepository의 쿼리 메서드를 이용한 과정까지입니다.
제가 원한건 No default constructor for entity
라는 에러가 발생하는 이유를 조금 더 디테일하게 알고 싶은것이었기 때문에 조금 더 분석을 해보았습니다.
분석 결과 Spring Data에서 제공하는 RepositoryComposition이 RepositoryMethodInvoker를 이용해 앞에서 얻은 Proxy의 타겟 클래스인 SimpleJpaRepository에 구현된 쿼리 메서드(findById()
같은)를 호출합니다.
그 다음부터는 Hibernate ORM의 영역입니다. Hibernate ORM에서 EntityManager의 역할을 하는 Session 인터페이스의 구현 클래스인 SessionImpl
에서 데이터베이스에 접속해 데이터를 조회합니다.
SessionImpl
로부터 데이터베이스에 데이터를 조회하는 복잡한 로직이 시작되고 결국 조회한 데이터를 개발자가 정의 Entity 클래스의 object로 변환하기 위해 아래와 같이 PojoInstantiator
의 instantiate()
에서 Reflection을 통해 얻은 Constructor
인스턴스로 newInstance()
를 호출해 Entity 클래스의 object를 생성합니다.
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.tuple;
import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import org.hibernate.InstantiationException;
import org.hibernate.PropertyNotFoundException;
import org.hibernate.bytecode.spi.ReflectionOptimizer;
import org.hibernate.internal.CoreLogging;
import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.internal.util.ReflectHelper;
import org.hibernate.mapping.Component;
/**
* Defines a POJO-based instantiator for use from the tuplizers.
*/
public class PojoInstantiator implements Instantiator, Serializable {
private static final CoreMessageLogger LOG = CoreLogging.messageLogger( PojoInstantiator.class.getName() );
private transient Constructor constructor;
private final Class mappedClass;
private final transient ReflectionOptimizer.InstantiationOptimizer optimizer;
private final boolean embeddedIdentifier;
private final boolean isAbstract;
...
...
public Object instantiate() {
if ( isAbstract ) {
throw new InstantiationException( "Cannot instantiate abstract class or interface: ", mappedClass );
}
else if ( optimizer != null ) {
return optimizer.newInstance();
}
else if ( constructor == null ) {
throw new InstantiationException( "No default constructor for entity: ", mappedClass );
}
else {
try {
return applyInterception( constructor.newInstance( (Object[]) null ) );
}
catch ( Exception e ) {
throw new InstantiationException( "Could not instantiate entity: ", mappedClass, e );
}
}
}
...
...
}
그런데, 코드를 보면 constructor가 null이라면 InstantiationException
을 throw하도록 구현된 것을 볼 수 있습니다.
이유는 Java의 Reflection에서 object를 생성하기 위해서는 파라미터가 없는 디폴트 생성자(default constructor)가 필요하기 때문입니다.
이러한 이유때문에 Spring Data JPA에서 사용되는 Entity 클래스에는 디폴트 생성자가 반드시 있어야한다는 사실을 기억하면 좋을 것 같습니다.
Java의 클래스에 파라미터를 가지는 별도의 생성자를 추가하지 않으면 내부적으로 디폴트 생성자가 있다고 가정하지만 오버로딩된 생성자가 추가되는 순간 디폴트 생성자는 개발자가 직접 지정을 해야한다는 사실은 잘 알고 있을거라 생각합니다.
그리고 lombok에서 지원하는 @Builder 패턴 등을 사용할 경우에는 항상 디폴트 생성자를 기본으로 가질 수 있도록 해야한다는 사실을 기억하면 좋을 것 같습니다.