본문 바로가기

정보/Database

프록시, JPA 최적화

 

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() 로는 확인하지 말 것.
  • 영속성 컨텍스트에 찾는 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")
    • 아래처럼 JPQL로 하면, EAGER로 하더라도 두 번 SELECT 조회(Member, Team 두 번)
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