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

최진민·2021년 6월 6일
0

JPA

목록 보기
8/11
post-thumbnail

프록시

  • 예를 들어, Member를 조회할 때 Team도 함께 조회해야 할까?

  • 메인

    public class JpaMain {
        public static void main(String[] args) {
            EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
            EntityManager em = emf.createEntityManager();
            EntityTransaction tx = em.getTransaction();
            tx.begin();
    
            try {
    
                Member member = em.find(Member.class, 1L);
                printMember(member);
                //printMemberAndTeam(member);
    
                tx.commit();
            } catch (Exception e){
                tx.rollback();
            } finally {
                em.close();
            }
    
            emf.close();
        }
    
        private static void printMember(Member member) {
            System.out.println("member = " + member.getUsername());
        }
    
        private static void printMemberAndTeam(Member member) {
            String username = member.getUsername();
            System.out.println("username = " + username);
    
            Team team = member.getTeam();
            System.out.println("team = " + team.getName());
        }
    }
    • Member member = em.find(Member.class, 1L);에서 둘 다 가져오지만 경우에 따라 멤버 정보만 가져오고 싶거나 멤버와 팀을 같이 가져오고 싶다.
  • 프록시 기초

    • em.find() vs em.getReference()

    • em.find() : DB를 통해 실제 엔티티 객체 조회

      try {
      
              Member member = new Member();
              member.setUsername("hello");
      
              em.persist(member);
      
              em.flush();
              em.clear();
              
              Member findMember = em.find(Member.class, member.getId());
              //System.out.println("findMember.id = " + findMember.getId());
              //System.out.println("findMember.username = " + findMember.getUsername());
      
              tx.commit();
          }
      • 아래와 같이 INSERT 쿼리 생성 후 SELECT 쿼리 생성

    • em.getReference() : DB 조회를 미루는 가짜(프록시) 객체 조회 (=DB에 쿼리가 날라가지 않고 조회가 된다.)

      try {
      
              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());
              //System.out.println("findMember.username = " + findMember.getUsername());
      
              tx.commit();
          }
      • 아래와 같이 INSERT 쿼리 생성 후 종료

      • 만약 주석 처리 된 sout()을 주석 해제하고 실행하면?

        • id.getReference()의 파라미터로 쓰였기 때문에 DB에 접근하지 않고 값을 출력했지만, username은 DB에 SELECT 쿼리 실행
      • findMember의 정체

        try {
        
                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());
                //System.out.println("findMember.id = " + findMember.getId());
                //System.out.println("findMember.username = " + findMember.getUsername());
        
                tx.commit();
            }
        print:
        findMember = class hellojpa.Member$HibernateProxy$yBRBFOPb
        • Member가 아닌 하이버네이트가 만든 💖프록시 객체
  • 프록시 특징 1)

    • 실제 클래스를 상속 받아서 만들어진다. (=실제 클래스와 겉모습이 같다.)

    • 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다(이론상).

    • 프록시 객체는 실제 객체의 참조(target)를 보관

    • 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드 호출(위임)

  • 💋프록시 특징 2)

    • 프록시 객체는 처음 사용할 때 한 번만 초기화

      Member member = em.getReference(Member.class, "pk");
      member.getName(); //초기화
      • 💋초기화 과정

        • .getName()(프록시 내 메소드)을 두 번째 이후로 호출할 때는 2~4 과정생략
    • 🧨프록시 객체 초기화 시, 프록시 객체가 실제 엔티티로 바뀌는 것이 아니며, 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능할 뿐이다. (target에만 값이 채워질 뿐!!)

    • 🧨프록시 객체는 원본 엔티티를 상속받는다. 따라서 타입 체크시 주의(== 비교 X, instead of 사용)

    • 🧨영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환

      try {
      
              Member member = new Member();
              member.setUsername("hello");
      
              em.persist(member);
      
              em.flush();
              em.clear();
      
              Member m1 = em.find(Member.class, member.getId());
              System.out.println("m1 = " + m1.getClass());
      
              /**
               * JPA는 영속성 컨텍스트에 엔티티가 존재할 때,
               * 1) 프록시를 반환해서 얻을 수 있는 성능 이점도 없고
               * 2) == 비교할 때 true를 리턴해주기 위해 영속성 컨텍스트에 존재한 엔티티와 프록시 객체를 동일시한다.
               * 바꾸어 말하면, 위의 em.find()와 아래의 em.getReference()의 위치를 바꾸면
               * 미리 호출된 프록시 객체로 인해 em.find()도 프록시 객체가 호출된다. (==의 결과값은 true)
               */
              Member reference = em.getReference(Member.class, member.getId());
              System.out.println("reference = " + reference.getClass());
      
              System.out.println("m1 == reference : " + (m1 == reference));
      
              tx.commit();
      print:
      m1 = class hellojpa.Member
      reference = class hellojpa.Member
      m1 == reference : true
    • 💥준영속 상태일 때, 프록시를 초기화하면 에러 발생 (org.hibernate.LazyInitializationException 예외)

      try {
      
                  Member member = new Member();
                  member.setUsername("hello");
                  em.persist(member);
      
                  em.flush();
                  em.clear();
      
                  Member refMember = em.getReference(Member.class, member.getId());
                  System.out.println("reference = " + refMember.getClass());
      
      						//영속성 컨텍스트 관리를 안하겠다! = 준영속상태
                  em.detach(refMember); 
      
                  System.out.println("refMember = " + refMember.getUsername());
      
                  tx.commit();
              } catch (Exception e) {
                  tx.rollback();
                  **System.out.println("e = " + e);**
              }
      print:
      reference = class hellojpa.Member$HibernateProxy$AnC0DuF8
      e = org.hibernate.LazyInitializationException: could not initialize proxy [hellojpa.Member#1] - no Session
  • 프록시 유틸리티 메소드(프록시 확인법)

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

      try {
      
                  Member member = new Member();
                  member.setUsername("hello");
                  em.persist(member);
      
                  em.flush();
                  em.clear();
      
                  Member refMember = em.getReference(Member.class, member.getId());
                  System.out.println("reference = " + refMember.getClass());
      
                  //초기화를 했기 때문에 결과는 true
                  refMember.getUsername();
                  System.out.println("isLoaded = " + emf.getPersistenceUnitUtil().isLoaded(refMember));
      
                  tx.commit();
              }
      • 현재 결과는 true! refMember.getUsername()을 주석 처리해 초기화를 하지 않으면 결과는 false
    • 프록시 클래스 확인 방법 : entity.getClass().getName()

    • 프록시 강제 초기화 : org.hibernate.Hibernate.initialize(entity)

      try {
      	...
      	Hibernate.initialize(refMember);
      	...
      }
      • 참고 : JPA 표준은 강제 초기화가 없다. 위는 하이버네이트 (강제 호출 : entity.getMethod())

즉시 로딩과 지연 로딩

  • Member를 조회할 때, Team도 꼭 같이 조회해야하나? (같이:즉시로딩, 멤버만:지연로딩)

    • 지연 로딩 LAZY를 이용해 프록시로 조회

      @Entity
      public class Member extends BaseEntity {
      		...
          @ManyToOne(fetch = FetchType.LAZY)
          @JoinColumn(name = "team_id")
          private Team team;
      		...
      }
      try {
      
              Team team = new Team();
              team.setName("teamA");
              em.persist(team);
      
              Member member = new Member();
              member.setUsername("hello");
              member.setTeam(team);
              em.persist(member);
      
              em.flush();
              em.clear();
      
              Member m = em.find(Member.class, member.getId());
              System.out.println("reference = " + m.getTeam().getClass());
      
              /**
               * 프록시 객체를 호출 및 초기화 했을때,,
               */
              System.out.println("=======구분=======");
              m.getTeam().getName();
              System.out.println("=======구분=======");
      
             tx.commit();
          }
      print:
      Hibernate: 
          select
              member0_.member_id as member_i1_1_0_,
              member0_.createdBy as createdB2_1_0_,
              member0_.createdDate as createdD3_1_0_,
              member0_.lastModifiedBy as lastModi4_1_0_,
              member0_.lastModifiedDate as lastModi5_1_0_,
              member0_.team_id as team_id7_1_0_,
              member0_.username as username6_1_0_ 
          from
              Member member0_ 
          where
              member0_.member_id=?
      reference = class hellojpa.Team$HibernateProxy$EQocszM6
      =======구분=======
      Hibernate: 
          select
              team0_.team_id as team_id1_4_0_,
              team0_.createdBy as createdB2_4_0_,
              team0_.createdDate as createdD3_4_0_,
              team0_.lastModifiedBy as lastModi4_4_0_,
              team0_.lastModifiedDate as lastModi5_4_0_,
              team0_.name as name6_4_0_ 
          from
              Team team0_ 
          where
              team0_.team_id=?
      =======구분=======
      • Member에 대해서만 SELECT 쿼리 생성
      • em.find()지만 m.getTeam()은 프록시 객체
      • 구분선 이후 m을 초기화했을 때 Team에 대해 SELECT 쿼리 생성
  • 지연 로딩

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

      Member member = em.find(Member.class, pk);

      Team team = member.getTeam();
      team.getName(); // 실제 team을 사용하는 시점에 초기화(DB 조회(쿼리 생성))
  • Member와 Team을 자주 함께 사용한다면? ⇒ ❗즉시 로딩

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

      @Entity
      public class Member extends BaseEntity {
      		...
          @ManyToOne(fetch = FetchType.EAGER)
          @JoinColumn(name = "team_id")
          private Team team;
      		...
      }
      • 메인을 그대로 실행했을 때 결과

        print:
        Hibernate: 
            select
                member0_.member_id as member_i1_1_0_,
                member0_.createdBy as createdB2_1_0_,
                member0_.createdDate as createdD3_1_0_,
                member0_.lastModifiedBy as lastModi4_1_0_,
                member0_.lastModifiedDate as lastModi5_1_0_,
                member0_.team_id as team_id7_1_0_,
                member0_.username as username6_1_0_,
                team1_.team_id as team_id1_4_1_,
                team1_.createdBy as createdB2_4_1_,
                team1_.createdDate as createdD3_4_1_,
                team1_.lastModifiedBy as lastModi4_4_1_,
                team1_.lastModifiedDate as lastModi5_4_1_,
                team1_.name as name6_4_1_ 
            from
                Member member0_ 
            left outer join
                Team team1_ 
                    on member0_.team_id=team1_.team_id 
            where
                member0_.member_id=?
        reference = class hellojpa.Team
        =======구분=======
        =======구분=======
      • JOIN을 이용한 SELECT 쿼리가 생성되어 DB에 Member와 Team에 대해 접근

      • 프록시 객체가 아닌 진짜 객체가 나왔기 때문에 구분선의 초기화 값이 나오지 않는다.

  • 즉시 로딩

    • Member 조회 시, Team도 항상 조회

      • JPA 구현체는 가능하면 조인을 사용해서 SQL 한번에 함께 조회
  • 💋프록시와 즉시 로딩 주의점

    • 🧨가급적 지연 로딩만 사용(특히 실무에서)

    • 즉시 로딩을 적용하면 예상하지 못한 SQL 발생

    • 즉시 로딩은 JPQL에서 N+1 문제 야기

      List<Member> members = em.createQuery("select m from Member m", Member.class).getResultList();
      Hibernate: 
          /* select
              m 
          from
              Member m */ select
                  member0_.member_id as member_i1_1_,
                  member0_.createdBy as createdB2_1_,
                  member0_.createdDate as createdD3_1_,
                  member0_.lastModifiedBy as lastModi4_1_,
                  member0_.lastModifiedDate as lastModi5_1_,
                  member0_.team_id as team_id7_1_,
                  member0_.username as username6_1_ 
              from
                  Member member0_
      Hibernate: 
          select
              team0_.team_id as team_id1_4_0_,
              team0_.createdBy as createdB2_4_0_,
              team0_.createdDate as createdD3_4_0_,
              team0_.lastModifiedBy as lastModi4_4_0_,
              team0_.lastModifiedDate as lastModi5_4_0_,
              team0_.name as name6_4_0_ 
          from
              Team team0_ 
          where
              team0_.team_id=?
      • JPQL로 Member를 조회할 때, Team도 가져온다.
    • @XXXToOne은 기본이 즉시 로딩이기 때문에 ⇒ LAZY(지연 로딩)로 설정 변경

    • @XXXToMany는 기본이 지연 로딩

  • 지연 로딩 활용 - 실무

    • 🧨모든 연관관계에 지연 로딩을 사용하자
    • 💥실무에서 즉시 로딩 X
    • JPQL fetch 조인이나, Entity 그래프를 사용하자

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

  • 지연, 즉시 로딩과 전혀 무관

  • 영속성 전이(CASCADE) : 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때!

    • ex) 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장
  • 영속성 전이 : 저장

    • Parent

      @Entity
      public class Parent {
      
          @Id
          @GeneratedValue
          @Column(name = "parent_id")
          private Long id;
      
          private String name;
      
          @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
          private List<Child> children = new ArrayList<>();
      
      		//편이 메소드
          public void addChild(Child child){
              this.children.add(child);
              child.setParent(this);
          }
      		//getter, setter
      }
    • Child

      @Entity
      public class Child {
      
          @Id
          @GeneratedValue
          @Column(name = "child_id")
          private Long id;
      
          private String name;
      
          @ManyToOne(fetch = FetchType.LAZY)
          @JoinColumn(name = "parent_id")
          private Parent parent;
      		//getter, setter
      }
    • Main

      try {
      
                  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);
      
                  tx.commit();
              }
      • 출력

        print:
        Hibernate: 
            /* insert hellojpa.Parent
                */ insert 
                into
                    Parent
                    (name, parent_id) 
                values
                    (?, ?)
        Hibernate: 
            /* insert hellojpa.Child
                */ insert 
                into
                    Child
                    (name, parent_id, child_id) 
                values
                    (?, ?, ?)
        Hibernate: 
            /* insert hellojpa.Child
                */ insert 
                into
                    Child
                    (name, parent_id, child_id) 
                values
                    (?, ?, ?)
        • em.persist(parent) ⇒ 연관관계의 child 모두 영속성 컨텍스트에 넣는다.
  • CASCADE 주의

    • 영속성 전이는 연관관계를 매핑하는 것과 무관
    • 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함을 제공할 뿐
    • 한 파일이 여러 엔티티에서 관리될 경우에는 쓰지 말자(⇒@OneToMany에서 mappedBy에 해당하는 컬럼이 One에 해당하는 클래스와 유일하게 연관관계를 가질 경우에만 쓰자)
  • CASCADE 속성 종류

    • ALL : 모두 적용
    • PERSIST : 영속
    • REMOVE : 삭제
    • (MERGE) : 병합
    • (REFRESH)
    • (DETACH)
  • 고아 객체

    • 고아 객체 제거 : 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동 삭제

    • orphanRemoval = true

      @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
      private List<Child> childs = new ArrayList<>();
      try {
      
              Child child1 = new Child();
              Child child2 = new Child();
      
              Parent parent = new Parent();
              parent.addChild(child1);
              parent.addChild(child2);
      
              /**
               * 영속성 전이(CASCADE)로 인해 parent만 영속성 컨텍스트에 저장해도
               * parent에 연관관계를 맺은 child1, child2은 자동적으로 영속성 컨텍스트에 저장된다.
               */
              em.persist(parent);
              //em.persist(child1);
              //em.persist(child2);
      
              //remove(index)를 통해 index에 해당하는 데이터를 삭제
              Parent parent1 = em.find(Parent.class, parent.getId());
              parent1.getChilds().remove(0);
      
              tx.commit();
          }
      • DB 결과

        • Child를 2개 넣었지만 remove로 인해 한 개만 조회됐다.
    • DELETE FROM CHILD WHERE ID=?

  • 고아객체 주의

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

    • CascadeType.ALL + orphanRemoval = true

      @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
      private List<Child> childs = new ArrayList<>();
      • 메인에서 Parent에 대해서 영속화하면 연관관계를 맺은 Child도 영속화하고, Parent를 지우면 Child에 대한 영속성 컨텍스트도 전부 지워진다.
    • 스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화 / em.remove()로 제거

    • But, 두 옵션을 모두 활성화하면 부모 엔티티를 통해서 자식의 생명 주기도 관리 가능

    • DDD(도메인 주도 설계)의 Aggregate Root 개념을 구현할 때 유용


예제 5) 연관관계 관리

  • 글로벌 페치 전략
    • 모든 연관관계를 지연 로딩
    • @ManyToOne,
      @OneToOne은 디폴트가 즉시 로딩이므로 지연 로딩으로 변경
  • 영속성 전이(CASCADE) 설정
    • OrderDelivery 영속성 전이 ALL : 주문을 하면 배송도 같이 하게끔 생명주기를 관리
    • OrderOrderItem 영속성 전이 ALL
profile
열심히 해보자9999

0개의 댓글