본문 바로가기

BackEnd/JPA

[JPA] 프록시와 연관관계 관리

프록시

  • em.find() vs em.getReference()
  • em.find() : 데이터베이스를 통해 실제 엔티티 객체를 조회한다.
  • em.getReference() : 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체를 조회한다.
    김영한의 자바 ORM 표준 JPA 프로그래밍

 

 

 프록시 특징

 

  • 실제 클래스를 상속 받아서 만들어지며, 실제 클래스와 겉 모양이 같다.
  • 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다.
  • 프록시 객체는 실제 객체의 참조(target)을 보관한다.
  • 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드를 호출한다.
    김영한의 자바 ORM 표준 JPA 프로그래밍

 프록시 객체의 초기화

  • 프록시 객체는 처음 사용할 때 한번만 초기화 된다.
  • 프록시 객체를 초기화 할때, 프록시 객체가 실제 엔티티로 바뀌는 것이 아니다.  초기화 되면 프록시 객체를 통해 실제 엔티티에 접근이 가능한 것이다.
    Member member = em.getreference(Member.class, "id1");
    member.getName();​

    김영한의 자바 ORM 표준 JPA 프로그래밍
  • 프록시 객체는 원본 엔티티를 상속받는다.  따라서 타입 체크 시 주의가 필요하다.
    ( == 비교 실패, 대신 instance of를 사용해 동일한지 확인이 가능하다)
    Member member1 = new Member();
    member1.setUserName("hello1");
    em.persist(member1);
    Member member2 = new Member();
    member2.setUserName("hello2");
    em.persist(member2);
    
    // 프록시는 Member가 아니므로 비교 시 동일하지 않다.
    System.out.println("member1 == member2 " +( m1.getClass() == m2.getClass()));  // false
    System.out.println("member1 == member2" + (m1 instanceof Member));  // true
    System.out.println("member1 == member2" + (m2 instanceof Member));  // true​
  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시 초기화 시 오류가 발생한다.
    (하이버네이트는 org.hibernate.LazyInitializationException 예외 발생)

 프록시확인

  • 프록시 인스턴스의 초기화 여부를 확인할 수 있다.
    PersistenceUnitUtil.isLoaded(Object entity)
  • 프록시 클래스 확인 방법
    entity.getClass().getName() 출력 (..javasist.. or HibernateProxy..)
  • 프록시 강제 초기화
    org.hibernate.Hibernate.initialize(entity);
  • 참고 : JPA 표준은 강제 초기화가 존재하지 않는다.  표준일 경우 강제 호출을 통해 초기화한다.
    member.getName();
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
    EntityManager em = emf.createEntityManager();
    EntityTransaction tx = em.getTransaction();
    tx.begin();  // 시작
    try {
        Member member1 = new Member();
        member1.setUserName("hello1");
        em.persist(member1);
        em.flush();
        em.clear();
    
        Member refMember = em.getReference(Member.class, member1.getId());
        
        System.out.println("before isLoaded = " + emf.getPersistenceUnitUtil().isLoaded(refMember)); // false
        Hibernate.initialize(refMember); // 프록시 강제 초기화
        // refMember.getUserName();      // 프록시 강제 호출
        System.out.println("after isLoaded = " + emf.getPersistenceUnitUtil().isLoaded(refMember));  // true          
    
        tx.commit();
    } catch (Exception e) {
        System.out.println(e.getMessage());
        tx.rollback();
    } finally {
        em.close();
    }
    emf.close();

즉시 로딩과 지연 로딩

 지연 로딩 LAZY를 사용해서 프록시로 조회한다.

  • 단순히 member의 정보만을 조회하는 비지니스 로직이 존재할 경우 Team도 항상 조회할 필요성이 있을까?
    - 단순히 member의 정보만을 확인하면 되는데 Team도 함께 조회 시 성능상 비효율적이다.
    - 지연로딩 LAZY를 사용해서 member만을 불러와 Team을 불러오지 않고 member만 참조할 수 있다.
    @Entity
    public class Member {
        @Id @GeneratedValue
        @Column(name = "member_id")
        private Long id;
    
        @ManyToOne(fetch = FetchType.LAZY)  // 지연로딩
        @JoinColumn(name="team_id")
        private Team team;
    }
    
    @Entity
    public class Team {
        @Id @GeneratedValue
        @Column(name = "team_id")
        private Long id;
        private String name;
    }
    김영한의 자바 ORM 표준 JPA 프로그래밍

 지연 로딩 LAZY을 사용해 프록시로 조회

  • Member만을 조회하고 Team 관련 값들을 호출하지 않았을 경우 Member만을 조회한다.|
    Member member1 = new Member();
    member1.setUserName("hello1");
    em.persist(member1);
    
    Team team = new Team();
    team.setName("teamA");
    member1.setTeam(team);
    em.persist(team);
    
    em.flush();
    em.clear();
    
    Member m1 = em.find(Member.class, member1.getId());​
    김 
  • Team의 값을 실제 불러왔을 경우에 비로소 Team의 값을 조회한다.
    Team team = member.getTeam();
    team.getName();  // 실제 team을 사용하는 시점에 초기화(DB조회)​

 즉시 로딩 EAGER를 사용해서 함께 조회

  • 지연 로딩과 달리 Member와 Team을 자주 함께 사용할 경우 사용한다.
  • Member를 조회 시 항상 Team도 조회가 된다.
  • JPA 구현체는 가능하면 조인을 사용해서 SQL 한번에 함께 조회한다.
@Entity
public class Member {
    @Id @GeneratedValue
    @Column(name = "member_id")
    private Long id;

    @ManyToOne(fetch = FetchType.EAGER)  // 지연로딩
    @JoinColumn(name="team_id")
    private Team team;
}

@Entity
public class Team {
    @Id @GeneratedValue
    @Column(name = "team_id")
    private Long id;
    private String name;
}

김영한의 자바 ORM 표준 JPA 프로그래밍

 프록시와 즉시로딩 주의점

  • 가급적 지연로딩만을 사용해야 한다. 왜냐하면,  즉시 로딩을 적용 시 예상치 못한 SQL을 발생하기 떄문이다.
  • 즉시 로딩은 JPQL에서 N + 1 문제를 일으킨다. 
  • @ManyToOne, @OneToOne은 기본이 즉시 로딩이므로 필히 LAZY(지연)로 설정이 필요하다.
  • @OneToMany, @ManyToMany는 기본이 지연 로딩이다.
  • 지연, 즉시 로딩의 활용이 가능하나 모든 연관관계를 우선적으로 지연 로딩으로 사용해야 한다.
  • 즉시 로딩이 필요 시 JPQL fetch 조인이나, 엔티티 그래프 기능을 사용해야 한다.
List<Member> members = new ArrayList<>();
members = em.createQuery("select m from Member m left join fetch m.team", Member.class).getResultList();

 

 

 

영속성 전이 : CASCADE

  • 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때 사용한다.
    (예 : 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장한다.)
    김영한의 자바 ORM 표준 JPA 프로그래밍

 영속성 전이 : 저장

  • 영속성 전이는 연관관계를 매핑하는 것과는 아무 관련이 없다.
  • 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함 때문에 제공이 되는 것 뿐이다.
  • CASCADE의 타입 종류 : ALL(모두적용), PERSIST(영속), REMOVE(삭제), MERGE(병합)
    , REFRESH(REFRESH), DETACH(DETACH)가 있다.
  • 실제 사용하기 위해서는 ManyToOne관계에 영속성 전이를 영속타입으로 설정한다.
    @Entity
    public class Parent {
        @Id @GeneratedValue
        @Column(name = "parent_id")
        private Long id;
        private String name;
    
        @OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST) // 영속성전이 설정
        private List<Child> childList = new ArrayList<>();
    
        // 양방향 연관관계 편의 함수
        public void addChild(Child child) {
            childList.add(child);
            child.setParent(this);
        }
    }
    
    @Entity
    public class Child {
        @Id @GeneratedValue
        @Column(name = "child_id")
        private Long id;
        private String name;
    
        @ManyToOne
        @JoinColumn(name = "parent_id")
        private Parent parent;
    }​
  • 부모 엔티티인 parent에 대해서만 persist를 진행해도 자식들도 영속이기 때문에 함께 저장된다.
    Child child1 = new Child();
    Child child2 = new Child();
    Parent parent =  new Parent();
    parent.addChild(child1);
    parent.addChild(child2);
    em.persist(parent);​

고아 객체

  • 고아 객체는 부모엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 것을 의미한다.
  • 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 판단해 삭제하는 기능이다.
  • 고아 객체는 참조하는 곳이 하나일 때 사용해야 한다.  즉, 특정 엔티티가 개인 소유할 때 사용해야 한다.
  • @OneToOne, @OneToMany만 고아객체를 사용할 수 있다.
  • 관계형 데이터베이스 관점으로 보았을 경우 부모를 제거 시 자식은 고아가 된다.
    따라서 고아 객체 제거 기능을 활성화 시 부모를 제거할 때 자식도 함께 제거되는 것이다.
    이것은 CascadeType.REMOVE처럼 동작한다.
  • 아 객체도 연관관계 설정 시 엔티티에 해당 속성을 사용한다.
    @Entity
    @Getter
    @Setter
    public class Parent {
        @Id @GeneratedValue
        @Column(name = "parent_id")
        private Long id;
        private String name;
    
        @OneToMany(mappedBy = "parent", orphanRemoval = true) // 고아 객체 설정
        private List<Child> childList = new ArrayList<>();
    
        // 양방향 연관관계 편의 함수
        public void addChild(Child child) {
            childList.add(child);
            child.setParent(this);
        }
    }
    
    @Entity
    public class Child {
        @Id @GeneratedValue
        @Column(name = "child_id")
        private Long id;
        private String name;
    
        @ManyToOne
        @JoinColumn(name = "parent_id")
        private Parent parent;
    }
  • 고아 객체 설정 후 부모를 삭제 시 자식도 함께 삭제된다.
    Child child1 = new Child();
    Child child2 = new Child();
    Parent parent =  new Parent();
    parent.addChild(child1);
    parent.addChild(child2);
    em.persist(parent);
    em.persist(child1);
    em.persist(child2);
    
    em.flush();
    em.clear();
    Parent findParent = em.find(Parent.class, parent.getId());
    em.remove(findParent);
      

영속성 전이 + 고아 객체, 생명주기

  • Cascade.Type.ALL + orphanRemoval=true를 조합 해 생명주기를 관리할 수 있다.
    @Entity
    @Getter
    @Setter
    public class Parent {
        @Id @GeneratedValue
        @Column(name = "parent_id")
        private Long id;
        private String name;
    	// 영속성 전이 + 고아 객체 설정
        @OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST, orphanRemoval = true) 
        private List<Child> childList = new ArrayList<>();
    
        // 양방향 연관관계 편의 함수
        public void addChild(Child child) {
            childList.add(child);
            child.setParent(this);
        }
    }
    
    @Entity
    public class Child {
        @Id @GeneratedValue
        @Column(name = "child_id")
        private Long id;
        private String name;
    
        @ManyToOne
        @JoinColumn(name = "parent_id")
        private Parent parent;
    }​
  • 스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화하고 em.remove()로 제거한다.
    Child child1 = new Child();
    Child child2 = new Child();
    Parent parent =  new Parent();
    parent.addChild(child1);
    parent.addChild(child2);
    em.persist(parent);
    // 영속성 전이기능 활성화로 인한 생략 가능
    // em.persist(child1);
    // em.persist(child2);
    
    em.flush();
    em.clear();
    Parent findParent = em.find(Parent.class, parent.getId());
    em.remove(findParent);
    
    // 고아 객체 기능 활성화로 생략 가능
    //findParent.getChildList().remove(0);
  • 영속성 전이와 고아 객체 옵션을 활성화 시 부모 엔티티를 통해 자식의 생명주기를 관리할 수 있다.
  • 도메인 주도 설계(DDD)의 Aggregate Root 개념을 구현할 때 유용하다.