8. 프록시와 연관관계 관리


프록시란?

  • JPA의 지연 로딩 기능을 사용하려면 실제 엔티티 객체 대신에 데이터베이스 조회를 지연할 수 있는 가짜 객체가 필요한데, 이것을 프록시 객체라고 함
  • JPA의 지연 로딩 : 엔티티가 실제 사용될 때까지 데이터베이스 조회를 지연시키는 방법(실제 데이터를 사용할 때 조회)
  • JPA의 표준 명세는 지연 로딩의 구현 방법을 JPA 구현체에 위임함
    • 하이버네이트 기준으로 아래 내용 작성
    • 하이버네이트가 지연 로딩을 지원하는 방법
      • 프록시
      • 바이트코드 수정 : 복잡해서 다루지 않음



1. 프록시 기초

  • EntityManager.find()
    • JPA에서 데이터베이스로부터 엔티티를 조회할 때 사용
    • 영속성 컨텍스트에 엔티티가 없으면 데이터베이스를 조회하고 엔티티(객체) 반환
    • 조회한 엔티티를 실제 사용 여부와 상관 없이 일단 데이터베이스 조회
  • EntityManager.getReference()
    • 엔티티를 실제 사용하는 시점까지 조회를 미루고 싶을 때 사용
    • JPA는 해당 메소드 호출 시 바로 데이터베이스르 조회하지 않고 실제 엔티티 객체도 생성하지 않음
    • 데이터베이스 접근을 위임한 프록시 객체를 반환
    •     Member member = em.getReference(Member.class, "member1");
      
  • 1

  • 프록시의 특징
    • 실제 클래스를 상속 받아서 만들어지므로 실제 클래스와 겉 모양이 같음
    • 따라서 사용하는 입장애서 프록시인지, 객체인지 구분할 필요 없음
    • 2
    • 아래 그림과 같이 프록시는 실제 객체에 대한 참조(target)을 보관함
      • 프록시 객체의 메소드를 호출하면 프록시 객체가 실제 객체의 메소드를 호출
    • 3
    • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 데이터베이스를 조회할 필요가 없음
      • 따라서 프록시가 아닌 실제 엔티티 객체가 반환됨
    • 프록시 초기화는 영속성 컨텍스트를 통해서 진행됨
      • 따라서 준영속 상태의 프록시 초기화는 에러 발생함 (아래 코드 참조)
      •   Member member = em.getReference(Member.class, "member1");
          tx.commit();
          em.close();
        
          member.getName(); // 준영속 상태에서 호출
        
  • 프록시 객체의 초기화
    • 실제 데이터를 필요로 하는 메소드 호출 시 데이터베이스를 조회해서 실제 엔티티 객체를 생성함 (프록시 객체의 초기화라고 함)
      • 4
      •   // 프록시 클래스의 예상 코드
          class MemberProxy extends Member {
              Member target = null;
        
              public String getName() {
                  if(target == null){
                      // 2. 초기화 요청
                      // 3. DB 조회
                      // 4. 실제 엔티티 생성 및 참조 보관
                      this.target = ...;
                  }
                  return target.getName();
              }
          }
        
  • 프록시와 식별자
    • 엔티티를 프록시로 조회할 때 식별자(PK) 값을 파라미터로 전달, 프록시는 이 식별자를 보관함
    • 식별자를 가지고 있는 상태에서 getId() 메소드를 호출하면?
      • @Access(AccessType.PROPERTY)
        • 이미 id 필드가 채워져 있으므로 프록시 초기화(DQ 쿼리) 하지 않음
      • @Access(AccessType.FIELD)
        • 이미 id 필드가 채워져 있지만 프록시를 초기화 함
    •     Member member = em.find(Member.class, "member1");
          Team team = em.getReference(Team.class, "team1"); // SQL 실행하지 않음
          member.setTeam(team);
      
    • 연관관계 설정은 식별자 값만 사용하므로 위에 코드처럼 프록시를 사용하면 데이터베이스 접근 횟수를 줄일 수 있음
      • 이 상황에서는 엔티티 접근 방식이 필드로 설정해도 프록시 초기화 하지 않음
  • 프록시 확인
    • 프록시 인스턴스가 초기화 됐는지 확인하는 방법?
      •     PersistenceUnitUtil.isLoaded(Object entity);
        
    • 조회한 엔티티가 프록시로 조회한 것인지 진짜 엔티티로 조회한 것인지 확인 방법?
      • 클래스 명을 출력해봤을 때 이름 뒷부분에 “javassist”가 있으면 프록시



2. 즉시 로딩과 지연 로딩

  • 즉시 로딩 : 엔티티 조회 시 연관 엔티티도 함께 조회
    • @ManyToOne(fetch = FetchType.EAGER)
  • 지연 로딩 : 연관된 엔티티를 실제 사용할 때 조회
    • @ManyToOne(fetch = FetchType.LAZY)


  • 즉시 로딩
    • @ManyToOne(fetch = FetchType.EAGER)
    • 5
    • em.find(Member.class, “member1”) 호출과 동시에 팀도 함께 조회를 함
    • 회원과 팀 두 테이블을 조회해야 하므로 쿼리를 2번 실행하나?
      • 대부분의 JPA 구현체가 조인 쿼리를 이용해서 한번에 처리
    • JPA는 그럼 조인 전략을 어떤 전략을 사용하냐?
      • LEFT OUTER JOIN을 상요함
      • 왜 INNER JOIN을 안할까?
        • 외래키가 null인 경우 INNER JOIN을 하면 아무 데이터도 조회가 안되는 참사가..
        • 그래서 JPA는 기본적으로 OUTER JOIN을 사용함
        • 만약 외래키에 nullable = false 설정을 해주면 JPA는 INNER JOIN을 사용함
        •     @JoinColumn(name = "TEAM_ID", nullable = false)
              private Team team;
          
  • 지연 로딩
    • @ManyToOne(fetch = FetchType.LAZY)
    • 6
    • em.find(Member.class, “member1”) 호출 시 회원만 조회
      • 팀은 조회하지 않고 프록시 객체만 넣어둠
    •     Member member = em.find(Member.class, "member1")
          Team team = em.getTeam(); // 프록시 객체 사용
          team.getName(); // 팀 객체 실제 사용 - SQL 쿼리
      


  • 프록시와 즉시로딩 정리
    • 지연 로딩(LAZY)
      • 연관된 엔티티를 프록시로 조회
      • 프록시를 실제 사용할 때 초기화하면서 데이터베이스 조회
    • 즉시 로딩(EAGER)
      • 연관된 엔티티 즉시 조회
      • 하이버네이트트 가능하면 SQL 조인으로 한 번에 조회함
  • 프록시와 즉시로딩 주의
    • 즉시 로딩 갖다 버리고 지연 로딩만 써라..
      • JPQL의 fetch 조인이나 엔티티 그래프 기능을 사용해야 함
    • @ManyToOne, @OneToOne은 기본이 즉시 로딩을 사용하도록 되어있음
    • 즉시 로딩은 상상하지 못한 쿼리가 나감
    • 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다
      • 이게 무슨소리냐면.. JPA는 조인으로 한 쿼리로 연관관계를 가져오지만 JPQL은 SQL과 같기 때문에 직접 조인을 해주지 않는 이상 그러지 못함
      •   List<Memebr> members = em.createQuery("select m from Member m", Member.class)
              .getResultList();
          // SQL1 : select * from Member
          // SQL2 : select * from Team where Member.TEAM_ID = TEAM_ID
        



3. 영속성 전이 : CASCADE

  • 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티는 어떻게 되나?
    • 영속성 전이(transitive persistence) 기능을 사용하면 연관된 엔티티도 영속 상태로 만들 수 있음
  • 연관관계 매핑과는 전혀 관련이 없고, 엔티티를 영속화할 때 연관된 엔티티도 같이 영속화 할 수 있는 편의를 제공하는 기능

  • 영속성 전이 : 저장
    •     @OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST)
          private List<Child> children = new ArrayList<Child>();
      
    • 영속성 전이 설정을 통해 부모만 영속 상태로 만들어도 자식도 함께 영속 상태로 됨
    • 7
  • 영속성 전이 : 삭제
    • 부모와 자식 엔티티를 삭제하려면..?
    •     Parent parent = em.find(Parent.class, 1L);
          Child child1 = em.find(Child.class, 1L);
          Child child2 = em.find(Child.class, 2L);
      
          em.remove(child1);
          em.remove(child2);
          em.remove(parent);
      
    • 연관관계 엔티티들을 모두 조회해서 일일이 remove 해줘야 함..
    • 영속성 전이를 통해 연관관계 엔티티도 함께 삭제가 가능함
    •   @OneToMany(mappedBy = "parent", cascade = { // 여러 속성 동시 사용
            CascadeType.PERSIST,
            CascadeType.REMOVE, 
        })
        private List<Child> children = new ArrayList<Child>();
      
        ...
      
        Parent findParent = em.find(Parent.class, 1L);
        em.remove(findParent); // 자식도 함께 삭제 Cascading
      
    • 삭제 순서는 외래키 제약조건을 고려해서 자식 먼저 삭제 후 부모 삭제됨
    • 만약 Cascade 속성 없이 부모만 삭제하면?
      • 자식 테이블에 걸려있는 외래키 제약조건에 의해 외래키 무결성 예외가 발생함..
    • 전이의 발생 시점?
      • em.persist(), em.remove() 호출 시 전이가 되는게 아님!!
      • flush() 호출 시 전이됨!!
  • CASCADE의 종류
    •     public enum CascadeType {
              ALL,        // 모두 적용
              PERSIST,    // 영속
              MERGE,      // 병합
              REMOVE,     // 삭제
              REFRESH,    // REFRESH
              DETACH      //DETACH
          }
      



4. 고아 객체

  • 고아 객체 삭제 : 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능
    • 부모 엔티티 컬렉션에서 자식 엔티티의 참조만 제거하면 자식 엔티티가 자동으로 삭제
  •   @Entity
      public class Parent {
          ...
          @OneToMany(mappedBy = "parent", orphanRemoval = true)
          private List<Child> children = new ArrayList<Child>();
          ...
      }
    
  • orphanRemoval = true 설정 시 컬렉션에서 삭제된 엔티티는 자동으로 삭제됨
  • 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 것
    • 따라서 참조하는 곳이 하나일 때만 이 기능을 적용해야 함(특정 엔티티가 개인 소유할 때)
    • 따라서 @OneToOne, @OneToMany에서만 사용



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

  • CascadeType.ALL + orphanRemoval = true를 동시에 사용하면?
    • 원래의 엔티티는 EntityManager를 통해 자신의 생명주기를 스스로 관리함
      • em.persist(), em.remove()를 통해 스스로 영속, 비영속 되는 것과 같이
    • 위와 같은 옵션을 사용할 경우 부모가 자식의 생명주기를 완전히 관리 가능
      • 자식을 부모에 등록할 때(CASCADE)
      •   Parent parent = em.find(Parent.class, parentId);
          parent.addChild(child1);
        
      • 자식을 부모에서 삭제할 때(orphanRemoval)
      •   Parent parent = em.find(Parent.class, parentId);
          parent.getChildren().remove(removeObject);
        


정리하면?

  • JPA 구현체는 객체 그래프 탐색 기능 지원을 위해 프록시 기술을 사용함
  • 객체를 조회할 때 연관된 객체를 즉시 로딩, 지연 로딩하는 방법을 지원함
  • 객체를 저장하거나 삭제할 때 연관된 객체도 함께 저장하거나 삭제할 수 있고 이를 영속성 전이라고 함
  • 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하려면 고아 객체 제거 기능을 사용





© 2020.02. by blupine