JPA는 엔티티에 기본 생성자, 즉 아무런 매개변수를 받지 않는 생성자를 만드는 것을 강제하고 있습니다. JPA 구현체마다 스펙이 조금 달라서 기본 생성자를 만들지 않아도 정상적으로 작동하는 경우가 있지만, 엔티티에는 기본 생성자가 있어야 한다
가 공식 스펙이기 때문에 반드시 기본 생성자를 만들어주는 것이 좋습니다.
All persistent classes must have a default constructor (which can be non-public) so that Hibernate can instantiate them using Constructor.newInstance(). It is recommended that you have a default constructor with at least package visibility for runtime proxy generation in Hibernate.
그렇다면 JPA는 왜 엔티티에 기본 생성자를 만들도록 강제하고 있는 것일까요? 알아보기에 앞서, JPA와 유사하게 기본 생성자를 강제하는 경우를 생각해봅시다. 우리는 이미 스프링을 하면서 기본 생성자가 강제되는 경우를 경험해보았습니다. 바로 웹 요청으로 들어오는 @RequestBody
를 객체(DTO)로 바인딩하는 과정입니다. @RequestBody
에 바인딩할 타입에 기본 생성자가 존재하지 않는다면 정상적으로 바인딩되지 않는 예외가 발생하게 됩니다. 이는 스프링의 @RequestBody
바인딩 방식이 기본 생성자를 통해 객체를 생성한 후 Java Reflection
을 이용해 필드 값을 집어넣어 주는 방식이기 때문입니다. Reflection은 클래스 이름만 알면 생성자, 필드, 메서드 등 클래스의 모든 정보에 접근이 가능합니다. 하지만 Reflection이 가져올 수 없는 정보가 있는데요, 바로 생성자의 매개변수 정보
입니다. 때문에 Reflection으로 생성할 객체에 모든 필드를 받는 생성자가 있더라도 Reflection은 해당 생성자를 호출할 수가 없는 것이죠. 그래서 Reflection은 기본 생성자로 객체를 생성하고 필드 값을 강제로 매핑해주는 방식을 사용합니다.
JPA 역시 데이터를 DB에서 조회해 온 뒤 객체를 생성할 때 Reflection을 사용합니다. 때문에 기본 생성자로 객체를 생성합니다. 실제로 빈 생성자와 완전한 생성자에 각각 다른 로그를 찍어놓고 테스트해보면, 기본 생성자에 만들어 놓은 로그만 찍히는 것을 볼 수 있습니다. 결론적으로 기본 생성자가 존재하지 않는다면 데이터베이스에서 조회해 온 값을 엔티티로 만들 때 객체 생성 자체에 실패하게 되기 때문에, JPA에서는 기본 스펙으로 기본 생성자를 반드시 생성해 줄 것을 정해놓고 있는 것입니다.
그렇다면 왜 Reflection을 사용할까요? 이는 우리가 엔티티로 어떤 타입을 생성할 지 JPA는 알 수 없기 때문입니다. Reflection을 사용하지 않고 객체를 생성하려면 미리 객체의 타입을 알고 있어야 합니다. 하지만 프레임워크나 라이브러리는 사용자가 정의할 구체 클래스 정보를 알 수가 없습니다. 때문에 어떤 타입으로 엔티티를 만들더라도 해당 엔티티를 생성하기 위해 Reflection을 사용하여 엔티티 인스턴스를 만들어 주는 것입니다.
그런데 어차피 Reflection으로 객체를 생성할거라면, 보통 @RequestBody
의 용도로 사용할 클래스들의 기본 생성자의 접근 제한자를 private
으로 제한하는 것 처럼 엔티티의 기본 생성자 역시 private
이어도 상관없지 않을까요?
엔티티 객체의 생성 자체만 생각하면 옳은 얘기일지 모릅니다. 하지만 Proxy
와 Lazy Loading
이라는 개념이 들어가게 되면 다릅니다. JPA의 엔티티를 매핑하는 방식으로 조회 시 연관된 엔티티 정보를 바로 가져오는 즉시 로딩(Eager Loading)
방식과 연관된 엔티티에 프록시, 즉 가짜 객체를 넣어준 뒤 해당 객체의 정보가 실제로 필요한 타이밍에 연관된 엔티티를 조회해오는 지연 로딩(Lazy Loading)
이라는 방식이 있는데요, 여기서 지연 로딩과 프록시 객체를 사용하기 위해서는 생성자가 private이어서는 안됩니다.
어째서 그럴까요? 자세한 것은 지연 로딩과 프록시 객체의 동작 방식에 대해 이해하고 넘어가야 하는 부분인데요, 결론부터 말씀드리자면 엔티티의 프록시는 원본 엔티티를 상속해서 만들기 때문입니다. 지연 로딩으로 인해 프록시 객체를 넣어줘야 하면 원본 엔티티를 상속한 프록시 엔티티를 만들고, 실제 필요한 타이밍에 엔티티를 조회해 온 뒤 프록시 엔티티가 원본 엔티티를 참조하도록 하여 사용하는 것이죠. 때문에 연관된 엔티티 자리에 지연 로딩으로 인해 프록시가 들어가든 즉시 로딩으로 실제 엔티티가 들어가든 상관 없이 항상 정상적으로 기능해야 하니 프록시는 당연히 원래 들어가야 할 엔티티의 하위 타입일 수 밖에 없다고 생각하면 이해가 빠를 것 같습니다.
그럼 이제 왜 엔티티 기본 생성자의 접근 제한자가 private일 수 없는지 감이 오시나요? 만약 기본 생성자가 private으로 선언되어 있다면 해당 엔티티를 상속한 프록시를 만들 수 없을 것입니다. 상속한 객체의 생성자는 반드시 부모 객체의 생성자 super
를 호출해야 하는데, private이면 상속받은 클래스에서 호출할 수 없기 때문입니다. 때문에 엔티티 클래스의 생성자는 private일 수 없습니다. 단, 이는 컴파일 타임에 잡아내는 오류는 아닙니다. 인텔리제이를 사용하면 public이나 protected로 선언된 기본 생성자가 없는 클래스에 Class 'XXX' should have [public, protected] no-args constructor
라는 경고를 볼 수 있지만, 기본 생성자의 접근 제어자에 관련된 예외는 런타임 예외이기 때문에 즉시 로딩을 사용하거나 하여 프록시를 사용할 일이 없다면 관련 예외가 발생하지 않고 정상적으로 동작합니다.
Hibernate 공식 문서 - Chapter 4. Persistent Classes
Reflection API 간단히 알아보자.
자바 ORM 표준 JPA 프로그래밍 - 김영한 저