This default constructor is used by JPA to create instances of the entity class using Reflection. It provides a way to dynamically create instances of classes, call methods, and access fields.
https://www.baeldung.com/jpa-no-argument-constructor-entity-class
Need for Default Constructor in JPA Entities | Baeldung
Learn about the JPA requirement for a default no-argument constructor in an Entity class.
www.baeldung.com
문서에 따르면 Entity 객체를 생성할 때 Reflection을 사용한다고 한다.
JPA는 데이터베이스에서 데이터를 읽어와 엔티티 객체(Entity)로 매핑할 때 객체를 생성한다.
EntityManager em = ...;
Member member = em.find(Member.class, 1L);
예시 코드다.
em.find()를 하면 member 객체가 생성이 된다. new Member()를 호출한 적이 없지만 JPA 내부에서 자동으로 객체를 생성한다.
어떻게 자동으로 객체를 생성할까?
JPA의 구현체인 Hiberate는 위와 같이 객체를 생성한다.
Constructor<Member> constructor = Member.class.getDeclaredConstructor();
constructor.setAccessible(true);
Member member = constructor.newInstance();
간단하게 정리를 하자면,
JPA는 reflection을 통해 Entity 객체를 생성하는데, reflection으로 객체를 생성하는 과정에서 기본생성자가 필요하다.
라고 정리할 수 있다.
추가로 궁금한 점 : 왜 JPA에서는 기본생성자의 접근제어자를 public/ protected로 요구할까?
https://openjpa.apache.org/builds/1.2.3/apache-openjpa/docs/jpa_overview_pc.html
The JPA specification requires that all persistent classes have a no-arg constructor. This constructor may be public or protected. Because the compiler automatically creates a default no-arg constructor when no other constructor is defined, only classes that define constructors must also include a no-arg constructor.
스펙 명세에 따르면 JPA는 public / protected의 기본생성자를 요구하는 걸 확인할 수 있다.
왜 그럴까? reflection은 생성자가 private 이어도 상관없을 텐데...
여러 가지 이유가 있겠지만, 그중 하나는 proxy 때문이다.
Lazy 로딩을 할 경우 프록시 객체가 필요하다. 그리고 프록시 객체는 기존의 Entity를 상속받아 사용한다. 그런데 기존 Entity의 기본생성자가 private이라면 상속을 할 수가 없다. 위와 같은 이유로 private 생성자를 사용할 경우, proxy 객체 초기화를 하지 못하고, LazyInitializerException이 발생한다.
간단한 테스트를 통해 실험해 보자.
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Member() {}
public Member(Long id, String name) {
this.id = id;
this.name = name;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
}
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
private Member member;
private Order() {}
public Order(Long id, Member member) {
this.id = id;
this.member = member;
}
public Member getMember() {
return member;
}
}
Member와 Order Entity를 만들고, 연관관계를 ManyToOne으로 해줬다.
눈여겨볼 부분은, 기본생성자의 접근제어자를 private으로 선언했다.
테스트 코드
@DataJpaTest
@ContextConfiguration(classes = DemoApplication.class)
public class ConstructorTest {
@PersistenceContext
private EntityManager em;
@Autowired
private MemberRepository memberRepository;
@Autowired
private OrderRepository orderRepository;
@Test
void a() {
Member member1 = new Member(1L, "jenson");
Order order1 = new Order(1L, member1);
memberRepository.save(member1);
orderRepository.save(order1);
em.clear(); // 캐시 삭제
Optional<Order> findOrderResult = orderRepository.findById(1L);
Order order = findOrderResult.get();
order.getMember().getName(); // 에러 발생!
}
}
fetch 타입을 Lazy로 설정할 경우, 다음과 같은 오류가 발생한다.
org.hibernate.HibernateException: HHH000143:
Bytecode enhancement failed because no public,
protected or package-private default constructor was found for entity
위에서 설명한 오류상황이다. 기본생성자를 private으로 선언하니 proxy 객체가 초기화되지 않는다는 오류가 발생한다.
그렇다면, fetch 타입을 EAGER로 변경하면 어떻게 될까?
@ManyToOne(fetch = FetchType.EAGER)
private Member member;
성공한다...!
위의 실험을 통해 JPA에서 기본생성자의 접근제어자를 protected/public으로 제한하는 이유는, proxy 객체를 초기화하기 위해서라는 걸 알 수 있다.
https://openjpa.apache.org/builds/1.2.3/apache-openjpa/docs/jpa_overview_pc.html
https://www.baeldung.com/jpa-no-argument-constructor-entity-class
참고자료
'JPA' 카테고리의 다른 글
@Transactional이 없을 때 Lazy 로딩이 실패하는 이유 (2) | 2025.05.21 |
---|