[스프링 JPA] - 프록시와 로딩

sonnng·2023년 10월 4일
0

Spring

목록 보기
16/41

목차

  1. 프록시
  2. 즉시로딩과 지연로딩
  3. 지연로딩 활용
  4. 영속성 전이 :CASCADE
  5. 고아 객체
  6. 실전예제


1. 프록시


[프록시 기초]

em.find() vs em.getReference()를 비교해보면,

em.find() : 데이터베이스를 통해 실제 엔티티 객체를 조회, sql 쿼리문이 나간다.

em.getReference() : 엔티티 객체의 필드를 조회하려고 할 때 그 때 데이터베이스에 대한 sql쿼리문이 실행, 데이터베이스 조회를 미룬다.

EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();

EntityTransaction tx = em.getTransaction();
tx.begin();
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());

    Member findMember2 = em.getReference(Member.class, member.getId());//1
    System.out.println("findMember = "+ findMember2.getClass()); //0
    System.out.println("findMember.id = "+findMember2.getId());//2
    System.out.println("findMember.id = "+findMember2.getUserName()); //3

    tx.commit();
}catch(Exception e){
    tx.rollback();
}finally {
    em.close();
}
emf.close();

1, 2번 -- SELECT 쿼리문이 실행x

3번 -- SELECT 쿼리문이 실행o



0번 -- findMember = class jpabook.Member$HibernateProxy$z10qFr6p 로 출력된다.

= 프록시가 붙은 객체임을 알 수 있다.



[프록시 특징]

em.getReference()
:

  • 실제 엔티티를 상속받아서 겉모양이 같게 만들어진다.
  • 실제 객체의 참조를 보관한다.
    Proxy

    [Entity target(실제 객체에 대한 참조)]

    [getId()]

    :

    프록시 객체에 없는 값을 요청한다면 아래와 같은 로직이 작동
  1. Client가 프록시.getName() 요청
  2. 프록시에는 getName()이 없으면 영속성 컨텍스트에 초기화 요청
  3. 영속성 컨텍스트 -> DB 조회
  4. 영속성 컨텍스트 -> 실제 Entity 생성(프록시 객체가 실제 엔티티로 바뀌는 것이 아니다.)
  5. target.getName() 가지고 옴


    :
  • 프록시 객체 초기화할 때, 프록시 객체가 실제 엔티티로 바뀌는 것이 아니다.
  • 프록시 객체는 원본 엔티티를 상속받음
  • 타입체크시 instance of 사용(== 체크X)
  • 영속성 컨텍스트에 객체가 채워진 후 -> em.getReference하는 경우, 영속성 컨텍스트 실제 엔티티객체를 상속받음(이 경우 프록시 객체가 X)(반대도 성립!)
Member m1 = em.find(Member.class, member1.getId());    
Member m1Reference = em.getReference(Member.class, member1.getId());

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

[출력결과]

reference = :class jpabook.Member

m1 == m1Reference: true

(== 비교에도 true로 출력됨)

=> JPA는 한 트랜잭션 안에서 같은 영속성 컨텍스트에서 가져온 객체로 == 비교를 한다면 true로 보장

  • em.getReference로 프록시 객체를 반환 받은 후 -> em.find하는 경우에도 같은 프록시 객체를 반환받게 된다.

  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때 프록시를 초기화하면 문제 발생

EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();

EntityTransaction tx = em.getTransaction();
tx.begin();
try{
    Member member1 = new Member();
    member1.setUserName("hello");
    em.persist(member1);

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

    Member m1Reference = em.getReference(Member.class, member1.getId());
    System.out.println("m1 = :"+m1Reference.getClass());//Proxy

    em.detach(m1Reference);//영속성 컨텍스트에서 관리 안해!
    //em.close();도 마찬가지로 에러를 발생시킨다.
    //em.clear();도 마찬가지

    m1Reference.getUserName();//SELECT 쿼리 발생(영속성 컨텍스트의 도움을 받아서)


    tx.commit();
}catch(Exception e){
    tx.rollback();
    e.printStackTrace();
}finally {
    em.close();
}
emf.close();

`org.hibernate.LazyInitializationException: could not initialize proxy [jpabook.Member#1] - no Session `
:
영속성 컨텍스트에서 관리를 안한다고 `em.detach`했기 때문에 getUserName() 불가
  • 초기화 했는지 여부 확인 방법 emf.getPersistenceUnitUtil().isLoaded(프록시 객체) or Hibernate.initialize(프록시 객체)
Member member1 = new Member();
member1.setUserName("hello");
em.persist(member1);

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

Member m1Reference = em.getReference(Member.class, member1.getId());
System.out.println("m1 = :"+m1Reference.getClass());//Proxy
m1Reference.getUserName(); //강제 호출
System.out.println("isLoaded = "+ emf.getPersistenceUnitUtil().isLoaded(m1Reference));

tx.commit();



2. 지연로딩과 즉시로딩

1) 지연로딩의 경우


@Id
@GeneratedValue //생략하면 AUTO
@Column(name = "MEMBER_ID")
private Long id;
private String username;

@ManyToOne(fetch = FetchType.LAZY)//지연로딩..멤버클래스만 디비에서 조회
@JoinColumn(name = "TEAM_ID") //읽기전용
private Team team;

@OneToMany(mappedBy = "member")
private List<MemberProduct> memberProducts = new ArrayList<>();


  • Team은 FetchType.LAZY로 설정되어 있으므로 프록시 엔티티를 가져오게 된다.

[순서]

1. 로딩 -> MEMBER 객체 영속성 컨텍스트에 저장
2. 지연로딩 LAZY -> TEAM 엔티티 프록시 객체 생성



[테스트 결과]

//프록시 = FetchType.LAZY
Member m = em.find(Member.class, member1.getId());//member만 SELECT
System.out.println("team = "+m.getTeam().getClass());//Team 정보는 프록시로 가져온 것
  • m.getTeam().getClass() : team = class jpabook.Team$HibernateProxy$YpIAexe7 출력됨을 확인
  • m.getTeam().getName() : SELECT TEAM 쿼리문이 작성된다. 실제 TEAM을 사용하는 시점에 초기화된다.(db조회)(getName에 해당하는 값이 없으므로 해당 값에 대해서 초기화된다.)

-> Member만 많이 사용하는 경우 LAZY 지연로딩을 사용




2) 즉시로딩의 경우


@Id
@GeneratedValue //생략하면 AUTO
@Column(name = "MEMBER_ID")
private Long id;
private String username;

@ManyToOne(fetch = FetchType.EAGER)//즉시로딩..멤버클래스와 팀 클래스 조인으로 함께 디비에서 조회
@JoinColumn(name = "TEAM_ID") //읽기전용
private Team team;

@OneToMany(mappedBy = "member")
private List<MemberProduct> memberProducts = new ArrayList<>();


  • Team은 FetchType.EAGER로 설정되어 있으므로 실제 엔티티를 가져오게 된다.

[순서]

1. 로딩 -> MEMBER 객체 영속성 컨텍스트에 저장
2. 지연로딩 LAZY -> TEAM 실제 엔티티 객체 생성



[테스트 결과]

//프록시 = FetchType.LAZY
Member m = em.find(Member.class, member1.getId());//member만 SELECT
System.out.println("team = "+m.getTeam().getClass());//Team 정보는 실제 엔티티 클래스가 출력된다.

System.out.println("==================");
System.out.println("team.getName() = "+m.getTeam().getName());
System.out.println("==================");
  • m.getTeam().getClass() : team = class jpabook.Team 출력됨을 확인
  • m.getTeam().getName() : 바로 team.getName() 정보를 가져오므로 초기화되지 않는다.

-> Member와 TEAM을 함께 많이 사용하는 경우 EAGER 즉시로딩을 사용




[프록시와 즉시로딩 주의]


  • 가급적 지연로딩만 사용(특히 실무에서)
  • 즉시로딩은 JPQL에서 N+1문제를 일으킨다.(대부분의 대안 : LAZY + join fetch..한번에 Member와 Team을 조인하여 한번에 값들을 가져온다. )




//프록시 = FetchType.LAZY
Member m = em.find(Member.class, member1.getId());//member만 SELECT
System.out.println("team = "+m.getTeam().getClass());//Team 정보는 프록시로 가져온 것

List<Member> members = em.createQuery("select m from Member m join fetch m.team", Member.class).getResultList();


[결과]
Hibernate: 
/* select
m 
from
Member m 
join
fetch m.team */ select
    member0_.MEMBER_ID as MEMBER_I1_4_0_,
    team1_.TEAM_ID as TEAM_ID1_9_1_,
    member0_.createdBy as createdB2_4_0_,
    member0_.createdDate as createdD3_4_0_,
    member0_.lastModifiedBy as lastModi4_4_0_,
    member0_.lastModifiedDate as lastModi5_4_0_,
    member0_.TEAM_ID as TEAM_ID7_4_0_,
    member0_.username as username6_4_0_,
    team1_.createdBy as createdB2_9_1_,
    team1_.createdDate as createdD3_9_1_,
    team1_.lastModifiedBy as lastModi4_9_1_,
    team1_.lastModifiedDate as lastModi5_9_1_,
    team1_.name as name6_9_1_ 
from
    Member member0_ 
inner join
    TEAM team1_ 
        on member0_.TEAM_ID=team1_.TEAM_ID

  • @ManyToOne, @OneToOne은 기본값이 즉시로딩 -> LAZY로 설정 필수(이외는 기본값이 지연로딩)



[지연로딩 활용]

  • 실무에서는 지연로딩으로 해야한다는 전제하에 보도록 한다.
  • 모든 연관관계에 지연로딩을 사용한다.
  • 실무에서는 즉시로딩을 사용하지 않도록 한다.
  • 즉시로딩은 상상하지 못한 쿼리가 나간다...JPQL fetch 조인이나 엔티티 그래프 기능을 사용하도록 한다.



3. 영속성 전이:CASCADE

  • 특정 엔티티를 영속 상태로 만들 때 연관 엔티티로 함께 영속 상태로 만들고 싶을 때 사용하는 것
  • 단일 엔티티에 완전히 종속적일 때 사용해도 무방하다.


[예]

package jpabook;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

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


package jpabook;

import javax.persistence.*;

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

    private String name;

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

}


ntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();

EntityTransaction tx = em.getTransaction();
tx.begin();
try{

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

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

    em.persist(parent);//자동으로 child도 persist로 되었으면 좋겠다..how?
    em.persist(child1);
    em.persist(child2);

    tx.commit();
}catch(Exception e){
    tx.rollback();
    e.printStackTrace();
}finally {
    em.close();
}
emf.close();

이 경우 INSERT 문이 총 3개가 출력..PARENT만 PERSIST해도 관련된 child는 모두 PERSIST 하고 싶을 때, cascade를 사용하도록 하자



[변경된 코드]

package jpabook;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

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

    private String name;

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

    public void addChild(Child child){//양방향 연관관계
        childList.add(child);
        child.setParent(this);
    }
}
  em.persist(parent);


결과는 INSERT문이 총 3개가 나가며, parent와 관련된 child까지 모두 PERSIST된다.




[영속성 전이 - CASCADE 주의할 점]


  • 연관관계 매핑과 전혀 관계없다.
  • CASCADE 종류 중 ALL(모두 적용), PERSIST(영속) 만 사용하는 것을 추천한다.
  • 사용조건 : Life Cycle이 똑같을 때 + 소유자가 하나 일때 사용하도록 한다.



4. 고아객체

  • 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 말한다.
  • 고아객체 제거 = 이러한 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 것을 말한다.(orphanRemoval = true로 설정하면 가능)


@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Child> childList = new ArrayList<>();

parent 클래스에서 orphanRemoval = true로 설정하면, 연관관계가 끊어진 고아객체를 자동으로 삭제해준다.

[Test]

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

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

em.persist(parent);//자동으로 child도 persist로 되었으면 좋겠다..how?

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

Parent findParent = em.find(Parent.class, parent.getId());
findParent.getChildList().remove(0);


/* delete jpabook.Child */ delete 
    from
        Child 
    where
        id=?

이러한 쿼리문이 나간다.



[고아객체 - 주의할 점]

  • 참조하는 곳이 하나일때 사용해야 한다
  • 특정 엔티티가 개인 하나만 소유할 때 사용하도록 한다
  • @OneToOne, @OneToMany에서만 가능
  • orphanRemoval = true = CascadeType.REMOVE처럼 동작(parent 삭제되도 child 모두 함께 삭제됨을 확인함 = CascadeType.ALL에서도 마찬가지)


[영속성 전이 + 고아객체, 생명주기 함께?]

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



0개의 댓글