JPA 프록시와 연관관계 관리

떡ol·2023년 4월 23일
0

JPA는 JAVA의 객체지향적인 아키택처가 가능하게 해주는 라이브러리입니다. 그러니 ORM(Object Relational Mapping)으로 불립니다.
다만, 여러개의 객체를 담고있는 하나의 Entity가 굳이 전체의 내용을 조회할 필요는 없습니다. 아래 예시를 보면 이해하실겁니다.

그래서 저희는 이런 불필요한 쿼리를 사용하는 일을 없애기위한 연관관계 관리 방법을 배워보겠습니다.

프록시(Proxy)

1. 프록시(Proxy)란?

우선 앞선 내용을 알기전에 프로시에 대해서 알아볼 필요가있습니다.
프록시란 실제 객체(Member,Orders etc...)를 참조해주는 객체(Object)라고 생각하면 됩니다.
프록시를 알아보기 위해서는 getReference() 라는 매서드를 알아봐야합니다.
find()를 해당 매서드로 치환해서 실행해보겠습니다.

	Member member = new Member();
	member.setName("Faker");
	member.setTeam(team);
	em.persist(member);

	em.flush();
	em.clear(); // 일단 데이터 넣어주고 플러시 클리어까지 진행

	Member reference = em.getReference(Member.class, member.getId());//em.find를 바꾼겁니다.
	System.out.println("member ID: " + reference.getId());
	System.out.println("member name: " + reference.getName());
	System.out.println("member Class Type: " + reference.getClass());

위에 소스를 실행시키면 어떻게 될까요?
find()의 경우에는 영속성에 캐쉬값이 존재하면 그값을 리턴합니다. 위에 코드에서는 clear()를 진행하여 진짜 쿼리문을 한번 실행하게 될겁니다. 하지만 getReference() 는 그렇지 않습니다.

위에 소스를 실행한 결과입니다. 보이시나요? reference.getId() 경우에는 member.getId()가 캐쉬에 있으니 그대로 가져온 결과입니다. 하지만 reference.getName()는 출력 전에 쿼리문이 한번 실행되었습니다. find하고는 다르게 실제 사용되어질때 쿼리문이 실행되고 값을 받아옵니다. 그리고 해당 class는 Memeber.class가 아니라, HibernateProxy라고 적혀져있습니다. 프록시는 이처럼 데이터베이스 조회를 미루는 가짜 엔티티가 되는 겁니다. 영속성 컨텐츠 안에서 잠자고 있다가, 호출되어 사용되는시점에서 내부적으로 조회를 실행하게 되는겁니다.

2. 프록시의 특징

프록시 객체는 처음 사용할 때 한 번만 초기화
위에 코드에서 fmember.getName()를 한번 더 실행한다고해서 쿼리문이 다시 날라가고 프록시 객체를 생성하고 그러지는 않습니다. 그냥 처음에 썻던걸 그대고 가져와서씁니다.

프록시 != 엔티티
프록시는 해당 엔티티 객체를 가져오는게 아닙니다. 원본 엔티티를 상속받는 겁니다. 따라서 타입 체크를 할때에는 조심하셔야합니다.

	Member reference = em.getReference(Member.class, member.getId());
	System.out.println("reference == member : " + (member == reference)); //false
	System.out.println("reference == member, class : " + (member.getClass() == reference.getClass())); //false
	System.out.println("reference instanceof member " + (reference instanceof Member)); // true

그렇다면 영속성 캐쉬에 같은 값이 존재하면 어떻게 될까요? 그땐 캐쉬에서 조회하므로 같은 값이라고 합니다.

	Member member1 = em.find(Member.class, member.getId());
	System.out.println("member1= " + member1.getClass()); // Member.class
	Member reference = em.getReference(Member.class, member.getId()); // find() 값을 불러오므로 proxy가 아니게 됩니다.
	System.out.println("reference = " + reference.getClass()); // Member.class

마찬가지로 그반대도 같은 결과가 나타납니다.

	Member member1 = em.getReference(Member.class, member.getId());
	System.out.println("member1= " + member1.getClass()); // Proxy.class
	Member reference = em.find(Member.class, member.getId()); // 이번엔 Reference를 가져옵니다.
	System.out.println("reference = " + reference.getClass()); // Proxy.class

준영속 상태일 때, 에러
어찌보면 당연한거겠죠? 불러올 값이 빠졌는데 프록시 객체가 초기화되는게 이상한겁니다. 에러가 발생할겁니다. (하이버네이트는 org.hibernate.LazyInitializationException 예외를 터트림)

3. 프록시 유틸

isLoaded
프록시 객체가 초기화 되었는지를 확인하는 매서드입니다.

Member reference = em.getReference(Member.class, member.getId()); // 초기화가 안됐다...
emf.getPersistenceUnitUtil().isLoaded(reference) // false

Hibernate.Initialize
이 매서드는 Hibernate에서 지원하는 기능입니다. JPA lib은 다양하게 존재합니다. 따라서 다른 JPA에도 이게 있는지는 모르겠네요...

Member reference = em.getReference(Member.class, member.getId()); 
Hibernate.Initialize(reference);
// reference.getName(); //이 방법이 정통적인 초기화방법...
emf.getPersistenceUnitUtil().isLoaded(reference) // true

즉시로딩과 지연로딩

1. fetch, 로딩의 종류

지연로딩 fetch = FetchType.LAZY
앞에서 질문하였던 Member를 불러올때 Team 없이 조회가 가능한가? 이걸 가능하게 하는게 지연로딩입니다. 지연로딩은 연관관계 맵핑에서 옵션을 선언하면 사용이 가능합니다. 조회 매서드는 앞에서 배운 getReference()를 사용하겠습니다.

@Entity
public class Member {
    @ManyToOne(fetch = FetchType.LAZY) // 지연로딩으로 선언했다.
    @JoinColumn(name = "TEAM_ID")
    private Team team;
}

table join없이 member만 조회된것을 확인 가능합니다.

그렇다면 이후에 team을 조회하면 어떻게 될까요?

	System.out.println("member ID: " + reference.getId());
	System.out.println("member name: " + reference.getName()); //member를 우선 조회후
	System.out.println("team name: " + reference.getTeam().getName()); // member에서team을 조회해보겠습니다.


네... 잘되네요. 따로따로 값을 쿼리에서 조회하고 결과를 반환해줍니다.
또한 이때의 proxy값을 조회해보면 값이 일치합니다. 초기화를 한번만 했다는 거죠.

find()를 사용해도됩니다.
find()라면 기존 맴버는 Member Object, Team은 Proxy Object로 반환 될겁니다.
앞의 내용들을 잘 이해했다면, 이해하실겁니다.

	find.getTeam().getClass() // print :: class com.Member
	find.getTeam().getClass() // print :: class com.Team$HibernateProxy$v0lXhwm1

즉시로딩 fetch = FetchType.EAGER
즉시로딩은 지연로딩에 반대입니다. 그냥 하던데로 tablejoin해서, 전체를 조회하는 겁니다.
해당 옵션에 결과는 따로 작성하지 않겠습니다.

2. 특징

  • 가급적 지연 로딩만 사용(특히 실무에서)
  • 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생
  • 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.
    - JPQL을 사용하면 em.createQuery("select m from Member m") 이런 쿼리문을 작성하게 됩니다. 이때, JPA가 이를 한번 조회하고, Member에 Team이 있다고 판단되면 다시 조회하게 됩니다 이때는 Member, Team을 두번 조회하겠네요.. 그래서 2(Memeber,Team) + 1(기본 쿼리문) 굉장히 어질어질 합니다..
  • @ManyToOne, @OneToOne은 기본이 즉시 로딩 > 지연으로 바꿔줍시다.
  • @OneToMany, @ManyToMany는 기본이 지연 로딩
profile
하이

0개의 댓글