forDevLife
JPA 사용 시 Entity에 기본 생성자가 필요한 이유 본문
JPA는 Entity로 사용할 객체에 반드시 기본 생성자가 있어야 한다.
왜냐하면 java Reflection이 가져올 수 없는 정보 중 하나가 바로 생성자의 인자 정보들이기 때문이라고 한다. 하이버네이트에서 내부적으로 constructor.newInstance()라는 리플렉션을 통해 해당 Entity의 기본 생성자를 호출하여 객체를 생성하는데, 이는 구체적인 생성자의 매개변수를 알 수 없기 때문에(객체마다 매개변수는 모두 다를 것이기 때문에) 반드시 기본 생성자를 지정해둬야 한다.
※ 당연히 개발자가 작성한 생성자가 없다면, 자바에서 자동으로 기본 생성자를 만들어주기 때문에 작성해주지 않아도 된다.
이건 JPA 학습하면서 기본적으로 배우는 내용이지만, 다음 내용이 궁금해졌다.
1. 말로만 듣던 리플렉션이 어떻게 호출되는건지? 눈으로 봐야 믿을 수 있을 것 같았다;
2. 기본 생성자로 만드는 과정은 OK.. 그럼 쿼리 실행 결과를 property에 어떻게 매핑하는걸까? setter 없어도 동작하는것 같던데.
1. 기본 생성자 호출 확인
실습을 위해 우선 간단한 Team 객체를 만들었다.
@Entity
public class Team {
@Id
private Long team_id;
private String name;
private String testField;
public Team(String name, String testField) {
this.name = name;
this.testField = testField;
}
// Getter
}
이 때 우선 기본생성자를 만들지 않았다. IntellJ에서 빨간줄로 경고를 주긴 하지만,, 아래에서 em.persist 후 정상적으로 데이터베이스에 반영된다.
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Team team1 = new Team("tempName", "testField");
em.persist(team1);
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
Database에서 조회하기 위해 EntityManager.clear() 후 조회를 진행했다.
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Team team1 = new Team("tempName", "testField");
em.persist(team1);
tx.commit();
em.clear();
System.out.println("===after commit===");
Team team = em.find(Team.class, team1.getTeam_id());
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
Error performing load command : org.hibernate.InstantiationException:
No default constructor for entity: : hellojpa.Team
영속성 컨텍스트를 초기화 한 후, 다시 find를 통해 Team 객체를 조회할 경우에는 위와 같이 예외가 발생한다.
위 예외 메세지를 만들어주는 코드, 즉 기본 생성자를 통해 객체를 동적으로 만들어주는 부분을 찾아가보면 다음과 같다.
위 코드는 모두 구현체이기 때문에 인터페이스 기준, 차례대로 역할을 설명하면 다음과 같다.
- Session : Java application과 Hibernate사이의 주요 런타임 인터페이스이며, Persistence의 핵심이다.
- EntityPersister : mapping된 Entity Class를 영속화하기 위한 정보 및 기능들을 가지고 있다.
- EntityTuplizer : 주어진 데이터를 통해 Entity를 생성하는 기능을 가지고 있다.
- Instantiator : 엔티티의 인스턴스화를 담당한다.
여기서 알 수 있는 것은, 먼저 인스턴스화를 하고 나서 이후에 데이터를 inject 하겠구나?라고 생각해볼 수 있겠다.
맨 마지막의 PojoInstantiator 클래스 생성자 부분을 먼저 살펴보면 다음과 같다.
public class PojoInstantiator implements Instantiator, Serializable {
private transient Constructor constructor;
public PojoInstantiator(파라미터 생략) {
// 내용 생략
try {
// Util Class를 통해 생성자를 전달받는다.
constructor = ReflectHelper.getDefaultConstructor(mappedClass);
}
catch ( PropertyNotFoundException pnfe ) {
LOG.noDefaultConstructor( mappedClass.getName() );
constructor = null;
}
}
Constructor라는 객체를 가지고 있으며, 생성자에서 ReflectHelper라는 Utilily class를 통해 생성자를 전달받는 부분을 확인할 수 있다.
생성자를 전달받는 과정에서 예외가 발생하면 null로 초기화됨을 알 수 있다.
ReflectHelper의 메서드는 다음과 같다.
public static <T> Constructor<T> getDefaultConstructor(Class<T> clazz) throws PropertyNotFoundException {
if ( isAbstractClass( clazz ) ) {
return null;
}
try {
Constructor<T> constructor = clazz.getDeclaredConstructor( NO_PARAM_SIGNATURE );
ensureAccessibility( constructor );
return constructor;
}
catch ( NoSuchMethodException nme ) {
throw new PropertyNotFoundException(
"Object class [" + clazz.getName() + "] must declare a default (no-argument) constructor"
);
}
}
드디어 리플렉션 코드를 발견했다..! 기본 생성자를 만들고, ensureAccessibility를 통해 access 가능하도록 변경한 후 반환한다.
이를 통해 PojoInstantiator에는 정상적으로 기본 생성자가 만들어졌다면 생성자가 존재, 그렇지 않고 예외가 발생했다면 null이 들어있을 것으로 예상할 수 있다.
다시 처음으로 돌아가서, PojoInstantiator의 instantiate()를 보면 다음과 같다.
public Object instantiate() {
// 생략
// ..
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 );
}
}
}
PojoInstantiator의 생성 과정에서 constructor가 null이라면, "No default constructor for entity" 예외 메시지가 발생한다. 앞서 확인한 메시지와 일치함을 알 수 있다.
Error performing load command : org.hibernate.InstantiationException:
No default constructor for entity: : hellojpa.Team
constructor가 null이 아니라면, constructor.newInstance(null)을 통해 런타임에 Entity가 생성됨을 알 수 있다..!
2. Property 매핑
이제 빈 객체가 만들어졌으니, 프로퍼티를 채워볼 차례이다.
먼저 아래 순서로 id를 먼저 세팅하는 것을 확인할 수 있다. 이 역할도 EntityTuplizer에서 담당한다.
(AbstractEntityTuplizer의 경우에만 클래스 내부에서 호출하는 메서드를 표현하고자 아래와 같이 표현했다.)
결국 set을 연속적으로 호출하는 과정이긴 하지만 흐름 파악 및 이후 실습을 위해 하나하나 따라가서 정리해보았다.
1) 앞서 Instantiator로부터 빈 Team 객체가 반환되었다. setIdentifier 호출을 통해 Id를 세팅한다.
@Override
public final Object instantiate(Serializable id, SharedSessionContractImplementor session) {
Object result = getInstantiator().instantiate( id );
if ( id != null ) {
setIdentifier( result, id, session );
}
return result;
}
2) setIdentifier 내부에서는 idSetter라는 객체의 set 메서드를 호출하게 된다.
※ 우선 idSetter에 대해서는 뒤에서 알아보고 흐름에 집중해보자.
@Override
public void setIdentifier(Object entity, Serializable id, SharedSessionContractImplementor session) {
// 생략
else if ( idSetter != null ) {
idSetter.set( entity, id, getFactory() );
}
}
3) idSetter는 SetterFieldImpl라는 클래스이며, 내부에서 Field라는 클래스의 set 메서드를 호출한다.
@Override
public void set(Object target, Object value, SessionFactoryImplementor factory) {
try {
field.set( target, value );
} // .. 생략
4) 여기에서부터는 Hibernate가 아닌 java code이다. Field 내에서는 FieldAccessor라는 객체를 대상으로 set을 호출한다.
getFieldAccessor의 결과로 UnsafeObjectFieldAccessorImpl이라는 객체가 얻어지며, 해당 객체를 대상으로 set을 호출된다.
public void set(Object obj, Object value) {
if (!override) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, obj);
}
getFieldAccessor(obj).set(obj, value);
}
5) 정말 다왔다!! 여기에서는 unsafe.putObject를 호출한다.
public void set(Object obj, Object value) {
// .. 생략
unsafe.putObject(obj, fieldOffset, value);
}
6) Unsafe의 putObject는 native 코드로, 더 이상 볼 수 없다. 다만 객체 o를 대상으로 offset이라는 long type 값을 통해 계산된 위치에 x라는 내용을 집어넣는다고 예상해볼 수 있다.
@HotSpotIntrinsicCandidate
public native void putObject(Object o, long offset, Object x);
앞서 사용된 Field와 Unsafe에 대해서 간단히 알아봤다.
Field
- 클래스 또는 인터페이스의 단일 필드에 대한 정보 및 동적 엑세스를 제공한다. 정적 필드 혹은 인스턴스 필드일 수 있으며, 쉽게 말해서 객체의 필드에 직접 접근해서 수정할 수 있는 방법을 제공한다고 이해하면 될 것 같다.
Unsafe
- 저수준의 작업, 즉 객체를 낮은 레벨(바이트코드 수준?)에서 수정할 수 있도록 해준다.
- 해당 객체를 생성하는 것은 제한되지만, 이 역시 아래에서 사용될 리플렉션을 통해 얻어볼 수 있다.
Field와 Unsafe를 통해 간단한 테스트를 해본 코드이다.
import java.lang.reflect.Field;
import sun.misc.Unsafe;
public class UnsafeTest {
public static void main(String[] args) throws Exception {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe)f.get(null);
Team team = new Team();
Field teamIdField = team.getClass().getDeclaredField("teamId");
unsafe.putObject(team, unsafe.objectFieldOffset(teamIdField), 10);
Field teamName = team.getClass().getDeclaredField("teamName");
unsafe.putObject(team, unsafe.objectFieldOffset(teamName), "LucidTeam");
System.out.println(team);
// 출력 결과 : Team{teamId=10, teamName='LucidTeam', teamLeader='null'}
}
}
위와 같이 unsafe 객체를 통해 Team 내 필드의 offset 값을 얻고 이를 통해 Team 객체에 직접 프로퍼티를 세팅할 수 있다.
앞서 예상했던 것처럼 하이버네이트의 코드에서도 위와 같이 putObject가 동작할 것임을 예상할 수 있다.
결론적으로, Id말고 다른 Property들도 위와 같은 방법으로 Unsafe를 통해 객체에 직접 매핑될거라는 걸 예상할 수 있다.
3. Setter
마지막으로, 앞서 넘어간 idSetter라는 것에 대해서 살짝 살펴보면 다음과 같다.
public abstract class AbstractEntityTuplizer implements EntityTuplizer {
private static final CoreMessageLogger LOG = messageLogger( AbstractEntityTuplizer.class );
private final EntityMetamodel entityMetamodel;
// id 전용 getter, setter
private final Getter idGetter;
private final Setter idSetter;
// 나머지 프로퍼티 전용 getter, setter
protected final Getter[] getters;
protected final Setter[] setters;
// ...
EntityTuplizer 내부에는 id와 프로퍼티 전용으로 각각 Getter, Setter라는 객체가 존재한다.
런타임에 프로퍼티 대상 setters를 대략 살펴보면 아래와 같은 정보가 담겨있다.
현재 작성한 Team 객체 내부에는 별도로 setter method를 두지 않았기 때문에 setterMethod 항목이 null임을 알 수 있다. 아마 작성했다면 리플렉션을 통해 해당 메서드를 사용할 수 있도록 추출했을 것으로 예상할 수 있다.
참고
'Spring' 카테고리의 다른 글
JUnit5 Extension (2) | 2022.07.24 |
---|---|
Springboot의 Connection Pool (0) | 2022.05.20 |
스프링 입문 - 웹 개발 기초 <4> (0) | 2021.05.24 |
스프링 입문 - 웹 개발 기초 <3> (0) | 2021.05.20 |
스프링 입문 - 웹 개발 기초 <2> (0) | 2021.05.18 |