프록시와 연관관계 관리

유동우·2023년 8월 6일
0
post-thumbnail

프록시

Member findMember = em.getReference(Member.class, member.getId());

//실제 객체 사용시 select쿼리 생성
System.out.println("findMember.getId() = " + findMember.getId());
System.out.println("findMember.getUsername() = " + findMember.getUsername());

위에서 생성한 객체인 findMember의 정체는 무엇일까?

System.out.println("findMember = " + findMember.getClass());

//출력값 
findMember = class hellojpa.Member$HibernateProxy$VLw384CP

메서드에서 매개변수가 프록시로 넘어올지 실제 값으로 넘어올지 모르기 때문에 == 비교를 하면 안된다
-> (ex) m1 instanceof Member 사용하기

Member m1 = em.find(Member.class, member1.getId());
Member m2 = em.getReference(Member.class, member2.getId());

System.out.println(m1.getClass());
System.out.println(m2.getClass());
System.out.println("m1 == m2: " + (m1.getClass() == m2.getClass()));


//출력값
class hellojpa.Member
class hellojpa.Member$HibernateProxy$FvgZWIDg
m1 == m2: false
Member member1 = new Member();
member1.setUsername("member1");
em.persist(member1);

em.flush(); //영속성 컨텍스트가 보관하고 있는 캐시된 쿼리를 해소
//flush는 persist 시에 자동으로 발생한다, 명시적으로 비었음을 강조하기 위해 호출한 것 뿐

em.clear(); //1차 캐시된 객체들을 clear로 정리하기 위함

Member m1 = em.find(Member.class, member1.getId());
System.out.println("m1 = " + m1.getClass());

Member reference = em.getReference(Member.class, member1.getId());
System.out.println("reference = " + reference.getClass());

//출력값
m1 = class hellojpa.Member
reference = class hellojpa.Member

위 코드 풀이 : 영속성 컨텍스트에 이미 찾는 엔티티가 있으면 em.getReference()를 호출해도 실제 엔티티를 반환한다.

1. 이미 멤버를 영속성 컨텍스트에 올려놓았기 때문에 굳이 프록시를 가져오지 않고 원본을 가져오는 것이 이득이다.
2. == 비교 : JPA는 한 영속성 컨텍스트 안에서 가져온것이고, PK값이 똑같으면 항상 true 값을 반환해야 한다.

2번을 코드로 풀면

System.out.println("refMember == findMember: " + (refMember == findMember));

//위 출력값이 항상 true가 나와야 한다 (같은 영속성 컨텍스트에 속해있을 경우)

//따라서 
Member refMember = em.getReference(Member.class, member1.getId());
System.out.println("refMember = " + refMember.getClass());

Member findMember = em.find(Member.class, member1.getId());
System.out.println("findMember = " + findMember.getClass());

//getReference 로 프록시를 먼저 호출한 후 find를 해도 프록시 값이 나오게된다
//(find를 먼저하고 getReference를 하면 프록시가 아닌 본래의 값이 나온다)

영속성 컨텍스트의 도움을 받지 못하는 준영속 상태일 경우, 프록시를 초기화하면 문제가 발생한다
=> 실무에서 자주 겪을 수 있다


em.detach(refMember)
//em.clear()
//em.close() 이후 

refMember.getUsername(); // 조회 시 

org.hibernate.LazyInitializationException //예외 발생 

즉시 로딩과 지연 로딩

멤버와 팀을 자주 같이 사용하지 않는 경우 (지연로딩)

멤버만을 조회하고 싶은데 조인관계로 인해 팀까지 조회하는 경우가 발생할 수 있다
=> "지연로딩" 이라는 옵션을 제공

지연로딩을 세팅하는 방법

public class Member{ 
	...
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn
    private Team team;
    ...
}

위처럼 함으로써 멤버를 조회할 경우 팀에대한 값을 프록시 값으로 가져온다.
이후 팀을 직접 사용할 때 초기화한다.


멤버와 팀을 자주 같이 사용하는 경우 (즉시로딩)

public class Member{ 
	...
    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn
    private Team team;
    ...
}

즉시로딩을 하면 em.find(Member.class, ... )를 호출할 때 프록시 값이 아닌 진짜 값을 가져온다.


중요

프록시와 즉시로딩 사용시 주의

  • 가급적 실무에서는 지연로딩을 사용하자
  • 즉시로딩은 예상치 못한 SQL이 발생하고, JPQL에서 N+1 문제가 발생한다
  • @ManyToOne, @OneToOne은 기본이 즉시로딩이다. -> LAZY로 설정하기

활용 예제

• Member와 Team은 자주 함께 사용 -> 즉시 로딩
• Member와 Order는 가끔 사용 -> 지연 로딩
• Order와 Product는 자주 함께 사용 -> 즉시 로딩

이후 프록시인 orders를 사용하게 되면 EAGER로 걸려있는 product는 프록시값이 아닌 본래의 값이 호출된다.

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

영속성 전이

em.persist(parent);
em.persist(child1);
em.persist(child2);

위 코드처럼 영속성을 parent만을 건드려서 child 또한 영속성 컨텍스트에 추가하고 싶은 경우

@OneToMany(mappedBy = "parent",cascade = CascadeType.ALL)

그렇다면 OneToMany 관계에서는 모두 cascade 옵션을 추가 해야되나?
= X

만약, child가 부모인 parent 클래스 외에 다른 클래스와도 관계가 있는경우에는
parent 클래스에 cascade를 해주지 않는다.
(운영이 너무 힘들어지기 때문)

즉, 단일 엔티티에 종속적인 경우에만 사용하자 (parent와 child의 life cycle이 동일할 경우)


고아 객체

부모 객체가 제거되면 그의 자식 객체도 같이 제거가 된다.
=> 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 판단하고 삭제하는 기능

cascade와 마찬가지로 아래의 조건 모두 만족할 때 사용한다

  • 참조하는 곳이 하나이다
  • 특정 엔티티가 개인 소유한다

@OneToMany, @OneToOne 만 가능하다
(부모 엔티티의 자식 엔티티를 자동으로 지우는 것이니 당연히 OneToXXX 만 가능)


Cascade 정리
1. 완전 개인 소유인 경우에만 사용할 수 있다 (게시판 <-> 첨부파일)
2. DDD(도메인 주도 설계)의 Aggregate Root와 어울린다
3. 애매하면 사용하지 않는다(Order <-> Delivery)

헷갈리는것 정리

CascadeType.ALL + orphanRemove=true 정리

  1. CascadeType.ALL 만 설정한 경우
  • 부모 객체를 삭제하면, 자식 객체까지 모두 삭제된다
  • findParent.getChildList().remove(0) 을 하면, parent의 리스트에서 제거는 했지만, orphanRemove 설정이 없기 때문에 DB에서 삭제되지 않는다.
  1. CascadeType.ALL + orphanRemove 모두 설정
  • 부모 객체 삭제시 자식 객체 모두 삭제
  • findParent.getChildList().remove(0) 을 하면, parent의 리스트에서 제거 될 시 (즉, 고아가 됨을 인지 했을 경우) DB에서 삭제된다

3.. orphanRemoval이 CascadeType.All or CascadeType.PERSIST 인 경우 정상적으로 DELETE 쿼리가 생성된다

=> 실무에서는 orphanRemoval만 따로 적용하는 경우도 많지 않을 뿐더러, 주인 엔티티가 하위 엔티티를 관리하는 경우에는 CascadeType.PERSIST + orphanRemoval 을 함께 적용하기 때문에 영향이 크지 않다.

실전 예제 5 - 연관관계 관리

Reference
김영한 님 - 자바 ORM 표준 JPA 프로그래밍 - 기본편

profile
효율적이고 꾸준하게

0개의 댓글