JPA

프록시와 연관관계 (즉시로딩/지연로딩)

쭈녁 2022. 12. 21. 22:30

em.find() vs em.getReference()

  • find : 데이터베이스를 통해 실제 엔티티 객체를 조회함
  • getReference : 데이터베이스 조회를 미루는 가짜 엔티티 객체() 조회

 

프록시의 특징

  • 프록시 객체는 처음 사용할 때 한번만 초기화됨
  • 프록시 객체를 초기화 할때 프록시 객체가 실제 엔티티로 바뀌는것이 아님, 초기화 되면 프록시 객체를 통해 실제 엔티티에 접근이 가능하다.
  • 프록시 객체는 원본 엔티티를 상속받음, 따라서 타입 체크시 == 비교가 불가, instance of 사용하여 비교.
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 getReference()를 호출해도 프록시 객체가 아닌 실제 엔티티 객체가 반환됨
  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때(EntityManager가 닫히거나 영속성 컨텍스트에서 뺐을 때 등등), 프록시를 초기화하면 문제 발생(하이버네이트는 org.hibernate.LazyInitializationException 예외를 터트림)

 

try{
    Member member1 = new Member();
    member1.setName("hello1");
    em.persist(member1);

    Member member2 = new Member();
    member2.setName("hello2");
    em.persist(member2);

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

    Member findMember1REF = em.getReference(Member.class, member1.getId());
    Member findMember1 = em.find(Member.class, member1.getId());
    Member findMember2 = em.find(Member.class, member2.getId());
    Member findMember2REF = em.getReference(Member.class, member2.getId());
    System.out.println("findMember1REF.getClass() = " + findMember1REF.getClass());
    System.out.println("findMember1.getClass() = " + findMember1.getClass());
    System.out.println("findMember2.getClass() = " + findMember2.getClass());
    System.out.println("findMember2REF.getClass() = " + findMember2REF.getClass());
    System.out.println("findMember1.getClass() == findMember2REF.getClass() = " + (findMember1.getClass() == findMember2REF.getClass()));
    System.out.println("findMember1==findMember1REF = " + (findMember1 == findMember1REF));
    
    tx.commit()
    }

 

위와같이 em.find() vs em.getReference() 를 사용했을 때 아래와 같은 결과를 얻을 수 있다.

 

특이사항은 getReference()를 통해 호출한 후 영속성 컨텍스트에 올라가는 find로 찾았을 때 EntityManager는 두 객체가 같음을 보장하기 위해 find에서도 프록시 객체를 가져오는 것을 볼 수 있다.

 

반대로 먼저 영속성 컨텍스트에 올라간 객체의 경우(find를 통한 엔티티 조회) 프록시 객체가 아닌 Meber객체를 그대로 반환하고 getRefernce()를 통한 조회에도 같은 Member객체를 반환한다.

 

때문에 조심해야할 점은 서로 다른 객체를 비교 할 때 == 비교가 아닌 같은 인스턴스를 가지고 있는지 비교하는 것이 바람직 하다

 

지연로딩과 즉시로딩

아래 코드와 같은 상황이 있을 때 Member 객체를 조회 한다고 가정하자.

  • 즉시로딩(EAGER)으로 세팅 시 Member객체를 조회해 올 때 연관된 Team 테이블을 조인해서 가져온다.
  • 지연로딩(LAZY)으로 세팅 시 Member에서 member.getTeam 과 같이 Team 테이블에 접근 시 Join 쿼리를 날려 테이블을 불러온다.
@Entity
public class Member extends BaseEntity {
    @Id
    @GeneratedValue
    private Long id;

    private String name;
    private Integer age;

    @ManyToOne(fetch = FetchType.LAZY) //지연로딩
    @ManyToOne(fetch = FetchType.EAGER) //즉시로딩
    @JoinColumn(name = "TEAM_ID")
    private Team team;

 

Lazy 예시

 

각 설정 시 실제 쿼리

Eager

프록시와 즉시로딩 주의

  • 가급적 지연로딩만 사용하길 권장
  • 즉시로딩을 적용하면 예상치못한 SQL이 발생
  • 즉시로딩은 JPQL에서 N+1의 문제가 일어남(즉시로딩과 JPQL을 동시 사용시 Member객체를 저장하기 위해 1번 Team을 찾아오기 위해 SELECT MEBER를 1번 호출한다.)

EAGER

Team 객체를 바로 Select 해옴

LAZY

 

호출전

호출 전 쿼리

 

Team객체 호출시

호출 시  Team 테이블을 SELECT 해옴

 


영속성 전의 : CASCADE

  • 특정 엔티티를 영속 상태로 만들때 연관된 엔티티도 함께 영속상태로 만들고 싶을때(예 : 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장)
  • 영속성 전이는 연관관계 매칭하는것과 관계 없음
  • 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화 하는 편리함 제공
    • ALL: 모두 적용
    • PERSIST: 영속
    • REMOVE: 삭제
  • 단 한곳에서만 연관관계의 주인일 때만 사용해야 한다. (LifeCycle이 동일한 두 객체)

 

 

고아 객체 (orphanRemovel=true)

  • 참조가 제거된 엔티티를 다른곳에서 참조하지 않도록 해당 객체를 삭제하는 기능
  • 참조하는곳이 하나일때만 사용해야함
  • @OneToMany, @OneToOne 로 참조된 곳에서 설정해야함
  • CascadeType.ALL + orphanRemovel=true
    • 스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화, em.remove()로 제거
    • 두 옵션을 모두 활성화 하면 부모 엔티티를 통해서 자식의 생명 주기를 관리할 수 있음
    • 도메인 주도 설계(DDD)의 Aggregate Root개념을 구현할 때 유용

예시

 

Child 객체

@Entity
public class Child {
    @Id
    @GeneratedValue
    private Long id;

    private String name;

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

    public Parent getParent() {
        return parent;
    }

 

parent 객체

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

 

위와 같이 설정하였을 때 CASCADE 설정으로 인해 Parent 객체에서 Child 객체를 넣어주는것 만으로 Child 객체가 생성된다.

 

또한 Parent객체가 제거되면 CascadeType.ALL 설정이 되어 있음으로 Child 객체도 영속성 컨텍스트에 올라간다.(해당 설정만으로는 Parent객체 삭제시 Child가 삭제되지 않음)

 

orphanRemovel=true 설정으로 연관관계의 주인인 Parent 가 childList에서 Child 객체를 제거하였을 때 해당 Child 객체는 고아객체가 되어 삭제(DELETE 쿼리가 날라감) 된다. 이는 Parent객체가 삭제 되어도 연관관계가 끊어짐으로 Child객체가 삭제되게 된다.

'JPA' 카테고리의 다른 글

JPA 쿼리 언어 종류  (1) 2022.12.22
값 타입 정리  (0) 2022.12.21
@MappedSuperclass  (0) 2022.12.20
상속관계 매핑  (0) 2022.12.20
연관관계 매핑  (0) 2022.12.20