08강. 프록시와 연관관계 관리


01. 프록시


프록시 기초


  • em.find() vs em.getReference()
    • em.find(): 데이터베이스를 통해서 실제 엔티티 객체 조회
    • em.getReference(): 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회
      • DB에 쿼리가 나가지 않는데 객체가 조회됨
      • target이 진짜 reference를 가리킴

  • 예제
    • getReference

        Member member = new Member();
        member.setUsername("hello");
              
        em.persist(member);
              
        em.flush();
        em.clear();
              
        Member findMember = em.getReference(Member.class, member.getId());
              
        tx.commit();
      
        Hibernate: 
            /* insert hellojpa.Member
                */ insert 
                into
                    Member
                    (createdBy, createdDate, lastModifiedBy, lastModifiedDate, USERNAME, MEMBER_ID) 
                values
                    (?, ?, ?, ?, ?, ?)
      
    • getReference + ?

        Member member = new Member();
        member.setUsername("hello");
              
        em.persist(member);
              
        em.flush();
        em.clear();
              
        Member findMember = em.getReference(Member.class, member.getId());
              
        System.out.println("findMember.id = " + findMember.getId()); // 파라미터로 넣은 ID는 DB에 가지 않아도 알 수 있음
        System.out.println("findMember.username = " + findMember.getUsername()); // Username은 DB에 있음. Reference에 없어서 DB에 쿼리를 날림
              
        tx.commit();
      
        Hibernate: 
            /* insert hellojpa.Member
                */ insert 
                into
                    Member
                    (createdBy, createdDate, lastModifiedBy, lastModifiedDate, USERNAME, MEMBER_ID) 
                values
                    (?, ?, ?, ?, ?, ?)
        findMember.id = 1 // DB에서 가져오지 않아도 됨
        Hibernate: // DB 찍고 가져옴
            select
                member0_.MEMBER_ID as MEMBER_I1_3_0_,
                member0_.createdBy as createdB2_3_0_,
                member0_.createdDate as createdD3_3_0_,
                member0_.lastModifiedBy as lastModi4_3_0_,
                member0_.lastModifiedDate as lastModi5_3_0_,
                member0_.TEAM_ID as TEAM_ID7_3_0_,
                member0_.USERNAME as USERNAME6_3_0_,
                team1_.TEAM_ID as TEAM_ID1_7_1_,
                team1_.createdBy as createdB2_7_1_,
                team1_.createdDate as createdD3_7_1_,
                team1_.lastModifiedBy as lastModi4_7_1_,
                team1_.lastModifiedDate as lastModi5_7_1_,
                team1_.name as name6_7_1_ 
            from
                Member member0_ 
            left outer join
                Team team1_ 
                    on member0_.TEAM_ID=team1_.TEAM_ID 
            where
                member0_.MEMBER_ID=?
        findMember.username = hello
      
    • findMember는 Proxy

        Member member = new Member();
        member.setUsername("hello");
              
        em.persist(member);
              
        em.flush();
        em.clear();
              
        Member findMember = em.getReference(Member.class, member.getId());
              
        System.out.println("findMember = " + findMember.getClass());
      
        Hibernate: 
            /* insert hellojpa.Member
                */ insert 
                into
                    Member
                    (createdBy, createdDate, lastModifiedBy, lastModifiedDate, USERNAME, MEMBER_ID) 
                values
                    (?, ?, ?, ?, ?, ?)
        findMember = class hellojpa.Member$HibernateProxy$LDM2UMzq
      
      • findMember는 Member가 아니고 Member$HibernateProxy : Hibernate가 만든 가짜 Proxy



프록시 객체의 초기화


Member member = em.getReference(Member.class, id1);
member.getName();

ex.

member는 프록시 객체를 가져온 상태

  1. member.getName()을 호출
  2. 처음에 Proxy를 봤는데, target에 값이 없음
  3. JPA가 영속성 컨텍스트에 요청 : member 객체 가져와 : 초기화 요청 !!
  4. 영속성 컨텍스트가 DB 조회해서 실제 Entity 객체를 생성해서 Member를 돌려주고 target에 연결
  5. 이제 getName() 하면 target의 getName()을 통해 Member의 getName()을 반환



프록시의 특징


  • 실제 클래스를 상속 받아서 만들어짐 -> 실제 클래스와 겉모양이 같음
  • 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 됨(이론상)
  • 프록시 객체는 실제 객체의 참조(target)를 보관
  • 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드 호출

    • ex. getName()을 호출하면 target에 있는 getName()을 호출
    • 처음에는 target 비어 있음 : DB에서 조회한 적이 없으니까
  • 프록시 객체는 처음 사용할 때 한 번만 초기화
    • 한 번 초기화하면 계속 사용
  • 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아님!!!
    • 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능한 것
    • 예제 : before findMember와 after findMember가 동일

        Member member = new Member();
        member.setUsername("hello");
              
        em.persist(member);
              
        em.flush();
        em.clear();
              
        Member findMember = em.getReference(Member.class, member.getId());
              
        System.out.println("before findMember = " + findMember.getClass());
        System.out.println("findMember.username = " + findMember.getUsername());
        System.out.println("after findMember = " + findMember.getClass());
              
        tx.commit();
      
        Hibernate: 
            /* insert hellojpa.Member
                */ insert 
                into
                    Member
                    (createdBy, createdDate, lastModifiedBy, lastModifiedDate, USERNAME, MEMBER_ID) 
                values
                    (?, ?, ?, ?, ?, ?)
        before findMember = class hellojpa.Member$HibernateProxy$L2EuOtQa
        Hibernate: 
            select
                member0_.MEMBER_ID as MEMBER_I1_3_0_,
                member0_.createdBy as createdB2_3_0_,
                member0_.createdDate as createdD3_3_0_,
                member0_.lastModifiedBy as lastModi4_3_0_,
                member0_.lastModifiedDate as lastModi5_3_0_,
                member0_.TEAM_ID as TEAM_ID7_3_0_,
                member0_.USERNAME as USERNAME6_3_0_,
                team1_.TEAM_ID as TEAM_ID1_7_1_,
                team1_.createdBy as createdB2_7_1_,
                team1_.createdDate as createdD3_7_1_,
                team1_.lastModifiedBy as lastModi4_7_1_,
                team1_.lastModifiedDate as lastModi5_7_1_,
                team1_.name as name6_7_1_ 
            from
                Member member0_ 
            left outer join
                Team team1_ 
                    on member0_.TEAM_ID=team1_.TEAM_ID 
            where
                member0_.MEMBER_ID=?
        findMember.username = hello
        after findMember = class hellojpa.Member$HibernateProxy$L2EuOtQa
      
  • 프록시 객체는 원본 엔티티를 상속 받음. 따라서 타입 체크시 == 가 아닌 instanceof 로 비교
    • 예제
      • find 결과를 비교

          Member member1 = new Member();
          member1.setUsername("member1");
          em.persist(member1);
                    
          Member member2 = new Member();
          member2.setUsername("member2");
          em.persist(member2);
                    
          em.flush();
          em.clear();
                    
          Member m1 = em.find(Member.class, member1.getId());
          Member m2 = em.find(Member.class, member2.getId());
                    
          System.out.println("m1 == m2 : " + (m1.getClass() == m2.getClass())); // true
                    
          tx.commit();
        
      • find 와 getReference 결과를 비교

        • 실제로 proxy가 넘어올지 실제 객체가 넘어올지 모르기 때문에 절대 ==으로 type 비교를 하면 안됨

            Member member1 = new Member();
            member1.setUsername("member1");
            em.persist(member1);
                          
            Member member2 = new Member();
            member2.setUsername("member2");
            em.persist(member2);
                          
            em.flush();
            em.clear();
                          
            Member m1 = em.find(Member.class, member1.getId());
            Member m2 = em.getReference(Member.class, member2.getId());
                          
            System.out.println("m1 == m2 : " + (m1.getClass() == m2.getClass())); // false (type 비교여서)
                          
            tx.commit();
          
        • instanceof로 비교

            Member member1 = new Member();
            member1.setUsername("member1");
            em.persist(member1);
                          
            Member member2 = new Member();
            member2.setUsername("member2");
            em.persist(member2);
                          
            em.flush();
            em.clear();
                          
            Member m1 = em.find(Member.class, member1.getId());
            Member m2 = em.getReference(Member.class, member2.getId());
                          
            logic(m1, m2);
            // m1 == m2 : true
            // m1 == m2 : true
          
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환
    • 예제

        Member member1 = new Member();
        member1.setUsername("member1");
        em.persist(member1);
              
        em.flush();
        em.clear();
              
        Member m1 = em.find(Member.class, member1.getId());
        System.out.println("m1 = " + m1.getClass()); // m1 = class hellojpa.Member
              
        // 현재 m1이 영속성 컨텍스트에 올라가 있는 상태
        Member reference = em.getReference(Member.class, member1.getId());
        System.out.println("reference = " + reference.getClass()); // reference = class hellojpa.Member
              
        tx.commit();
      
      • 이유
        1. JPA는 한 영속성 컨텍스트에서 가져온 것을 비교하면 항상 true를 반환을 보장
        2. 이미 영속성 컨텍스트에 올라와있는데 Proxy를 가져오는 것이 이점이 없음
  • 한 번 Proxy를 조회하면 em.find에서도 Proxy를 반환

      Member member1 = new Member();
      member1.setUsername("member1");
      em.persist(member1);
        
      em.flush();
      em.clear();
        
      Member refMember = em.getReference(Member.class, member1.getId());
      System.out.println("refMember = " + refMember.getClass()); // refMember = class hellojpa.Member$HibernateProxy$WmuYKyAh
        
      Member findMember = em.find(Member.class, member1.getId());
      System.out.println("findMember = " + findMember.getClass()); // findMember = class hellojpa.Member$HibernateProxy$WmuYKyAh
      // findMember 했는데 Proxy를 반환
      // 왜냐하면,, 한 번 Proxy를 조회하면 em.find에서 Proxy를 반환
        
      System.out.println("refMember == findMember : " + (refMember == findMember)); // refMember == findMember : true
        
      tx.commit();
    
  • ⭐️ 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생 (Hibernate는 org.hibernate.LazyInitializationException 예외)

      try {
        
      		Member member1 = new Member();
      	  member1.setUsername("member1");
      	  em.persist(member1);
        
      		em.flush();
      		em.clear();
        
      		Member refMember = em.getReference(Member.class, member1.getId());
      		System.out.println("refMember = " + refMember.getClass()); // Proxy
        
      		em.detach(refMember); // 영속성 컨텍스트에서 꺼내버림
      		// em.close(); // 이거도 마찬가지
      		// em.clear();
        
      		refMember.getUsername();
      		// 영속성 컨텍스트의 도움을 받아서 초기화해야하는데 영속성 컨텍스트에서 관리하지 않고 있음
      		// 따라서 could not initialize proxy
        
      		tx.commit();
        
      } catch (Exception e) {
        
      		tx.rollback();
      		e.printStackTrace();
        
      } finally {
        
      		em.close();
        
      }
    



프록시 확인


  • 프록시 인스턴스의 초기화 여부 확인 : emf.getPersistenceUnitUtil().isLoaded(Object entity)
    • 초기화 전

        Member member1 = new Member();
        member1.setUsername("member1");
        em.persist(member1);
              
        em.flush();
        em.clear();
              
        Member refMember = em.getReference(Member.class, member1.getId());
        System.out.println("refMember = " + refMember.getClass()); // Proxy
              
        System.out.println("isLoaded = " + emf.getPersistenceUnitUtil().isLoaded(refMember)); // false
              
        tx.commit();
      
    • 초기화 후

        Member member1 = new Member();
        member1.setUsername("member1");
        em.persist(member1);
              
        em.flush();
        em.clear();
              
        Member refMember = em.getReference(Member.class, member1.getId());
        System.out.println("refMember = " + refMember.getClass()); // Proxy
        refMember.getUsername(); // 초기화
        System.out.println("isLoaded = " + emf.getPersistenceUnitUtil().isLoaded(refMember)); // true
              
        tx.commit();
      
  • 프록시 클래스 확인 방법 : entity.getClass().getName() 출력

      Member member1 = new Member();
      member1.setUsername("member1");
      em.persist(member1);
        
      em.flush();
      em.clear();
        
      Member refMember = em.getReference(Member.class, member1.getId());
      System.out.println("refMember = " + refMember.getClass()); // refMember = class hellojpa.Member$HibernateProxy$bPWWO9Gu
        
      tx.commit();
    
  • 프록시 강제 초기화 : org.hibernate.Hibernate.initialize(entity);

      Member member1 = new Member();
      member1.setUsername("member1");
      em.persist(member1);
        
      em.flush();
      em.clear();
        
      Member refMember = em.getReference(Member.class, member1.getId());
      System.out.println("refMember = " + refMember.getClass());
        
      Hibernate.initialize(refMember);
        
      tx.commit();
    
      Hibernate: 
          call next value for hibernate_sequence
      Hibernate: 
          /* insert hellojpa.Member
              */ insert 
              into
                  Member
                  (createdBy, createdDate, lastModifiedBy, lastModifiedDate, USERNAME, MEMBER_ID) 
              values
                  (?, ?, ?, ?, ?, ?)
      refMember = class hellojpa.Member$HibernateProxy$X96OXpP5
      Hibernate: 
          select
              member0_.MEMBER_ID as MEMBER_I1_3_0_,
              member0_.createdBy as createdB2_3_0_,
              member0_.createdDate as createdD3_3_0_,
              member0_.lastModifiedBy as lastModi4_3_0_,
              member0_.lastModifiedDate as lastModi5_3_0_,
              member0_.TEAM_ID as TEAM_ID7_3_0_,
              member0_.USERNAME as USERNAME6_3_0_,
              team1_.TEAM_ID as TEAM_ID1_7_1_,
              team1_.createdBy as createdB2_7_1_,
              team1_.createdDate as createdD3_7_1_,
              team1_.lastModifiedBy as lastModi4_7_1_,
              team1_.lastModifiedDate as lastModi5_7_1_,
              team1_.name as name6_7_1_ 
          from
              Member member0_ 
          left outer join
              Team team1_ 
                  on member0_.TEAM_ID=team1_.TEAM_ID 
          where
              member0_.MEMBER_ID=?
    
  • 참고: JPA 표준은 강제 초기화 없음
    • 강제 호출 : member.getName()



02. 즉시 로딩과 지연 로딩


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


지연 로딩

  • 지연 로딩 : 프록시에서만 조회
  • 사용 방법 : fetch = FetchType.*LAZY*
@Entity
public class Member {

		@Id
		@GeneratedValue
		private Long id;

		@Column(name = "USERNAME")
		private String name;

		@ManyToOne(fetch = FetchType.LAZY) //**
		@JoinColumn(name = "TEAM_ID")
		private Team team;
		..
 }

JpaMain.java

// 생략

Member m = em.find(Member.class, member1.getId());

System.out.println("m = " + m.getTeam().getClass()); // m = class hellojpa.Team$HibernateProxy$vrnrtZ7q (Proxy)

System.out.println("===============");
m.getTeam().getName(); // 여기서 쿼리가 나감!!

tx.commit();
Hibernate: 
    call next value for hibernate_sequence
Hibernate: 
    call next value for hibernate_sequence
Hibernate: 
    /* insert hellojpa.Team
        */ insert 
        into
            Team
            (createdBy, createdDate, lastModifiedBy, lastModifiedDate, name, TEAM_ID) 
        values
            (?, ?, ?, ?, ?, ?)
Hibernate: 
    /* insert hellojpa.Member
        */ insert 
        into
            Member
            (createdBy, createdDate, lastModifiedBy, lastModifiedDate, TEAM_ID, USERNAME, MEMBER_ID) 
        values
            (?, ?, ?, ?, ?, ?, ?)
Hibernate: 
    select
        member0_.MEMBER_ID as MEMBER_I1_3_0_,
        member0_.createdBy as createdB2_3_0_,
        member0_.createdDate as createdD3_3_0_,
        member0_.lastModifiedBy as lastModi4_3_0_,
        member0_.lastModifiedDate as lastModi5_3_0_,
        member0_.TEAM_ID as TEAM_ID7_3_0_,
        member0_.USERNAME as USERNAME6_3_0_ 
    from
        Member member0_ 
    where
        member0_.MEMBER_ID=?
m = class hellojpa.Team$HibernateProxy$vrnrtZ7q
===============
Hibernate: 
    select
        team0_.TEAM_ID as TEAM_ID1_7_0_,
        team0_.createdBy as createdB2_7_0_,
        team0_.createdDate as createdD3_7_0_,
        team0_.lastModifiedBy as lastModi4_7_0_,
        team0_.lastModifiedDate as lastModi5_7_0_,
        team0_.name as name6_7_0_ 
    from
        Team team0_ 
    where
        team0_.TEAM_ID=?



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


Member member = em.find(Member.class, 1L);

Team team = member.getTeam();
team.getName(); // 실제 team을 사용하는 시점에 초기화(DB 조회)



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

: Member와 Team을 자주 함께 사용하는 경우


즉시 로딩


  • member1를 로딩할 때 team1까지 join해서 한 번에 가져옴
@Entity
public class Member {

		@Id
		@GeneratedValue
		private Long id;

		@Column(name = "USERNAME")
		private String name;

		@ManyToOne(fetch = FetchType.EAGER) //**
		@JoinColumn(name = "TEAM_ID")
		private Team team;
		..
 }

JpaMain.java

// 생략

Member m = em.find(Member.class, member1.getId());

System.out.println("m = " + m.getTeam().getClass()); // m = class hellojpa.Team

tx.commit();

실행 결과

  • 즉시 로딩이기 때문에 Proxy가 필요하지 않고 한 번에 다 땡겨옴
Hibernate: 
    /* insert hellojpa.Team
        */ insert 
        into
            Team
            (createdBy, createdDate, lastModifiedBy, lastModifiedDate, name, TEAM_ID) 
        values
            (?, ?, ?, ?, ?, ?)
Hibernate: 
    /* insert hellojpa.Member
        */ insert 
        into
            Member
            (createdBy, createdDate, lastModifiedBy, lastModifiedDate, TEAM_ID, USERNAME, MEMBER_ID) 
        values
            (?, ?, ?, ?, ?, ?, ?)
Hibernate: 
    select
        member0_.MEMBER_ID as MEMBER_I1_3_0_,
        member0_.createdBy as createdB2_3_0_,
        member0_.createdDate as createdD3_3_0_,
        member0_.lastModifiedBy as lastModi4_3_0_,
        member0_.lastModifiedDate as lastModi5_3_0_,
        member0_.TEAM_ID as TEAM_ID7_3_0_,
        member0_.USERNAME as USERNAME6_3_0_,
        team1_.TEAM_ID as TEAM_ID1_7_1_,
        team1_.createdBy as createdB2_7_1_,
        team1_.createdDate as createdD3_7_1_,
        team1_.lastModifiedBy as lastModi4_7_1_,
        team1_.lastModifiedDate as lastModi5_7_1_,
        team1_.name as name6_7_1_ 
    from
        Member member0_ 
    left outer join
        Team team1_ 
            on member0_.TEAM_ID=team1_.TEAM_ID 
    where
        member0_.MEMBER_ID=?
m = class hellojpa.Team
==============



프록시와 즉시로딩 주의


  • 가급적 지연 로딩만 사용(특히 실무에서)
    • 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생
  • 즉시 로딩은 JPQL에서 N+1 문제를 일으킴
    • N+1 문제 : 최초로 나간 쿼리(1) 때문에 추가 쿼리가 발생(N)

    ex.

      Team teamA = new Team();
      teamA.setName("teamA");
      em.persist(teamA);
        
      Team teamB = new Team();
      teamB.setName("teamB");
      em.persist(teamB);
        
      Member member1 = new Member();
      member1.setUsername("member1");
      member1.setTeam(teamA);
      em.persist(member1);
        
      Member member2 = new Member();
      member2.setUsername("member2");
      member2.setTeam(teamB);
      em.persist(member2);
        
      em.flush();
      em.clear();
        
      List<Member> members = em.createQuery("select m from Member m", Member.class).getResultList();
        
      tx.commit();
    
      Hibernate: 
          /* select
              m 
          from
              Member m */ select
                  member0_.MEMBER_ID as MEMBER_I1_3_,
                  member0_.createdBy as createdB2_3_,
                  member0_.createdDate as createdD3_3_,
                  member0_.lastModifiedBy as lastModi4_3_,
                  member0_.lastModifiedDate as lastModi5_3_,
                  member0_.TEAM_ID as TEAM_ID7_3_,
                  member0_.USERNAME as USERNAME6_3_ 
              from
                  Member member0_
      Hibernate: 
          select
              team0_.TEAM_ID as TEAM_ID1_7_0_,
              team0_.createdBy as createdB2_7_0_,
              team0_.createdDate as createdD3_7_0_,
              team0_.lastModifiedBy as lastModi4_7_0_,
              team0_.lastModifiedDate as lastModi5_7_0_,
              team0_.name as name6_7_0_ 
          from
              Team team0_ 
          where
              team0_.TEAM_ID=?
      Hibernate: 
          select
              team0_.TEAM_ID as TEAM_ID1_7_0_,
              team0_.createdBy as createdB2_7_0_,
              team0_.createdDate as createdD3_7_0_,
              team0_.lastModifiedBy as lastModi4_7_0_,
              team0_.lastModifiedDate as lastModi5_7_0_,
              team0_.name as name6_7_0_ 
          from
              Team team0_ 
          where
              team0_.TEAM_ID=?
    
    • Member를 조회하였는데 즉시 로딩 때문에 Team도 바로 조회
    • 지연로딩하면 프록시에서만 가져올 것을.. (Team은 프록시로 박혀있음)

        // @ManyToOne(fetch = FetchType.LAZY)
        Hibernate: 
            /* select
                m 
            from
                Member m */ select
                    member0_.MEMBER_ID as MEMBER_I1_3_,
                    member0_.createdBy as createdB2_3_,
                    member0_.createdDate as createdD3_3_,
                    member0_.lastModifiedBy as lastModi4_3_,
                    member0_.lastModifiedDate as lastModi5_3_,
                    member0_.TEAM_ID as TEAM_ID7_3_,
                    member0_.USERNAME as USERNAME6_3_ 
                from
                    Member member0_
      
    • 지연로딩 해결 방법
      • 일단 모든 연관 관계를 지연로딩으로 깔고
      • 방법 1. JPQL fetch join : 동적으로 원하는 Entity만 선택해서 가져옴

          Team teamA = new Team();
          teamA.setName("teamA");
          em.persist(teamA);
                    
          Team teamB = new Team();
          teamB.setName("teamB");
          em.persist(teamB);
                    
          Member member1 = new Member();
          member1.setUsername("member1");
          member1.setTeam(teamA);
          em.persist(member1);
                    
          Member member2 = new Member();
          member2.setUsername("member2");
          member2.setTeam(teamB);
          em.persist(member2);
                    
          em.flush();
          em.clear();
                    
          List<Member> members = em.createQuery("select m from Member m join fetch m.team", Member.class).getResultList();
                    
          tx.commit();
        
          // 지연로딩이지만 Member와 Team을 join해서 한 번에 같이 가져옴
          Hibernate: 
              /* select
                  m 
              from
                  Member m 
              join
                  fetch m.team */ select
                      member0_.MEMBER_ID as MEMBER_I1_3_0_,
                      team1_.TEAM_ID as TEAM_ID1_7_1_,
                      member0_.createdBy as createdB2_3_0_,
                      member0_.createdDate as createdD3_3_0_,
                      member0_.lastModifiedBy as lastModi4_3_0_,
                      member0_.lastModifiedDate as lastModi5_3_0_,
                      member0_.TEAM_ID as TEAM_ID7_3_0_,
                      member0_.USERNAME as USERNAME6_3_0_,
                      team1_.createdBy as createdB2_7_1_,
                      team1_.createdDate as createdD3_7_1_,
                      team1_.lastModifiedBy as lastModi4_7_1_,
                      team1_.lastModifiedDate as lastModi5_7_1_,
                      team1_.name as name6_7_1_ 
                  from
                      Member member0_ 
                  inner join
                      Team team1_ 
                          on member0_.TEAM_ID=team1_.TEAM_ID
        
      • 방법 2. @EntityGraph annotation
      • 방법 3. Batch Size : 쿼리가 N+1이 아니라 1+1로 나감
  • @ManyToOne, @OneToOne은 기본이 즉시 로딩 : LAZY로 설정
    • @OneToMany, @ManyToMany는 기본이 지연 로딩



03. 영속성 전이 : CASCADE


영속성 전이: CASCADE


  • (연관관계, 즉시 로딩, 지연 로딩과 관계 없음)
  • 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때

    ex. 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장

예제.


@Entity
public class Parent {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @OneToMany(mappedBy = "parent")
    private List<Child> childList = new ArrayList<>();

    public void addChild(Child child) {
        childList.add(child);
        child.setParent(this);
    }

    // getter, setter (생략)

}

@Entity
public class Child {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @ManyToOne
    @JoinColumn(name = "parent_id")
    private Parent parent;

    // getter, setter (생략)

}
Child child1 = new Child();
Child child2 = new Child();

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

// persist를 3번 해줘야 함. 그런데 좀 귀찮은데 ..
em.persist(parent);
em.persist(child1);
em.persist(child2);

em.flush();
em.clear();

tx.commit();
Hibernate: 
    /* insert hellojpa.Parent
        */ insert 
        into
            Parent
            (name, id) 
        values
            (?, ?)
Hibernate: 
    /* insert hellojpa.Child
        */ insert 
        into
            Child
            (name, parent_id, id) 
        values
            (?, ?, ?)
Hibernate: 
    /* insert hellojpa.Child
        */ insert 
        into
            Child
            (name, parent_id, id) 
        values
            (?, ?, ?)

이때, parent를 persist할 때 child도 persist 되게 하려면 : CASCADING

  • 사용방법

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

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

em.persist(parent);

em.flush();
em.clear();

tx.commit();
// parent만 persist 했는데, child1, child2도 persist됨
Hibernate: 
    /* insert hellojpa.Parent
        */ insert 
        into
            Parent
            (name, id) 
        values
            (?, ?)
Hibernate: 
    /* insert hellojpa.Child
        */ insert 
        into
            Child
            (name, parent_id, id) 
        values
            (?, ?, ?)
Hibernate: 
    /* insert hellojpa.Child
        */ insert 
        into
            Child
            (name, parent_id, id) 
        values
            (?, ?, ?)



🚨 주의

  • 영속성 전이는 연관관계를 매핑하는 것과 아무 관련 X
  • 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함을 제공할 뿐
  • 단일 소유자일 때만(정말 부모가 하나일 때) 사용 ex. parent만 child를 관리. 게시판의 첨부파일 경로 등



CASCADE의 종류


| 종류 | 상세 | | — | — | | ALL | 모두 적용 | | PERSIST | 영속 | | REMOVE | 삭제 | | MERGE | 병합 | | REFREPSH | Refresh | | DETACH | Detach |

  • 보통 이 중 ALL, PERSIST 사용



04. 고아 객체


고아 객체


  • 고아 객체 제거: 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제
  • orphanRemoval = true
Parent parent1 = em.find(Parent.class, id);
parent1.getChildren().remove(0); //자식 엔티티를 컬렉션에서 제거

// Delete 쿼리가 나감
DELETE FROM CHILD WHERE ID=?

예제


@Entity
public class Parent {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Child> childList = new ArrayList<>();
		// parent가 지워지면 그 하위 childList에 있는 child도 지워진다

    public void addChild(Child child) {
        childList.add(child);
        child.setParent(this);
    }

    // getter, setter (생략)

}
// JpaMain.java

Child child1 = new Child();
Child child2 = new Child();

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

em.persist(parent);

em.flush();
em.clear();

Parent findParent = em.find(Parent.class, parent.getId());
findParent.getChildList().remove(0); // 부모 제거 -> orphanRemoval이 동작

tx.commit();

Hibernate: 
    /* insert hellojpa.Parent
        */ insert 
        into
            Parent
            (name, id) 
        values
            (?, ?)
Hibernate: 
    /* insert hellojpa.Child
        */ insert 
        into
            Child
            (name, parent_id, id) 
        values
            (?, ?, ?)
Hibernate: 
    /* insert hellojpa.Child
        */ insert 
        into
            Child
            (name, parent_id, id) 
        values
            (?, ?, ?)
Hibernate: 
    select
        parent0_.id as id1_7_0_,
        parent0_.name as name2_7_0_ 
    from
        Parent parent0_ 
    where
        parent0_.id=?
Hibernate: 
    select
        childlist0_.parent_id as parent_i3_2_0_,
        childlist0_.id as id1_2_0_,
        childlist0_.id as id1_2_1_,
        childlist0_.name as name2_2_1_,
        childlist0_.parent_id as parent_i3_2_1_ 
    from
        Child childlist0_ 
    where
        childlist0_.parent_id=?
Hibernate: 
    /* delete hellojpa.Child */ delete 
        from
            Child 
        where
            id=?


🚨 주의

  • 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능
  • 참조하는 곳이 하나일 때 사용해야함!
  • ⭐️ 특정 엔티티가 개인 소유할 때 사용
  • @OneToOne, @OneToMany만 가능
  • 참고 : 개념적으로 부모를 제거하면 자식은 고아가 된다. 따라서 고아 객체 제거 기능을 활성화 하면, 부모를 제거할 때 자식도 함께 제거된다. 이것은 CascadeType.REMOVE처럼 동작한다.


CascadeType 지정 안하면 child도 각각 persist 해줘야 ,, (안하면 parent만 delete 됨)


@Entity
public class Parent {

    @Id
    @GeneratedValue
    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);
    }

    // getter, setter (생략)

}
// JpaMain.java

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);

tx.commit();
Hibernate: 
    /* insert hellojpa.Parent
        */ insert 
        into
            Parent
            (name, id) 
        values
            (?, ?)
Hibernate: 
    /* insert hellojpa.Child
        */ insert 
        into
            Child
            (name, parent_id, id) 
        values
            (?, ?, ?)
Hibernate: 
    /* insert hellojpa.Child
        */ insert 
        into
            Child
            (name, parent_id, id) 
        values
            (?, ?, ?)
Hibernate: 
    select
        parent0_.id as id1_7_0_,
        parent0_.name as name2_7_0_ 
    from
        Parent parent0_ 
    where
        parent0_.id=?
Hibernate: 
    select
        childlist0_.parent_id as parent_i3_2_0_,
        childlist0_.id as id1_2_0_,
        childlist0_.id as id1_2_1_,
        childlist0_.name as name2_2_1_,
        childlist0_.parent_id as parent_i3_2_1_ 
    from
        Child childlist0_ 
    where
        childlist0_.parent_id=?
Hibernate: 
    /* delete hellojpa.Child */ delete 
        from
            Child 
        where
            id=?
Hibernate: 
    /* delete hellojpa.Child */ delete 
        from
            Child 
        where
            id=?
Hibernate: 
    /* delete hellojpa.Parent */ delete 
        from
            Parent 
        where
            id=?



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


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


  • CascadeType.ALL + orphanRemoval=true
  • 스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화, em.remove()로 제거
  • 두 옵션을 모두 활성화 하면 부모 엔티티를 통해서 자식의 생명 주기를 관리할 수 있음
  • 도메인 주도 설계(DDD)의 Aggregate Root개념을 구현할 때 유용

Leave a comment