일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 객체지향 쿼리 언어
- ddd
- 큐시즘
- redis
- 최범균
- 자바 ORM 표준 JPA 프로그래밍
- 중간 장소 추천
- GitHub Actions
- K3S
- 쿠버네티스
- 도메인 주도 개발 시작하기
- 모이삼
- Container Registry
- RESTClient
- JPA
- kusitms
- 약속 장소 추천
- Spring
- 불변객체
- 모임 장소 추천
- Spring Batch
- 이펙티브자바
- java
- cicd
- springboot
- JPQL
- 한국대학생it경영학회
- Domain Driven Design
- 중간 지점 추천
- 백엔드
- Today
- Total
코딩은 마라톤
[JPA] 기본 생성자가 private인데도 에러가 안 나는 이유 본문
들어가며
엔티티를 설계하다 보면 기계적으로 붙이는 애노테이션이 있다.
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
항상 위 2개의 애노테이션을 붙이면서 엔티티 관련 문제가 발생한 적이 없고, 잘 쓰고 있었다.
NoArgsConstructor를 사용하는 이유는 JPA에서 엔티티 생성 시 Reflection 방식을 사용하는데 이때 기본 생성자가 필요하기 때문이다. 또한 지연로딩(Lazy Loading)을 사용해 연관된 엔티티를 조회할 때 실제로 사용하기 전까지는 프록시 객체를 사용하는데, 이때 기본 생성자가 private으로 선언되어 있다면 해당 엔티티를 상속한 프록시 객체를 사용할 수 없어 이를 방지하기 위해 public이나 protected를 사용해 기본 생성자를 만든다.
만약 private로 기본 생성자를 선언했다면, IDE는 친절하게 경고를 제공한다.
가설 : private으로 선언된 기본 생성자를 사용하면 프록시 객체가 생성되지 않는다.
@Data
@Entity
public class Taco {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private Date createdAt;
@NotNull
@Size(min = 5, message = "이름은 최소 5글자 이상이어야 합니다.")
private String name;
@ManyToMany(targetEntity = Ingredient.class)
@Size(min = 1, message = "최소 1개 이상의 재료가 선택해야 합니다.")
private List<Ingredient> ingredients;
@PrePersist
void createdAt() {
this.createdAt = new Date();
}
}
Taco 엔티티
- Taco 엔티티의 기본 생성자는 public으로 되어 있다.
- @ManyToMany로 Ingredient 엔티티와 연관 관계가 맺어 있고, ManyToMany는 지연 로딩이 기본값이다.
@Data
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@Entity
public class Ingredient {
@Id
private String id;
private String name;
private Type type;
public enum Type {
WRAP, PROTEIN, VEGGIES, CHEESE, SAUCE,
}
}
Ingredient 엔티티
- Ingredient 엔티티의 기본 생성자는 private으로 되어 있다.
실행 결과 확인
프록시 관련 에러가 발생하길 바라며, 테스트 코드를 돌려보면? 🧐
@DataJpaTest
class TacoRepositoryTest {
@Autowired
private TestEntityManager entityManager;
@Autowired
private TacoRepository tacoRepository;
@Test
void PRIVATE_생성자에서_프록시객체가_생성된다() {
// 1. 재료 데이터 저장
Ingredient ingredient = new Ingredient("ING1", "Lettuce", Ingredient.Type.VEGGIES);
entityManager.persist(ingredient);
// 2. 타코 데이터 저장
Taco taco = new Taco();
taco.setName("Veggie Taco");
taco.setIngredients(List.of(ingredient));
entityManager.persist(taco);
entityManager.flush();
entityManager.clear(); // 1차 캐시 초기화 (프록시 확인)
// 3. Taco 조회 (Lazy 로딩 확인)
Taco foundTaco = tacoRepository.findById(taco.getId()).orElseThrow();
System.out.println(">>> Taco 조회 완료");
// 4. Ingredient 프록시 여부 확인
List<Ingredient> ingredients = foundTaco.getIngredients();
System.out.println(">>> ingredients 클래스: " + ingredients.getClass().getName());
System.out.println(">>> 첫 번째 ingredient 클래스: " + ingredients.get(0).getClass().getName());
// 5. 실제 필드 접근 (강제 초기화)
System.out.println(">>> 첫 번째 ingredient ID: " + ingredients.get(0).getId());
}
}
서두에서 밝혔듯이, 연관관계가 맺어 있고, Taco 엔티티를 불러오면 Ingredient 엔티티가 지연 로딩으로 프록시 객체가 생성되어 프록시 관련 에러가 발생해야 한다.
하지만...
테스트가 성공함을 볼 수 있다..
왜 이런 일이 발생할까?
Hibernate:
select
next value for taco_seq
Hibernate:
insert
into
ingredient
(name, type, id)
values
(?, ?, ?)
Hibernate:
insert
into
taco
(created_at, name, id)
values
(?, ?, ?)
Hibernate:
insert
into
taco_ingredients
(taco_id, ingredients_id)
values
(?, ?)
Hibernate:
select
t1_0.id,
t1_0.created_at,
t1_0.name
from
taco t1_0
where
t1_0.id=?
>>> Taco 조회 완료
>>> ingredients 클래스: org.hibernate.collection.spi.PersistentBag
Hibernate:
select
i1_0.taco_id,
i1_1.id,
i1_1.name,
i1_1.type
from
taco_ingredients i1_0
join
ingredient i1_1
on i1_1.id=i1_0.ingredients_id
where
i1_0.taco_id=?
>>> 첫 번째 ingredient 클래스: spring.springinaction.tacos.domain.model.Ingredient
>>> 첫 번째 ingredient ID: ING1
테스트 코드의 쿼리를 보자.
Taco를 조회하면 ingredients는 지연 로딩으로 프록시 객체여야 한다.
ingredients 클래스: org.hibernate.collection.spi.PersistentBag
하지만 Proxy 객체가 아닌 PersistentBag이라는 생소한 클래스를 확인할 수 있다.
👜 PersistentBag
- Hibernate가 엔티티의 컬렉션 필드를 관리하기 위해 제공하는 Collection Wrapper이다.
- 하이버네이트는 Entity를 영속화할 때, Entity 안의 Collection을 추적하고 관리하기 위해 원본 컬렉션을 감싼 내장 컬렉션을 사용한다.
- 원본 Collection, List를 하이버네이트가 제공하는 내장 컬렉션(PersistentBag)으로 변경한다. (Set은 PersistentSet을 사용)
정리
프록시가 필요한 경우와 필요 없는 경우를 아래와 같이 나타낼 수 있다.
- 프록시가 필요한 경우 (단일 엔티티 연관관계: @ManyToOne, @OneToOne)
- 프록시가 필요 없는 경우 (컬렉션 연관관계: @OneToMany, @ManyToMany)
프록시가 필요한 경우인 단일 엔티티 연관관계에서는 지연 로딩에서 바로 프록시 객체를 사용하기 때문에 private 기본 생성자를 사용하면 에러가 발생한다.
프록시가 필요 없는 경우인 컬렉션 연관관계에서는 내장 컬렉션을 프록시 대신 사용하므로 private 기본 생성자라도 문제가 발생하지 않음을 알 수 있다.
하지만 JPA, Hibernate 표준 스펙에서 기본 생성자를 protected 이상으로 사용하길 권장한다.
따라서 웬만하면 protected로 기본 생성자를 선언함으로써, 상속과 캡슐화를 고려해서 사용하는 것이 가장 적절한 선택이라 생각한다!
참고
- 자바 ORM 표준 JPA 프로그래밍
- https://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/chapters/domain/entity.html
Entity types
The goal of the @Synchronize annotation in the AccountSummary entity mapping is to instruct Hibernate which database tables are needed by the underlying @Subselect SQL query. This is because, unlike JPQL and HQL queries, Hibernate cannot parse the underlyi
docs.jboss.org
'Backend > JPA' 카테고리의 다른 글
[JPA] 엔티티 저장 후 조회, 동등성 문제 및 해설 (@Transactional) (3) | 2024.12.31 |
---|---|
[자바 ORM 표준 JPA 프로그래밍] 10장. Criteria 쿼리 (1) | 2024.06.21 |
[자바 ORM 표준 JPA 프로그래밍] 10장. JPQL (1) (1) | 2024.06.16 |
[자바 ORM 표준 JPA 프로그래밍] 9장. 값 타입 (2) | 2024.03.17 |
[자바 ORM 표준 JPA 프로그래밍] 8장. 프록시와 연관관계 관리 (1) | 2024.02.25 |