GitHub

https://github.com/Choidongjun0830

Spring

[JPA 기본편] 프록시와 연관 관계 관리

gogi masidda 2024. 6. 23. 19:33

프록시

멤버와 팀이 연관된 정보라고 했을 때, 어떤 경우에는 멤버만 가져오고 싶고, 어떤 경우에는 멤버와 팀을 다 가져오고 싶을 수 있다.

 

프록시 기초

  • em.find() vs em.getReference()
    • em.find(): 데이터베이스를 통해서 실제 엔티티 객체 조회
    • em.getReference(): 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회 
      • getReference()를 호출하는 시점에는 데이터베이스의 쿼리를 안하고, 실제 사용되는 시점에 쿼리를 날린다. 
        • getId로 사용할 때는 데이터베이스에서 가져오는 것이 아니라서 쿼리를 날리지 않는다. 
  • 프록시는 실제 클래스를 상속받아서 만들어지고, 실제 클래스와 겉 모양이 같다. 
  • 프록시 객체는 실제 객체의 참조(target)을 보관하고, 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메서드를 호출한다. 
    • 유저가 프록시에 getName()을 호출하면 프록시는 영속성 컨텍스트에 초기화를 요청하고, 영속성 컨텍스트는 DB를 조회하고 실제 Entity를 생성한다. 그리고, 프록시는 target.getName()을 호출하여 유저의 요청에 응답할 수 있게 된다. 
  • 특징
    • 프록시 객체는 처음 사용할 때 한 번만 초기화된다.
    • 프록시 객체를 초기화할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다. 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근이 가능하다.
    • 프록시 객체는 원본 엔티티를 상속받는다. 그래서 타입 체크시에 주의해야 한다.  ( ==은 실패하고, instance of를 사용해야 한다)
    • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReferenct()를 호출해도 실제 엔티티를 반환한다.
      • 1차 캐시에 이미 있으니까.
      •  
    • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제가 발생한다. 
      • 하이버네이트는 org.hibernate.LazyInitializationException 예외를 터트린다. 
  • 확인
    • 프록시 인스턴스의 초기화 여부 확인
      • emf.PersistenceUtil.isLoaded(Object entity)
    • 프록시 클래스 확인 방법
      • entity.getClass().getName() 출력
    • 프록시 강제 초기화
      • org.hibernate.Hibernate.initialize(entity); 

즉시 로딩과 지연 로딩

  • 멤버 클래스에서 Team을 지연 로딩으로 설정해두면, em.find(Member.class, 1L);을 할 때는 Team을 프록시 객체로 두고, 실제 Team을 사용하는 시점에 초기화한다. 
  • 즉시 로딩은 멤버를 가지고 올 때 팀을 같이 가져오는 것. 시스템에서 멤버와 팀을 90%이상 함께 사용할 때.
  • 프록시와 즉시 로딩 주의
    • 가급적 지연 로딩만 사용 ( 특히 실무에서)
    • 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생
    • 즉시 로딩은 JPQL에서 N + 1 문제를 일으킨다.
      • JPQL을 Member만 조회하도록 했는데 쿼리는 Team까지 두개가 나간다. 
      • 최초 쿼리가 1. -> 쿼리 한개만 냈는데 추가 쿼리가 N개가 더 나간다.
    • @ManyToOne, @OneToOne (@...ToOne)은 기본이 즉시 로딩 -> LAZY로 설정
    • @OneToMany, @ManyToMany (@...ToMany)는 기본이 지연 로딩
  • 모든 연관 관계에 지연 로딩 사용
    • JPQL fetch 조인이나, 엔티티 그래프 기능을 사용하기
  • 즉시 로딩은 상상하지 못한 쿼리가 나간다. 

영속성 전이(CASCADE)와 고아 객체

영속성 전이(CASCADE)

  • 특정 엔티티를 영속 상태로 만들 때, 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때
    • 예: 부모 엔티티를 저장할 때, 자식 엔티티도 함께 저장. 
  • 종류
    • ALL: 모두 적용
    • PERSIST: 영속
    • REMOVE: 삭제
    • MERGE: 병합
    • REFRESH
    • DETACH 
            Child child1 = new Child();
            Child child2 = new Child();

            Parent parent = new Parent();
            parent.addChild(child1);
            parent.addChild(child2);

            em.persist(parent);

이런 상태면 persist()를 3번 해야하는데,

 

@Entity
public class Parent {

    @Id @GeneratedValue
    private Long id;

    private String name;

    @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
    private List<Child> childList = new ArrayList<>();

 이렇게 cascade를 해주면 부모만 persist()해줘도 자식이 함께 된다.

그래서 자식이 부모에게만 종속적일 때만 사용해야 한다! 

 

고아 객체

  • 고아 객체 제거: 부모 엔티티와 연관 관계가 끊어진 자식 엔티티를 자동으로 제거
  • orphanRemoval = true
  • 참조하는 곳이 하나일 때 사용해야함. 특정 엔티티가 개인 소유할 때 사용
  • @OneToOne, @OneToMany만 가능
  • 참고: 개념적으로 부모를 제거하면 자식은 고아가 된다. 따라서 고아 객체 제거 기능을 활성화하면, 부모를 제거할 때 자식도 함께 제거된다. 이것은 CascadeType.REMOVE처럼 동작한다. 

 

CascadeType.ALL + orphanRemoval = True

  • 스스로 생명 주기를 관리하는 엔티티는 em.persist()로 영속회, em.remove()로 제거
  • 두 옵션을 모두 활성화하면 부모 엔티티를 통해서 자식의 생명 주기를 관리할 수 있음. 
728x90