JPA를 공부하다보면 'JPA는 Entity로 사용할 객체에 반드시 기본 생성자가 있어야 합니다.' 라는 내용을 접한 적이 있을 것이다. 왜 필요해야 하는지 이유를 살펴보고자 한다.
Reflection?
구체적인 클래스 타입을 알지 못해도 클래스의 메서드나, 타입, 변수들에 접근할 수 있도록 해주는 api
자바에서 제공하는 리플렉션(Reflection)은 C, C++과 같은 언어를 비롯한 다른 언어에서는 볼 수 없는 기능이다.
이미 로딩이 완료된 클래스에서 또 다른 클래스를 동적으로 로딩(Dynamic Loading)하여 생성자(Constructor), 멤버 필드(Member Variables) 그리고 멤버 메서드(Member Method) 등을 사용할 수 있도록 한다.
그러니까, 컴파일 시간(Compile Time)이 아니라 실행 시간(Run Time)에 동적으로 특정 클래스의 정보를 객체화를 통해 분석 및 추출해낼 수 있는 프로그래밍 기법이라고 표현할 수 있다.
예제
public class Child {
private String name;
private Integer age;
private String gender;
public Child(String name, Integer age, String gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
public void getOld() {
this.age ++;
}
public void changeName(String newName) {
this.name = newName;
}
public void changeGender() {
if(this.gender.equals("M") {
this.gender = "Fe";
}else {
this.gender = "M";
}
}
자바의 특징 중 하나인 다형성 덕분에 Child 클래스의 부모 타입 클래스로 Child 클래스를 참조할 수 있다.
public static void main(String[] args) {
Object obj = new Child("Choi",27,"M"); // Object 타입은 모든 클래스의 부모 타입
obj.changeName("Park"); // 이름 변경 메소드 호출
}
위에 코드 처럼 obj 타입의 참조 변수로 자식 타입의 메소드인 changeName 을 호출하면 컴파일 에러가 난다.
public static void main(String[] args) {
Object obj = new Child("Choi",27,"M"); // Object 타입은 모든 클래스의 부모 타입
obj.changeName("Park"); // 컴파일 에러 발생 java : cannot find symbol ⛔️
}
이렇게 컴파일 에러가 나는 이유는 obj 참조 변수는 Object 클래스라는 타입만 알 뿐, 자식 클래스의 구체적인 타입을 알지 못하기 때문이다.
자바는 정적 언어로 컴파일 시점에 타입을 결정한다. 따라서 컴파일을 진행할때 obj이라는 객체의 타입을 Object로 결정한 것이다.
컴파일 시점에 타입을 결정하는 정적 언어 : Java, C, C++ 등등
런타임 시점에 타입을 결정하는 동적 언어 : JavaScript, Ruby 등등
컴파일 타임 : 작성한 코드를 기계어로 변환하여 실행가능한 프로그램으로 변환시키는 과정을 컴파일이라 하며, 이 과정동안 일어나는 시간을 컴파일타임 이라고 한다.
런타임 : 컴파일 과정을 마친 프로그램은 사용자에 의해 실행되어 지며, 이러한 응용프로그램이 동작되는 때를 런타임(Runtime) 이라고 부른다.
즉 obj이라는 객체는 자신이 Object타입이라는 사실만 알 뿐, Child 클래스에 대해서는 전혀 알지 못한다.
그렇기에 해당 클래스의 정보(변수, 타입, 메소드 등) 에 접근하지 못한다.
이제 위에서 설명한 구체적인 클래스 타입을 알지 못해도 의 의미를 어느정도 파악할 수 있게 되었다.
이제 Java Reflection API를 이용해서 구체적인 클래스 타입을 알지 못하지만 그 클래스의 메소드에 접근을 해보자.
public static void main(String[] args) throw Exception{
Object obj = new Child("Choi", 27, "M");
Class childClass = Child.class;
Method changeName = childCalss.getMethod("changeName");
changeName.invoke(obj, "ParK");
...
}
해당 코드는 Reflection API 를 통해 구체적인 클래스 Child 을 알지 못해도 changeName 메소드에 접근한 것이다.
Java 에서는 JVM 이 실행되면 사용자가 작성한 자바 코드가 컴파일러를 거쳐 바이트 코드로 변환된다.
이때 변환된 바이트 코드는 static 영역에 저장되는데, Reflection API 는 이 정보를 활용한다.
이때 클래스의 이름만 알고 있다면 static 영역을 탐색하여 해당 클래스이 정보를 가져올 수 있다
JPA Entity에 기본 생성자가 있어야 하는 이유
JPA 는 엔티티 조회시 데이터베이스 값을 객체 필드에 주입해야 한다.
이러한 과정은 컴파일 시점이 아니라 어플리케이션이 돌아가고 있는 런타임 시점에 이루어진다.
이때 Reflection API 을 이용하여 데이터베이스 값(칼럼 값) 을 엔티티에 매핑한다
Refelction API 를 사용하기 위해서는 기본 생성자가 필수적입니다.
이러한 이유 때문에 Reflection API 동작으로 생성되는 JPA 엔티티 클래스에는 기본 생성자가 필요하다.
여기서 주목해야할 점은 기본 생성자의 접근 제어 지시자는 Protected 혹은 Public 이어야 한다
즉 Private 로 선언하면 동일한 에러를 맞이하게 된다.
이렇게 기본 생성자가 없어도 코드가 동작하는 경우가 있다.
@Getter @Setter
@Entity
public class Member {
@Id
private Long id;
private String name;
public Member(Long id, String name){
this.id = id;
this.name = name;
}
}
이는 아래 김영한 님의 답변을 참고하면 좋을 것 같다.
Spring Data JPA와 기본 생성자 private이 불가한 이유?
Spring Data JPA에서 private 접근 제어자는 사용 불가능 한데, 이는 프록시와 관련이 있다.
지연로딩으로 인해 프록시 객체를 사용하는 경우 원본 엔티티를 상속한 프록시 객체를 생성하게 된다.
그 후 실제 사용 시점에 실제 엔티티 정보를 조회하여 프록시 엔티티가 원본 엔티티를 참조하도록 한다.
그러므로 기본 생성자에 private을 사용하게 된다면 상속받은 클래스(프록시 엔티티)에서 호출이 불가능하게 되고
public이나 protected 를 사용해야 한다는 오류가 발생하게 된다.
https://1-7171771.tistory.com/123