DevKim

[JPA] 프록시와 즉시-지연로딩 & CASCADE , 고아 객체 본문

JPA

[JPA] 프록시와 즉시-지연로딩 & CASCADE , 고아 객체

on_doing 2021. 6. 22. 18:54
728x90

[ 프록시 Proxy ]

즉시로딩과 지연로딩을 제대로 이해하기 위해선, 프록시에 대한 이해가 필요하다.

프록시 객체는 실제 클래스를 상속 받아서 만들어진 가짜 객체라고 생각하면 된다.

 

진짜 객체는 em.find( )로 불러오지만, 프록시 객체를 조회할 땐 em.getReference( )를 통해 불러온다.

 

프록시 객체는 실제 객체와 겉 모습은 같고, 실제 객체의 참조(target)을 보관하고 있다.

만약 여기서 프록시 객체의 getName()을 호출한다면, target에 있는 (진짜 객체) getName( )을 대신 호출해준다.

 

Client가 getName( )을 요청하면 내부적으로 영속성 컨텍스트에게 진짜 멤버 객체를 가져오라고 요청한다.

영속성 컨텍스트에서 DB를 조회해서 실제 엔티티를 생성해주고, 프록시와 연결해준다.

그 후에 타겟에 진짜 객체의 getName( )을 통해 값을 반환해준다.

 

프록시 객체는 처음 사용할 때 '한번만' 초기화된다.

프록시 객체를 초기화 할때, 프록시 객체가 실제 엔티티로 바뀌는 것은 당연히 아니고, 접근 가능한 상태가 되는 것이다.

 

프록시 객체는 원본 엔티티를 상속 받는다.따라서 타입 체크시 주의해야한다. 원본과 프록시를 == 비교하면 당연히 False가 뜨고, == 대신 instance of 를 사용해주면 True가 뜨는 것을 확인할 수 있었다.

Member member1 = new Member();
member1.setUsername("member1");
em.persist(member1);

Member member2 = new Member();
member2.setUsername("member2");
em.persist(member2);

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

System.out.println("member1==member2: "+(m1.getClass()==m2.getClass())); //False
System.out.println("member1==member2"+(m1 instanceof Member)); //True
System.out.println("member1==member2"+(m2 instanceof Member)); //True

 

영속성 컨텍스에 이미 찾는 엔티티가 있으면, 프록시를 호출해도 실제 엔티티를 반환한다.

( 그 반대로 refer가 있는상태에서 ,em.find( )로 반환하면 프록시를 반환 )

JPA는 같은 트렌젝션 안에서 한 영속성 컨텍스트에서 가져왔고, PK가 같으면 항상 == 은 TRUE를 반환해줘야하는 의무가 있기 때문이다.

Member member1 = new Member();
member1.setUsername("member1");
em.persist(member1);

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

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

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

준영속 상태로 분리하거나, 영속성 컨텍스트를 닫아버리거나 하면 프록시는 더이상 영속성 컨텍스트의 도움을 못 받기 때문에 이때 프록시를 초기화하게되면 당연히 예외를 터트린다.

이건 실무에서 반드시 만나게될 문제이므로 잘 기억하도록 해야한다고 한다.

보통 한 트렌젝션이 시작하고 끝나는 시점에 맞춰서 영속성 컨텍스트를 시작하고 끝내는데,

한 트렌젝션이 끝나고 프록시를 호출하는 상황에서 주의해야한다.

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

em.detach(refMember);
// em.close();
//em.clear();

refMember.getUsername();//프록시 초기화

프록시의 초기화 여부 확인, 프록시 클래스 확인 , 프록시 강제 초기화하는 방법들이 있다.

지금까진 get Name( )과 같이 사용함으로써 초기화를 해왔었다.

Member refMember=em.getReference(Member.class,member1.getId());

boolean loaded = emf.getPersistenceUnitUtil().isLoaded(refMember);
System.out.println("loaded = " + loaded); //초기화 여부 확인

System.out.println("refMember = " + refMember.getClass()); //프록시 클래스 확인

Hibernate.initialize(refMember); //강제 초기화

[ 즉시로딩과 지연로딩 ]

 

[ 지연로딩 ]

 

단순히 Member만 조회해야하는 비즈니스 로직에서 Team도 함께 조회를 하면 손해일 것 이다.

 

이때, 지연로딩을 사용해서 프록시로 조회할 수 있다.

fetch =fetchType.LAZY를 걸어두게되면, member을 조회할 때 Team을 프록시로 가져오게된다.

@Entity
public class Member{

    ...

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "TEAM_ID")
    private Team team;

}

지연로딩으로 가져왔다가

실제 팀을 '사용하는' 시점에 초기화가 일어난다.

이론적으로는,

만약 Team과 Member를 함께 가져오는 경우가 90% 이상인 비즈니스 로직에선

지연로딩은 쿼리를 두번 날려야하므로, 즉시로딩이 더 효율적이라고 생각할 수 있다.

 

하지만 실무에선 가급적 무조건 지연로딩만 사용하는 것을 권장한다고 한다.


[ 즉시로딩의 문제 ]

fetch = FetchType.EAGER

 

#1. 즉시로딩을 적용하면 예상하지 못한 SQL이 발생한다.

List<Member> members = em.createQuery("select m from Member m", Member.class)
                    .getResultList(); //쿼리가 두번나감

JPQL로 쿼리를 작성하게 되면, 먼저 SQL이 번역을 해서 멤버를 가져온다. 

멤버로 갔더니 Team이 즉시로딩으로 설정되어있으니 다시 쿼리 날려서 Team도 가지고 온다... (반복)

이렇게 객체가 더더 늘어나게된다면 join이 계속 일어나면서 성능적으로 손해다.

 

#2. 즉시로딩은 JPQL에서 N+1 문제를 일으킨다.


[ 해결방법 ]

 

해결방법으로는 엔티티 그래프 기능을 사용하거나 JPQL fetch 조인을 사용하는 것이다.

실무에선 거의 fetch 조인을 사용한다고한다.

 

1. Lazy+fetch join = 꿀조합 ㅎㅎ

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

한방 쿼리로 멤버랑 팀이랑 조인해서 가져오는 것을 확인할 수 있다.


[ 결론 ]

실무에선 일단 모두 지연로딩으로 깔아준다 -> fetch join과 적절히 섞어서 사용한다

** __ToOne 시리즈는 디폴트 값이 즉시로딩이므로 꼭 지연로딩으로 설정해주자 **


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

 

[ CASCADE 영속성 전이 ]

CASCADE는 특정 엔티티를 영속 상태로 만들 때, 연관된 엔티티도 함께 영속상태로 만들고 싶을 때 사용한다.

 

#원래 코드

...

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

귀찮음..Parent 중심의 코드임. Parent를 persist할때 child도 같이 persist하고 싶다면?

-->Cascade 사용

@Getter @Setter
@Entity
public class Parent {

    ...

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

    /*
    연관관계 편의 메서드
     */
    public void addchild(Child child)
    {
        childList.add(child);
        child.setParent(this);
    }
}

Parent만 em.persist로 넣어줘도, child모두 persist되는 것을 확인할 수 있었다.

 

CASCADE는 연관관계를 매핑하는 것과는 아무 관련이 없으묘, 그냥 연관된 엔티티도 함께 영속화하는 편리함만 제공할 뿐이다.

 

** 하나의 부모만이 child를 관리할때만 사용해야한다. (소유자가 하나일 때) **

다른 여러곳이랑 연관관계가 있으면 사용하면 안된다.


[ 고아객체 ]

부모엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제한다.

이것도 소유자가 하나일 때 (특정 엔티티가 개인 소유할 때..) 만 사용해야한다.

@Entity
public class Parent {
    ...

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

   ...
}

 

CascadeType.ALL + orphanRemovel = True 로 두개 모두 활성화하면,

부모 엔티티를 통해서 자식의 생명주기를 관리할 수 있게 된다.

 

참고 강의, 자료 출처

https://www.inflearn.com/course/ORM-JPA-Basic/dashboard

 

자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의

JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다., 본 강의는 자바 백엔

www.inflearn.com

 

728x90
Comments