안녕하세요! 자바와 스프링으로 개발하다 보면 JPA는 이제 선택이 아닌 필수처럼 느껴집니다. 그런데 JPA를 처음 다룰 때, 우리는 하나의 의문과 마주하게 됩니다.
"왜 엔티티(Entity) 클래스에는 꼭 비어있는 기본 생성자를 만들어야 할까?"
다른 생성자를 만들면 컴파일러가 알아서 기본 생성자를 만들어주지 않는데도, 우리는 굳이 protected Member() {} 같은 코드를 추가하곤 합니다. 그냥 "JPA 규칙이니까"라고 넘어가기엔 너무 궁금합니다.
오늘은 이 오래된 궁금증을 해결하기 위해, JPA 내부의 동작 원리를 살짝 엿보는 시간을 갖겠습니다.
시작: JPA의 고민 "이 데이터를 어디에 담아야 하지?"
우리가 memberRepository.findById(1L) 와 같은 코드를 실행하면, JPA는 우리를 대신해 데이터베이스에 다녀옵니다.
- 개발자: "JPA야, ID가 1번인 회원 정보 좀 찾아줘."
- JPA: "알겠습니다! (DB에 SELECT * FROM member WHERE id = 1 쿼리 실행)"
- DB: "여기 있습니다. id=1, name='홍길동', age=25"
- JPA: "좋아, 데이터를 가져왔다. 이제 이 데이터를 개발자에게 돌려줘야 하는데... 어? 이 id, name, age를 어디에 담아서 주지?"
바로 이 순간, JPA는 DB에서 가져온 데이터를 담을 '그릇', 즉 Member 클래스의 **객체(인스턴스)**를 새로 만들어야 할 필요성을 느낍니다.
JPA의 선택: 가장 간단하고 확실한 약속, "기본 생성자"
자, Member 객체를 만들어야 합니다. 그런데 Member 클래스에 다음과 같이 다양한 생성자가 있다면 어떨까요?
@Entity
public class Member {
@Id
private Long id;
private String name;
private int age;
private String email;
// 생성자 1
public Member(String name, int age) {
// ...
}
// 생성자 2
public Member(String name, String email) {
// ...
}
// ... 또 다른 생성자가 있을지도?
}
JPA는 어떤 생성자를 호출해야 할까요? name과 age를 받는 생성자? 아니면 name과 email을 받는 생성자? JPA는 개발자의 의도를 전혀 알 수 없습니다.
이런 혼란을 피하기 위해 JPA 명세(Specification)는 아주 간단하고 명료한 규칙을 만들었습니다.
"엔티티 객체를 생성할 때는, 어떤 상황에서도 예측 가능하도록 '인자가 없는 기본 생성자'를 사용하기로 약속한다!"
이것이 바로 JPA와 개발자 사이의 사회적 계약입니다.
마법의 기술 1: 애너테이션(Annotation)
JPA는 이 약속을 어떻게 실행할까요? 먼저, 어떤 클래스가 DB와 연결되는 특별한 클래스(엔티티)인지 알아야 합니다. 이때 사용되는 것이 바로 애너테이션(Annotation) 입니다.
@Entity // "JPA야, 이 Member 클래스는 너가 관리해야 할 특별한 엔티티야!"
public class Member {
@Id // "이 필드는 테이블의 기본 키(PK)야."
private Long id;
// ...
}
애너테이션은 코드에 붙이는 '꼬리표' 입니다. JPA는 애플리케이션이 실행될 때 @Entity라는 꼬리표가 붙은 클래스들을 모두 찾아내어 자신의 관리 대상으로 등록합니다.
마법의 기술 2: 리플렉션(Reflection)
자, 이제 JPA는 Member 클래스가 엔티티라는 것을 알게 되었고, DB에서 데이터를 가져왔습니다. 약속대로 기본 생성자를 호출해서 객체를 만들 차례입니다.
이때 사용하는 기술이 바로 리플렉션(Reflection) 입니다. 리플렉션은 프로그램 실행 중에(런타임에) 클래스의 설계도(필드, 메서드, 생성자 정보)를 거울처럼 들여다보고, 심지어 직접 조작까지 할 수 있는 강력한 기술입니다.
1단계: 기본 생성자로 '빈 집' 짓기
JPA는 리플렉션을 사용해 Member 클래스의 기본 생성자를 찾아 호출합니다.
// JPA 내부에서 일어나는 일을 상상해봅시다 (의사 코드)
// 1. Member 클래스의 설계도를 가져온다.
Class<Member> memberClass = Member.class;
// 2. 설계도에서 인자가 없는 생성자를 찾는다.
Constructor<Member> constructor = memberClass.getDeclaredConstructor(); // 기본 생성자 찾기!
// 3. 찾은 생성자를 호출해 '텅 빈' 객체를 만든다.
Member memberInstance = constructor.newInstance();
만약 이때 Member 클래스에 기본 생성자가 없다면? getDeclaredConstructor() 에서 NoSuchMethodException 예외가 발생하며 프로그램은 즉시 중단됩니다. "약속된 기본 생성자가 없어서 일을 진행할 수 없습니다!" 라고 외치는 셈이죠.
이것이 바로 우리가 기본 생성자를 반드시 만들어야 하는 첫 번째 이유입니다.
2단계: '자물쇠'를 열고 값 채워넣기
이제 텅 빈 memberInstance 객체가 만들어졌습니다. 하지만 필드는 모두 private으로 잠겨있죠. JPA는 어떻게 이 private 필드에 DB에서 가져온 값을 채워 넣을까요?
여기서 리플렉션의 두 번째 마법이 등장합니다.
// JPA 내부 상상도 2 (의사 코드)
// 1. DB에서 가져온 이름 '홍길동'을 'name' 필드에 넣어야 한다.
Field nameField = memberClass.getDeclaredField("name");
// 2. 'private'이라는 자물쇠를 강제로 연다!
nameField.setAccessible(true);
// 3. 필드에 직접 값을 '꽂아' 넣는다. (setter를 사용하지 않음!)
nameField.set(memberInstance, "홍길동");
// age, email 등 다른 모든 필드에 대해서도 이 작업을 반복...
setAccessible(true)는 private 접근 제어자를 런타임에 무시하게 만드는 강력한 기능입니다. 이 덕분에 JPA는 setter 메서드가 없어도 필드에 직접 값을 주입할 수 있습니다.
그렇다면 왜 protected를 쓸까?
기본 생성자를 만들 때 public 대신 protected를 권장하는 이유도 명확해집니다.
- public: 누구나 new Member()로 의미 없는 빈 객체를 만들 수 있게 허용합니다. 이는 객체의 상태를 불안정하게 만들 수 있습니다.
- protected: 개발자에게는 "이 생성자는 직접 사용하지 마세요!"라는 메시지를 주면서, JPA(같은 패키지 또는 자식 클래스-프록시)가 접근하는 것은 허용하는 가장 이상적인 타협점입니다.
결론
Q: JPA 엔티티에 기본 생성자는 왜 필수인가요?
A: JPA가 DB에서 데이터를 조회한 후, 그 데이터를 담을 객체를 '런타임'에 동적으로 생성해야 하기 때문입니다. 이때 JPA는 리플렉션 기술을 사용하여 어떤 상황에서든 예측 가능한 '기본 생성자'를 호출하기로 약속(JPA 명세 규칙)했습니다. 따라서 개발자는 이 약속을 지키기 위해 반드시 기본 생성자를 제공해야 합니다.
긴 글 읽어주셔서 감사합니다!

