Member와 Team을 같이 출력해야 하는 경우, 따로 조회해야 하는 경우 등
비즈니스 로직은 다양하다. 그렇기에 상황에 따라 최적화가 필요함.
지연 로딩, 즉시 로딩 등을 사용하려면 프록시에 대한 이해가 필요하다.
1. Proxy Class
em.find vs em.getReference
- 전자는 DB를 통한 실제 엔티티 객체 조회
- 후자는 DB조회를 미루는 가짜(프록시) 엔티티 객체 조회
Member findMember = em.getReference(Member.class, member.getId());
// 사용을 안하면 SELECT query안나감
Member findMember = em.getReference(Member.class, member.getId());
System.out.println(findMember);
// 사용하면 쿼리가 나감.
이때 findMember의 클래스를 출력해보면 아래와 같다. (find를 사용할 경우는 진짜 class를 줌)
class hellojpa.Member$HibernateProxy$nuiJ9Nzf
Proxy 객체의 정체
- Hibernate가 내부적으로 만드는 객체.
- 실제 클래스를 상속 받아서 만들어졌지만, 겉 모양은 같다. 사용하는 입장에서는 구분하지 않고 사용하면 된다.
- 실제 객체의 참조를 보관
- 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드 호출.
- target(target Entity)의 참조와 메서드만이 존재.
Proxy 객체의 초기화
- 필요한 타겟 엔티티의 요청 시
➡️ 영속성 컨텍스트를 통해서 초기화 요청 ➡️ DB 조회
➡️ 실제 Entity 생성 ➡️ target.getName (proxy 객체의 메서드)와 실제 Entity의 메서드와 매핑
2. Proxy 심화
- 처음 사용할 때 한 번만 초기화
- 프록시 객체 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는게 아니다.
✅ 계속해서 프록시 객체를 사용하되, 내부의 target에만 값이 정해지는 것. 원본 Entity를 상속받는 것.
- 따라서 타입 체크 시 == 비교가 아닌, instanceof 사용해야 한다.
가령 m1이 find, m2가 getReference일 때, (m2 instanceof Member) 처럼 확인,
m1.getClass() == m2.getClass() 로는 확인하지 말 것.
- 따라서 타입 체크 시 == 비교가 아닌, instanceof 사용해야 한다.
- 영속성 컨텍스트에 찾는 Entity가 이미 있으면, getReference()를 호출해도 Entity를 반환한다.
Member m1 = em.find(Member.class, member.getId());
System.out.println(m1.getClass());
Member reference = em.getReference(Member.class, member.getId());
System.out.println(reference.getClass()); // proxy 객체가 아닌, Member 객체로 반환
System.out.println(m1 == reference); // true 이는 transaction 내에서 보장됨
- Proxy를 한 번 조회할 경우, 이후에 find로 조회해도 find로 받은 class는 proxy객체 반환해서
두 개가 서로 같도록 해주더라. - 실무에서 많이 만나는 예외. 준영속 상태일때, proxy 초기화를 할 경우 LazyInitializationException 뜬다.
- 왜냐하면, proxy의 초기화는 무조건 영속성 컨텍스트를 통해 지원되기 때문.
Member reference = em.getReference(Member.class, member.getId());
em.detach(reference);
System.out.println(reference.getUsername());
프록시 확인
- 프록시 초기화 여부 확인
EntityManagerFactory.getPersistenceUnitUtil.isloaded(Object proxy) - 프록시 클래스 확인
entity.getClass().getName() - 프록시 강제 초기화 (메서드 사용이 아닌, 내가 원하는 타이밍)
Hibernate.initialize(Object proxy)
3. 즉시 로딩과 지연 로딩
연관 관계 @ManyToOne과 같은 어노테이션에 fetch = FetchType.LAZY
이렇게 하면 해당 타입을 Proxy로 가져오게 된다.
이후 Team의 field를 실제 사용할 때 초기화(DB 조회)
이를 Lazy Loading 이라고 한다.
즉시 로딩은 FetchType.EAGER를 통해서 사용.
이때는 Proxy가 아닌 Entity로 함께 조회한다.
활용 - 비즈니스 로직상 최적화 판단해서 사용할 것.
- 가급적 지연 로딩만 사용할 것. (특히 실무)
- 즉시 로딩을 적용하면 예상치 못한 SQL이 발생
- 즉시 로딩이 JPQL에서 N+1 문제
- 해결책? 일단 전부 지연로딩으로 바꾸고 FetchJoin을 사용해서
("select m from Member m join fetch m.team")
- 해결책? 일단 전부 지연로딩으로 바꾸고 FetchJoin을 사용해서
- 아래처럼 JPQL로 하면, EAGER로 하더라도 두 번 SELECT 조회(Member, Team 두 번)
- 즉시 로딩이 JPQL에서 N+1 문제
List<Member> members = em.createQuery("select m from Member m", Member.class)
.getResultList();
- @ManyToOne, @OneToOne은 default가 즉시 로딩이기에 LAZY로 설정할 것.
FetchType fetch() default FetchType.EAGER;
- @OneToMany, @ManyToMany는 기본이 지연 로딩
4. 영속성 전이 CASCADE
parent를 persist할 때, child 또한 persist하는게 cascade
연관관계 매핑과는 관련 없고, persist상의 편의를 제공하는 것일 뿐.
Cascade 종류 ALL, PERSIST 두 개 정도만 추천
REMOVE, MERGE 등... 다른 옵션도 있기는 함.
- 써야할 때? parent라는 엔티티가 child를 관리할 때는 사용해도 괜찮음.(소유자가 하나일 때)
- 안써야할 때? 파일을 다른 엔티티에서 관리할 때 (소유자가 하나가 아닐 때)
5. 고아 객체
부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제
연관관계 어노테이션에 orphanRemovel = true 추가 후
children.remove()를 통해서 삭제할 경우, 엔티티 자체를 삭제한다.
주의 사항
- 참조하는 곳이 하나일 때 사용해야 한다.
특정 엔티티가 개인 소유할 때 사용할 것. - cascadeType.REMOVE 처럼 작동 -> parent가 remove를 통해 제거될 경우, child도 같이 날아간다.
고아 객체 + 영속성 Cascade를 통해서 할 수 있는 것?
- CascadeType.ALL + orphanRemoval = true를 할 경우
- 부모 Entity를 통해 자식의 생명 주기를 관리할 수 있음.
'정보 > Database' 카테고리의 다른 글
JPA와 데이터 타입 (2) | 2024.11.17 |
---|---|
연관 관계 Mapping (3) (0) | 2024.11.17 |
연관 관계 Mapping (2) (1) | 2024.11.15 |
연관 관계 Mapping (1) (0) | 2024.11.15 |
Entity Mapping (3) | 2024.11.15 |