em.find()
vs em.getReference()
em.find()
- 데이터베이스를 통해서 실제 엔티티 객체를 조회
em.getReference()
- 데이터베이스 조회를 미루는 가짜 엔티티 객체 조회
Member member = new Member(null, "memberA");
em.persist(member);
em.flush();
em.clear();
Member findMember = em.find(Member.class, member.getId());
System.out.println("findMember = " + findMember);
tx.commit();
find()
로 멤버를 조회할 시에는 findMember
변수의 사용 여부와 관계없이 메서드가 실행되는 시점에 즉시 실제 데이터베이스로부터 select
쿼리를 통해서 멤버를 조회한다.
Member member = new Member(null, "memberA");
em.persist(member);
em.flush();
em.clear();
Member findMember = em.getReference(Member.class, member.getId());
//System.out.println("findMember = " + findMember);
tx.commit();
getReference()
로 멤버를 조회할 때에는 println()
메서드와 같이 해당 변수가 사용되지 않을 경우에는 조회쿼리가 나가지 않고, 조회한 엔티티가 사용되는 시점에 실제 데이터베이스에 조회쿼리를 날려 데이터를 가져온다.
==약간 쿼리를 킵해뒀다가 필요할 때 쿼리를 DB에 날려 사용하는느낌이다.
참고로, findMember.getId
를 사용할 경우에는 findMember
의 id값은 이미 영속성 컨텍스트에 저장되어있기때문에 이 경우에는 조회쿼리를 날리지 않는다.==
이러한 방식이 가능한 이유는 프록시 객체를 사용하기 때문인데, 프록시란 가짜를 의미한다.
em.getReference()
메서드를 호출하는 시점에는 findMember
에 실제 Member
클래스를 상속받아 생성한 프록시 객체를 임의로 저장해둔다.
(프록시 객체는 객체의 필드를 Null값으로 가지고 있으며, 실제 객체의 메서드만 가지고 있다.)
후에 System.out.println(findMember.getName)
과 같이 실제 데이터베이스에 저장되어 있는 데이터가 필요할 경우 프록시객체는 JPA에 초기화를 요청하고 영속성 컨텍스트에서 데이터베이스에 접근해서 쿼리를 날려 실제 엔티티를 가져오게 된다.
이제 프록시객체는 target.getName()
을 통해 실제 객체의 name
데이터를 가져오게 된다.
프록시 객체의 특징
- 프록시 객체는 처음 사용할 때 한 번만 초기화한다.
- 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아니며 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근이 가능해지는 것이다.
- 프록시 객체는 원본 엔티티를 상속받기때문에 타입 체크시
instance of
를 사용해야한다. - 영속성 컨텍스트에 찾는 엔티티가 이미 있으면
em.getReference()
를 호출해도 실제 엔티티를 반환한다. - 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하는 문제가 발생한다.
이 때 하이버네이트는LazyInitializationException
예외를 터트린다.
프록시 객체의 사용
실제로 실무에서 프록시객체를 직접 불러오는 getReference()
메서드를 잘 사용하지는 않는다.
하지만 지연로딩과 즉시로딩의 개념을 확실하게 이해하기 위해서는 프록시 객체를 이해해야한다.
만약 Member
엔티티와 Team
엔티티가 서로 연관관계를 이루고 있다고 가정해보자,
애플리케이션 로직에서 Member
를 조회할 때 Team
을 함께 조회하는 경우도 있지만, Team
을 조회하지 않고 Member
만 조회하는 경우도 있을 것이다.
Member memberA = new Member(null, "memberA");
em.persist(memberA);
Team team = new Team();
team.setTeamName("teamA");
em.persist(team);
memberA.setTeam(team);
em.flush();
em.clear();
Member findMember = em.find(Member.class, memberA.getId());
tx.commit();
위와 같이 findMember
에 Member
만을 조회하고 싶지만 실제 JPA의 쿼리 결과를 보면 Team
을 조인하여 Team
엔티티도 함께 조회된다는 점을 확인할 수 있다.
select
m1_0.id,
m1_0.name,
t1_0.TEAM_ID,
t1_0.TEAM_NAME
from
Member m1_0
left join
Team t1_0
on t1_0.TEAM_ID=m1_0.TEAM_ID
where
m1_0.id=?
이렇게 Member
객체만을 조회하고자하는 의도로 사용했지만, Team
와의 연관관계를 이루고 있기때문에 join
쿼리가 반드시 나가게되어 성능이 저하될 수 있다.
JPA는 이러한 경우를 방지하고 성능을 최적화하기위해서 프록시를 이용한 지연로딩과 즉시로딩 기능을 제공하는 것이다.
지연로딩
Member memberA = new Member(null, "memberA");
em.persist(memberA);
Team team = new Team();
team.setTeamName("teamA");
em.persist(team);
memberA.setTeam(team);
em.flush();
em.clear();
Member findMember = em.find(Member.class, memberA.getId());
tx.commit();
프록시 페이지에서 정리했던 듯이 위 코드를 실행할 경우 Member
만 조회하고자 했음에도 Team
도 연관관계로 인해 함께 조회되기때문에 조인쿼리가 발생되고 이로인해 성능저하 문제가 발생하게 된다.
이 때 지연로딩을 통해 Team
을 조회하지 않고 Member
엔티티만 조회할 수 있다.
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
연관관계의 주인인 Member
엔티티에서 fetch
속성을 LAZY로 주게되면 지연로딩을 적용하여 Member
엔티티만을 조회할 수 있게된다
이 때 조회한 Member
클래스의 Team
에 대해 클래스를 조회하면 Team
객체는 프록시 객체로 생성된 것을 확인할 수 있다.
Member findMember = em.find(Member.class, memberA.getId());
System.out.println(findMember.getClass());
System.out.println(findMember.getTeam().getClass());
tx.commit();
class hellojpa.Member
class hellojpa.Team$HibernateProxy$4y8HO1UP
Team
객체에 대해서는 프록시객체로 생성해 둔 뒤, Team
객체에 대해서 실제로 값을 조회하거나 사용하고자 할 때 Team
의 데이터를 실제 데이터베이스로부터 받고 Team
프록시 객체는 데이터를 담고 있는 실제 Team
객체를 호출하여 데이터를 사용한다.
### 즉시로딩
즉시로딩은 말그대로 프록시객체를 생성하지 않고, Member
객체만 사용하고자 하더라도 연관관계 매핑되어있는 Team
객체도 함께 가져오는 쿼리를 날리게 된다.
지연로딩에서는 Team
객체가 사용되는 시점에 Team
객체를 사용하는데 필요한 쿼리를 날렸지만,
즉시로딩에서는 Team
객체를 가져오는순간, 또는 Member
객체를 가져오는 순간에 쿼리를 날리며 join
쿼리를 날리기때문에 프로젝트의 규모가 작다면 상관없겠지만 만약 테이블의 개수가 많을 경우에는 성능문제가 발생하고 JPQL을 사용시에 N+1문제가 발생한다.
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "TEAM_ID")
private Team team;
지연로딩과 마찬가지로 연관관계 주인의 fetch
속성을 EAGER
로 설정해주면 즉시로딩을 사용할 수 있다.
즉시로딩과 지연로딩의 비교를 위한 JPA로그
지연로딩
select
m1_0.id,
m1_0.name,
m1_0.TEAM_ID
from
Member m1_0
where
m1_0.id=?
Team
객체를 사용하지 않았기때문에 프록시객체가 생성만 되어있고Team
객체를 위한 쿼리는 사용하지 않는다.
즉시로딩
select
m1_0.id,
m1_0.name,
t1_0.TEAM_ID,
t1_0.TEAM_NAME
from
Member m1_0
left join
Team t1_0
on t1_0.TEAM_ID=m1_0.TEAM_ID
where
m1_0.id=?
- 즉시로딩 사용시에는
Team
객체를 사용하지 않더라도join
쿼리를 날리는 것을 확인할 수 있다.
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!