엔티티 객체에서 Setter를 삭제하고 정적 팩토리 메서드를 이용해서 객체 생성에 필요한 인자값을 넘겨서 객체를 생성하는 방식을 적용해보았습니다.
@Entity
@Getter
public class Question {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(length = 200)
private String subject;
@Column(columnDefinition = "TEXT")
private String content;
private LocalDateTime createDate;
@OneToMany(mappedBy = "question", cascade = CascadeType.REMOVE)
private List<Answer> answerList;
private Question(String subject, String content) {
this.subject = subject;
this.content = content;
this.createDate = LocalDateTime.now();
}
public static Question of(String subject, String content) {
return new Question(subject, content);
}
}
@Entity
@Getter
public class Answer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(columnDefinition = "TEXT")
private String content;
private LocalDateTime localDateTime;
@ManyToOne(fetch = FetchType.LAZY)
private Question question;
private Answer(String content, Question question) {
this.content = content;
this.question = question;
this.localDateTime = LocalDateTime.now();
}
public static Answer of(String content, Question question) {
return new Answer(content, question);
}
}

@Test
void findQuestionTest() {
List<Question> all = this.questionRepository.findAll();
assertEquals(2, all.size());
Question q = all.get(0);
assertEquals("sbb가 무엇인가요?", q.getSubject());
}
간단히 Question이 DB에 잘 저장되어 있는지 확인하는 코드이다.
하지만, 이 테스트 코드를 실행하면 오류가 발생한다.

에러 내용을 보면 No default constructor for entity 'com.mysite.question.Question Question 엔티티에 기본 생성자가 없어서 나는 에러같다.
@Entity
@Getter
public class Question {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(length = 200)
private String subject;
@Column(columnDefinition = "TEXT")
private String content;
private LocalDateTime createDate;
@OneToMany(mappedBy = "question", cascade = CascadeType.REMOVE)
private List<Answer> answerList;
private Question() {
}
private Question(String subject, String content) {
this.subject = subject;
this.content = content;
this.createDate = LocalDateTime.now();
}
public static Question of(String subject, String content) {
return new Question(subject, content);
}
}
자바에서 기본 생성자(default constructor)는 매개변수가 없는 생성자를 말하므로 Question엔티티에 매개변수가 없는 생성자를 private 접근자로 추가해주었다.
기본 생성자를 추가해주니 테스트가 정상적으로 통과한다
그런데 뭔가 이상하다.
기본 생성자를 만들어 줬지만private접근자로 만들었기 때문에Question의 정적 팩토리 메서드인of에서만 접근이 가능하다.
외부에서는 접근이 안되는데 굳이 왜 기본 생성자를 추가해줘야 하는 걸까?
Reflection API이란?
구체적인 클래스 타입을 알지 못해도 그 클래스의 정보(매서드, 타입, 변수 등등)에 접근할 수 있게 해주는 자바 API
동작 원리
자바에서는 JVM이 실행되면 사용자가 작성한 자바 코드가 컴파일러를 거쳐 바이트 코드로 변환되어 static 영역에 저장된다. Reflection API는 이 정보를 활용한다. 그래서 클래스 이름만 알고 있다면 언제는 static 영역을 뒤져서 정보를 가져올 수 있다. 직접 접근할 수 없는 private 인스턴스 변수, 메서드에 접근 할 수 있다.
참고
Reflection API로 가져올 수 없는 정보 중 하나가 생성자의 인자 정보이다. 그래서 기본 생성자가 반드시 있어야 객체를 생성할 수 있다.
기본 생성자로 객체를 생성만 하면 필드 값 등은 Reflection API로 넣어 줄 수 있다.
Spring Data JPA 에서 Entity에 기본 생성자가 필요한 이유는 동적으로 객체 생성 시 Reflection API를 활용하기 때문이다.
JPA는 DB값을 객체 필드에 주입할 때 기본 생성자로 객체를 생성한 후 Reflection API를 사용하여 값을 매핑한다. 때문에 기본 생성자가 없다면 Reflection API는 해당 객체를 생성할 수 없기 때문에 기본 생성자가 필요하다.
요약하자면 JPA동작 과정에서 Reflection API를 사용해 객체를 생성해야 하기 때문에 기본 생성자가 필요하다.
JPA를 사용하는 Entity객체에는 기본 생성자가 필요하다는 것을 알 수 있었다.
하지만, 관련 내용을 조금 더 찾아보다 보니 Entity 객체의 기본 생성자의 접근 제한자는 public이나 protected로 설정해야한다는 글이 많았다.
위의 테스트 코드를 보면 private 접근 제한자로 기본 생성자를 생성하고 테스트를 진행했을때 정상 동작했었는데 뭔가 이상하다..
기본 생성자의 접근제한자를 private가 아닌 public이나 protected로 해야하는 이유에 대해 궁금증이 생겨 다른 테스트 코드를 작성해보았다.
@Entity
@Getter
public class Answer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(columnDefinition = "TEXT")
private String content;
private LocalDateTime localDateTime;
@ManyToOne(fetch = FetchType.EAGER)
private Question question;
private Answer() {
}
private Answer(String content, Question question) {
this.content = content;
this.question = question;
this.localDateTime = LocalDateTime.now();
}
public static Answer of(String content, Question question) {
return new Answer(content, question);
}
}
Answer 엔티티에 private 접근제한자로 기본 생성자를 추가하고 question 필드에 FetchType.EAGER 옵션을 주었다.
FetchType.EAGER를 주면 Answer를 조회할때 Question에 대한 정보도 같이 가져오게 된다.
@DisplayName("Answer 조회 테스트")
@Test
void AnswerQuestionTest() {
Optional<Answer> answer = answerRepository.findById(1);
assertTrue(answer.isPresent());
System.out.println(answer.get().getQuestion());
}
DB에 ID가 1인 값이 있으면 가져오고 콘솔에 가져온 Question을 찍는 단순한 테스트이다.
@Entity
@Getter
public class Answer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(columnDefinition = "TEXT")
private String content;
private LocalDateTime localDateTime;
@ManyToOne(fetch = FetchType.LAZY)
private Question question;
private Answer() {
}
private Answer(String content, Question question) {
this.content = content;
this.question = question;
this.localDateTime = LocalDateTime.now();
}
public static Answer of(String content, Question question) {
return new Answer(content, question);
}
}
위의 코드와 유사하지만 question필드에 FetchType.EAGER 대신에 FetchType.LAZY가 붙었다. 이 옵션이 붙으면 Answer를 조회할때 Question의 정보를 같이 가져오지 않고 Answer를 통해서 Qeustion에 직접 접근할때 DB에서 Question에 대한 정보를 가져오게 된다.
@DisplayName("Answer 조회 테스트")
@Test
void AnswerQuestionTest() {
Optional<Answer> answer = answerRepository.findById(1);
assertTrue(answer.isPresent());
System.out.println(answer.get().getQuestion());
}
테스트 코드는 Answer 조회 테스트 1과 같다.
하지만, 테스트 결과는 실패다.
에러 문구를 보면 NoSuchMethodException, Question$HibernateProxy$mMMVpd3V.<init>() 뭔진 모르겠지만Question$HibernateProxy에 <init> 즉, 생성자가 없어서 나는 에러 같다.
아니 private로 기본 생성자를 만들어 줬는데 왜 생성자가 없다는 에러가 날까?
private로 기본 생성자를 만들면 안되는 이유는 proxy 객체 때문이다.
위의 Answer 테스트에서 Fetch.Type = LAZY를 사용할때만 에러가 발생했었다. 바로 Fetch.Type = LAZY 동작 과정에서 proxy 객체를 사용하게 된다.
Fetch.Type = LAZY 즉, 지연 로딩을 사용하면 JPA는 proxy 객체를 사용하여 연관된 데이터를 실제 사용하는 시점에 조회할 수 있는데, proxy 객체는 직접 만든 객체를 상속하기 때문에 public 혹은 protected 기본 생성자가 필요하다. (private로 생성자를 만들게 되면 파생 클래스로부터의 상속 형태가 접근 불가가 되어버리기 때문에 제약이 생기게 된다)
JPA의 Entity는 반드시 파라미터가 없는 기본 생성자를 지녀야 한다.
그리고 그 기본 생성자는 public, protected 이어야하고 private 으로 선언해서는 안된다.
https://hyeonic.tistory.com/191
https://velog.io/@yyy96/JPA-%EA%B8%B0%EB%B3%B8%EC%83%9D%EC%84%B1%EC%9E%90
https://tecoble.techcourse.co.kr/post/2020-07-16-reflection-api/