JPA의 Entity를 작성하다가, 지인에게서 접근제어자가 protected인 기본 생성자를 작성해주어야 한다는 말을 들었다.
왜 엔티티에 기본 생성자를 작성해야 하며, 특히 접근제어자는 왜 protected여야 할까?
기본 생성자를 작성해야 하는 이유에 대해서 알아보기 전에 Java의 Reflection API란 무엇인지 간략하게나마 알고 넘어가야 한다.
Reflection API란 “구체적인 클래스 타입을 알지 못해도 그 클래스의 정보(메서드, 타입, 변수 등)에 접근할 수 있게 해주는 Java API”다.
처음에는 이게 무슨 말인지 도통 이해가 가지 않을테지만, 간단한 예제를 보면 조금은 이해할 수 있을 것이다.
class Bird {
public Bird() {}
public void fly() {
System.out.println("🕊️🕊️🕊️");
}
}
위와 같이 하늘을 날 수 있는 Bird 클래스가 있다고 할 때, Object 타입의 animal은 Bird 클래스의 fly() 메서드를 호출할 수 있을까?
Object animal = new Bird();
animal.fly();
animal 객체는 Object 타입이므로, Object 클래스의 메서드만 사용할 수 있기 때문에 컴파일 오류가 발생할 것이다.
이번에는 다음과 같이 코드를 작성해보자.
Object animal = new Bird();
Class birdClass = Bird.class;
Method fly = birdClass.getMethod("fly");
// fly 메서드 실행
fly.invoke(animal);
작성한 코드를 실행해보면, 놀랍게도 컴파일 에러 없이 새가 하늘을 나는 모습을 볼 수 있다.
어떻게 fly 메서드를 실행할 수 있는걸까?
JVM이 실행되면 사용자가 작성한 코드는 컴파일러를 거쳐 static 영역에 저장되는데, 런타임 시점에 Reflection API가 그 정보를 활용한다. 그래서 클래스 이름만 알고 있다면 static 영역에 있는 해당 클래스에 접근할 수 있게 되는 것이다.
Reflection API를 사용하면 private으로 접근을 막아놓은 필드에도 접근할 수 있게 되므로, 우리가 해당 API를 사용할 일은 없을 것이다.
다만 Spring 프레임워크나 다른 라이브러리는 사용자가 어떤 클래스를 작성할지 모르기 때문에 Reflection API를 사용한다.
JPA 또한 Reflection API를 활용하여 DB에서 가져온 데이터로 객체를 생성한다.
Reflection API로 가져올 수 없는 정보가 몇 가지 있는데, 그중 하나가 바로 생성자의 매개변수다.
따라서 JPA는 기본 생성자를 통해서만 객체를 생성하는 것이고, 이러한 이유로 우리는 엔티티에 기본 생성자를 작성해주어야 한다.
엔티티에 기본 생성자를 작성해주어야 한다는 사실은 이제 알겠다. 그런데 왜 private을 사용하면 안되는 걸까?
그 이유는 JPA의 N+1 문제를 막기 위해서 즉시 로딩 대신 지연 로딩 전략을 사용하기 때문이다.
즉시 로딩은 엔티티를 조회하면 실제 엔티티 정보를 가져오지만, 지연 로딩의 경우 엔티티 조회 시점에 실제 엔티티 대신 프록시를 가져온다. 여기서 엔티티의 프록시는 실제 엔티티를 상속 받은 객체라고 생각하면 된다.
왜냐하면 즉시 로딩으로 인해 실제 엔티티를 가져오던, 지연 로딩으로 인해 프록시를 만들던 항상 똑같이 동작해야 하기 때문에 프록시는 당연히 실제 엔티티의 하위 타입일 수밖에 없는 것이다.
이를 통해서 왜 기본 생성자로 private을 사용하면 안되는지 쉽게 알 수 있다.
하위 타입의 객체를 생성하기 위해서는 반드시 부모 객체의 생성자인 super를 호출해야 하는데, 부모 객체의 생성자가 private이면 호출할 수 없게 된다.
이러한 이유로 엔티티에 기본 생성자를 작성할 때 private 대신 public과 protected를 사용해야 하며, 안정성 측면에서 조금 더 나은 protected를 사용하는 것이다.