본문 바로가기

Spring 정리/Spring JPA

JPA 프록시(+즉시로딩, 지연로딩 비교)

반응형

 개요


 엔티티가 양방향으로 매핑을 하는데 혹시 연관관계를 조회할 필요가 없는 경우는 어떻게 할까요?

 

예를들어, Member가 Team을 칼럼으로 가지고 있지만, Member만 조회하고 싶을 때 어떻게 해야할까요?

Team을 거의 사용하지 않은 경우, 매번 Member와 함께 조회하는 것은 비효율적입니다. JPA는 Proxy와 지연로딩을 통해서 해당 문제를 해결합니다.

 

 

 

 

 

JPA 프록시 내부 동작 방식


 JPA라고 해서 프록시가 특별한 것은 아닙니다. 단지 Hibernate가 내부적으로 프록시를 이용해서 구현할 뿐입니다. 기존의 프록시의 특징과 비교해서 살펴보면 이해가 쉽습니다. "JPA 프록시"의 핵심은 가짜 엔티티 객체를 조회해서 실제 DB조회를 끝까지 미루기 위함입니다. 

 

 

1. JPA 프록시 객체는 실제 클래스와 겉모양이 같다.

 

 겉모양이 같다는 것은 뒤집어 말해, 사용자 입장에서 객체를 사용할 때 진짜 객체인지, 프록시 객체인지 알 필요가 없다는 뜻입니다. 실제로 개발자들이 사용하는 많은 라이브러리들의 함수가  사용자 입장에서는 프록시의 존재를 모른 채, 단순히 사용법에 따라서 메서드를 호출하고 사용하면 됩니다. 실제로, 가짜 엔티티 객체도 해당 객체의 칼럼을 조회하는 순간, DB조회 쿼리문이 나갑니다.

 

 

2. JPA 프록시 객체는 실제 클래스를 "상속" 받아서 만들어지며, 실제 객체의 "참조"를 보관한다.

 

상속

프록시 객체는 단독적으로 존재하는 것이 아니라, 실제 객체를 상속하여 만들어집니다. 초기화 될 때, 실제 객체가 생성되고, 프록시 객체는 역시나 생성된 실제 객체를 참조합니다.

 

 

3. JPA 프록시 객체를 호출하면, 프록시 객체는 실제 객체의 메소드를 "호출"한다.

 

Proxy는 단지 Entity를 참조하고 호출할 뿐

 

 아래에 예시에도 나오지만, 결국 한번 프록시 객체는 영원히 프록시 객체입니다. (지연로딩에서 확인할 수 있습니다.) 프록시 객체가 실제 엔티티가 될 수 없습니다. 칼럼을 호출하여 DB에 SQL 쿼리를 날려도 영원히 프록시 객체입니다. 

 

 

 

프록시와 영속성 컨텍스트의 관계


 

처음에 지연로딩으로 호출할 때 proxy에 초기화 요청을 하고. 이후에는 실제로 해당 proxy가 참조하고 있는 객체를 호출합니다.

 

 

* em.find() vs em.getReference() 비교로 프록시 조회 알아보기

 

 이전까지는 엔티티를 영속성컨텍스트에 조회하는 em.find()를 사용했는데, "실제 엔티티"가 아닌 "프록시 객체"를 조회하는 em.getReference()가 있습니다.

 

 

1. em.find()로 쿼리 조회하기

 

일반적인 em.find()를 통해 호출하여 영속성 컨텍스트에 Member 엔티티를 조회합니다.

 

Member member = new Member();
member.setName("memberA");
em.persist(member);

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

Member savedMember = em.find(Member.class, member.getId());

tx.commit();

 

쿼리 결과는 다음과 같습니다.

 

Hibernate: 
    select
        member0_.id as id1_1_0_,
        member0_.age as age2_1_0_,
        member0_.name as name3_1_0_,
        member0_.TEAM_ID as TEAM_ID4_1_0_ 
    from
        Member member0_ 
    where
        member0_.id=?

 

 

 

 

2-1. em.getReference()로 조회해보기

 

em.find()이외에 em.getReference()가 있습니다. 실제 데이터베이스 조회를 미루는 가짜 엔티티를 조회합니다. 조회된 객체는 가짜이기 때문에 DB 조회 쿼리문이 나가지 않습니다.

 

Member member = new Member();
member.setName("memberA");
em.persist(member);

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

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

tx.commit();

 

실제 savedMember의 필드가 조회되기 전까지는 아무런 쿼리를 날리지 않습니다.

 

없을 무!

 

 

 

 

2.2 em.getReference()의 리턴타입은?

 

 

em.getReference() 로 조회한 객체는 어떤 리턴 타입일까요?

 

Member member = new Member();
member.setName("memberA");
em.persist(member);

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

Member savedMember = em.getReference(Member.class, member.getId());
System.out.println(savedMember.getClass());

tx.commit();

 

em.getReference()로 savedMember 객체를 조회해보면, HibernateProxy라고 출력된다.

 

class domain.Member$HibernateProxy$9NuLZsay

 

 

2.3 em.getReference()는 언제 조회 쿼리를 날릴까?

 

그렇다면, em.getReference()로 호출한 savedMember는 언제 DB 조회 쿼리문을 날릴까요?

 

Member member = new Member();
member.setName("memberA");
em.persist(member);

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

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

System.out.println(savedMember.getId());
System.out.println(savedMember.getName());

tx.commit();

 

122
Hibernate: 
    select
        member0_.id as id1_1_0_,
        member0_.age as age2_1_0_,
        member0_.name as name3_1_0_,
        member0_.TEAM_ID as TEAM_ID4_1_0_ 
    from
        Member member0_ 
    where
        member0_.id=?
memberA

 

savedMember.getName()을 할 때 쿼리가 나갑니다.(해당 객체의 칼럼을 조회할 때 쿼리가 나갑니다.) member.getId()는 122를 별도의 쿼리문 없이 바로 조회하는 이유는, Member savedMember = em.getReference(Member.class, member.getId()); 해당 쿼리에서 member.getId()로 id 정보를 알고 있기 때문입니다. 프록시 객체, 가짜 객체 savedMember는 id 이외의 칼럼을 호출할 때, 비로소 DB 조회 쿼리를 날립니다.

 

가짜 객체 조회

 

 em.getReference()로 Proxy를 호출하면, 처음에는 Entity target = null; 입니다. 영속성 컨텍스트에 해당 엔티티가 없어도 실제 DB에 쿼리문을 날리지 않고 null을 가져옵니다. (이후 해당 객체의 칼럼을 조회할 때 쿼리가 나갑니다.)

 

 

 

프록시의 특징


1. 처음 사용할 때 한 번만 초기화된다.

 

member1을 초기화하고 다음에 조회해보면 쿼리가 안나갑니다.

 

Member member = new Member();
member.setName("memberA");
member.setAge(15);
em.persist(member);

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

Member savedMember = em.getReference(Member.class, member.getId());
System.out.println("이름 조회로 프록시 초기화 >>> " + savedMember.getAge());
System.out.println("쿼리 나가는가? >>> " + savedMember.getAge());

tx.commit();

 

Hibernate: 
    select
        member0_.id as id1_1_0_,
        member0_.age as age2_1_0_,
        member0_.name as name3_1_0_,
        member0_.TEAM_ID as TEAM_ID4_1_0_ 
    from
        Member member0_ 
    where
        member0_.id=?
이름 조회로 프록시 초기화 >>> 15
쿼리 나가는가? >>> 15

 

 

2. 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것이 아니라, 프록시 객체로 실제 엔티티를 참조한다. 

 

Member member = new Member();
member.setName("memberA");
em.persist(member);

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

Member savedMember = em.getReference(Member.class, member.getId());
System.out.println("프록시 객체호출 >>> " + savedMember.getClass());
savedMember.getName();
System.out.println("프록시 객체인가? >>> " + savedMember.getClass());

tx.commit();

 

프록시 객체호출 >>> class domain.Member$HibernateProxy$qV2c410u
Hibernate: 
    select
        member0_.id as id1_1_0_,
        member0_.age as age2_1_0_,
        member0_.name as name3_1_0_,
        member0_.TEAM_ID as TEAM_ID4_1_0_,
        team1_.id as id1_3_1_,
        team1_.name as name2_3_1_ 
    from
        Member member0_ 
    left outer join
        Team team1_ 
            on member0_.TEAM_ID=team1_.id 
    where
        member0_.id=?
프록시 객체인가? >>> class domain.Member$HibernateProxy$qV2c410u

 

프록시 객체를 호출하고 칼럼을 조회해서 쿼리를 날리고, 이후에 다시 프록시 객체를 호출해봅니다. 처음에 프록시 객체였으며, 칼럼을 조회하여 프록시 객체가 실제 엔티티로 바뀐다고 착각할 수 있지만, 여전히 프록시 객체입니다.

(한번 프록시 객체면 계속 프록시 객체, 한번 실제 엔티티는 계속 엔티티이다.)

 

 

3. 프록시 객체는 원본 엔티티를 상속받기 때문에, "타입 체크"는 == 대신 instance of를 사용한다.

 

*em.find() 와 em.getReference() 다른 객체 == 비교(false)

 

Member findMember = em.find(Member.class, member1.getId());
Member refMember = em.getReference(Member.class, member2.getId());
System.out.println("== 타입 검사 결과는? " + (findMember.getClass() == refMember.getClass()));
System.out.println("findMember >>> " + findMember.getClass());
System.out.println("refMember >>> " + refMember.getClass());

 

Hibernate: 
    select
        member0_.id as id1_1_0_,
        member0_.age as age2_1_0_,
        member0_.name as name3_1_0_,
        member0_.TEAM_ID as TEAM_ID4_1_0_ 
    from
        Member member0_ 
    where
        member0_.id=?
== 타입 검사 결과는? false
findMember >>> class domain.Member
refMember >>> class domain.Member$HibernateProxy$q91lCaFZ

 

member1 member2를 각각 em.find(), em.getReference() 객체로 참조하고 ==으로 비교합니다.

코드상에서 Member라는 같은 리턴형을 사용하고 있기 때문에, true라고 착각할 수 있지만, getClass()로 비교하면 실제 엔티티 객체와 프록시 객체로 구분이 됩니다. 사용자 입장에서는 판단할 수 없습니다. 따라서 상속받는 부모 클래스 Member와 타입체크를 합니다.

 

 

*em.find()로 같은 Member 객체 == 비교(true)

 

Member findMember1 = em.find(Member.class, member1.getId());
Member findMember2 = em.find(Member.class, member1.getId());
System.out.println("== 타입 검사 결과는? " + (findMember1.getClass() == findMember2.getClass()));
System.out.println("findMember1 >>> " + findMember1.getClass());
System.out.println("findMember2 >>> " + findMember2.getClass());

 

Hibernate: 
    select
        member0_.id as id1_1_0_,
        member0_.age as age2_1_0_,
        member0_.name as name3_1_0_,
        member0_.TEAM_ID as TEAM_ID4_1_0_ 
    from
        Member member0_ 
    where
        member0_.id=?
== 타입 검사 결과는? true
findMember1 >>> class domain.Member
findMember2 >>> class domain.Member

 

 

*em.find()로 다른 Member 객체 == 비교(true)

 

Member findMember1 = em.find(Member.class, member1.getId());
Member findMember2 = em.find(Member.class, member2.getId());
System.out.println("== 타입 검사 결과는? " + (findMember1.getClass() == findMember2.getClass()));
System.out.println("findMember1 >>> " + findMember1.getClass());
System.out.println("findMember2 >>> " + findMember2.getClass());

 

Hibernate: 
    select
        member0_.id as id1_1_0_,
        member0_.age as age2_1_0_,
        member0_.name as name3_1_0_,
        member0_.TEAM_ID as TEAM_ID4_1_0_ 
    from
        Member member0_ 
    where
        member0_.id=?
== 타입 검사 결과는? true
findMember1 >>> class domain.Member
findMember2 >>> class domain.Member

 

 

*em.getReference()로 같은 Member 객체 == 비교(true)

 

Member refMember1 = em.getReference(Member.class, member1.getId());
Member refMember2 = em.getReference(Member.class, member1.getId());
System.out.println("== 타입 검사 결과는? " + (refMember1 == refMember2));
System.out.println("refMember1 >>> " + refMember1.getClass());
System.out.println("refMember2 >>> " + refMember2.getClass());

 

Hibernate: 
    /* insert domain.Member
        */ insert 
        into
            Member
            (age, name, TEAM_ID, id) 
        values
            (?, ?, ?, ?)
== 타입 검사 결과는? true
refMember1 >>> class domain.Member$HibernateProxy$abMLqX3Y
refMember2 >>> class domain.Member$HibernateProxy$abMLqX3Y

 

em.getReference(), em.getReference()로 같은 객체를 호출하면 m1==m2 true

 

 

*em.getReference()로 다른 Member 객체 == 비교(false)

 

Member refMember1 = em.getReference(Member.class, member1.getId());
Member refMember2 = em.getReference(Member.class, member2.getId());
System.out.println("== 타입 검사 결과는? " + (refMember1 == refMember2));
System.out.println("refMember1 >>> " + refMember1.getClass());
System.out.println("refMember2 >>> " + refMember2.getClass());

 

Hibernate: 
    /* insert domain.Member
        */ insert 
        into
            Member
            (age, name, TEAM_ID, id) 
        values
            (?, ?, ?, ?)
== 타입 검사 결과는? false
refMember1 >>> class domain.Member$HibernateProxy$pF5g3eVM
refMember2 >>> class domain.Member$HibernateProxy$pF5g3eVM

 

같은 em.find()끼리, 같은 em.getReference()끼리 "==" 을 비교하면 무조건 true입니다.

em.find()는 상관없지만, em.getReference()는 다른 객체인 경우 "==" 비교가 false입니다.

em.find()em.getReference()는 같은 엔티티인 경우 "==" 비교가 false입니다.

 

 

 

 

4. 영속성 컨텍스트에 찾는 엔티티가 이미 있으면, em.getReference()는 실제 엔티티를 반환한다.

 

(반대로, em.getReference()로 이미 프록시 객체가 있으면 em.find()는 프록시 객체를 반환한다.)

 

JPA에서는 한 영속성 컨텍스트 안에서 PK가 같으면 ==으로 비교할 때, 무조건 true로 동일성을 보장합니다. 한 트랜잭션 안에서 영속성 컨텍스트의 동일성을 보장하기 때문에, 실제 엔티티가 객체가 있으면 em.getReference()를 호출해도 실제 엔티티 객체이고, 프록시 객체가 있으면 em.find()를 호출해도 프록시 객체입니다.

 

 

*em.find() 이후 em.getReference() 호출

 

Member findMember = em.find(Member.class, member1.getId());
Member refMember = em.getReference(Member.class, member1.getId());
System.out.println("== 타입 검사 결과는? " + (findMember == refMember));
System.out.println("findMember >>> " + findMember.getClass());
System.out.println("refMember >>> " + refMember.getClass());

 

Hibernate: 
    select
        member0_.id as id1_1_0_,
        member0_.age as age2_1_0_,
        member0_.name as name3_1_0_,
        member0_.TEAM_ID as TEAM_ID4_1_0_ 
    from
        Member member0_ 
    where
        member0_.id=?
== 타입 검사 결과는? true
findMember >>> class domain.Member
refMember >>> class domain.Member

 

 

*em.getReference(), 초기화 이후 em.find() 호출

 

Member refMember = em.getReference(Member.class, member1.getId());
refMember.getName();
Member findMember = em.find(Member.class, member1.getId());
System.out.println("== 타입 검사 결과는? " + (refMember == findMember));
System.out.println("refMember >>> " + refMember.getClass());
System.out.println("findMember >>> " + findMember.getClass());

 

Hibernate: 
    select
        member0_.id as id1_1_0_,
        member0_.age as age2_1_0_,
        member0_.name as name3_1_0_,
        member0_.TEAM_ID as TEAM_ID4_1_0_ 
    from
        Member member0_ 
    where
        member0_.id=?
== 타입 검사 결과는? true
refMember >>> class domain.Member$HibernateProxy$6q9fRcbJ
findMember >>> class domain.Member$HibernateProxy$6q9fRcbJ

 

em.getReference() 로 먼저 호출한다면, em.find()로 호출한다고 해도 무조건 프록시 객체입니다.

반대로, em.find()를 먼저 호출하고, em.getReference()를 호출해도 무조건 실제 엔티티 객체입니다.

왜냐하면 영속성 컨텍스트 내에서 같은 객체에 대해서 "=="이 true인 동일성을 보장해야 하기 때문입니다.

 

 

 

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

(하이버네이트는 org.hibernate.LazyInitializationException 예외를 터트림)

 

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

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

System.out.println(refMember.getName());

 

org.hibernate.LazyInitializationException: could not initialize proxy [domain.Member#212] - no Session
	at org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:169)
	at org.hibernate.proxy.AbstractLazyInitializer.getImplementation(AbstractLazyInitializer.java:309)
	at org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor.intercept(ByteBuddyInterceptor.java:45)
	at org.hibernate.proxy.ProxyConfiguration$InterceptorDispatcher.intercept(ProxyConfiguration.java:95)
	at domain.Member$HibernateProxy$0iX1PxHa.getName(Unknown Source)
	at ProxyMain2.main(ProxyMain2.java:34)

 

원래는 트랜잭션과 영속성 컨텍스트의 생명주기를 맞추지만, 혹여나 영속성 컨텍스트에서 프록시 객체를 detach() 하거나 clear()해서 생명주기가 먼저 끝난다면, 이후 조회하는 프록시 객체는 더이상 관리되지 않기 때문에, no session 에러가 납니다.

 

 

* LAZY  전략 VS EAGER 전략 쿼리문 비교하기

 

LAZY와 EAGER가 어떻게 작동하는지, 이 때 프록시 객체는 어떻게 동작하는지 살펴보겠습니다

 

 

* Team 객체 조회하기

 

Team team = new Team();
team.setName("teamA");
em.persist(team);

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

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

Member savedMember = em.find(Member.class, member.getId());
Team savedTeam = savedMember.getTeam();

tx.commit();

 

Member 엔티티와, Team 엔티티를 생성하고 member 객체에 teamA를 추가합니다.

그리고, em.find()로 Member 객체로 시작해 Team 객체를 호출합니다.

 

 

1.LAZY 전략

 

Hibernate: 
    select
        member0_.id as id1_1_0_,
        member0_.age as age2_1_0_,
        member0_.name as name3_1_0_,
        member0_.TEAM_ID as TEAM_ID4_1_0_ 
    from
        Member member0_ 
    where
        member0_.id=?

 

 LAZY 전략은, Team 엔티티를 프록시 객체로 호출하므로, 실제 Team 객체의 칼럼이 조회되기 전까지 Team과 관련한 쿼리문이 나가지 않습니다. 실제 team 객체의 칼럼을 호출 할 때 쿼리문이 나갑니다.

 

 

2. EAGER 전략

 

Hibernate: 
    select
        member0_.id as id1_1_0_,
        member0_.age as age2_1_0_,
        member0_.name as name3_1_0_,
        member0_.TEAM_ID as TEAM_ID4_1_0_,
        team1_.id as id1_3_1_,
        team1_.name as name2_3_1_ 
    from
        Member member0_ 
    left outer join
        Team team1_ 
            on member0_.TEAM_ID=team1_.id 
    where
        member0_.id=?

 

EAGER의 경우, Team 객체를 바로 영속성 컨텍스트에 조회하기 때문에 join과 관련한 쿼리가 발생합니다.

 

 

* Member 객체 조회하기

 

Team team = new Team();
team.setName("teamA");
em.persist(team);

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

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

Member savedMember = em.find(Member.class, member.getId());
System.out.println(savedMember);

tx.commit();

 

Team 엔티티를 전혀 조회하지 않고 Member 엔티티만 조회합니다. 주의 할 것은, Member와 연관관계에 있는 Team 객체도 getter 함수로 조회하여 자동으로 호출된다는 것입니다.

 

 

1. LAZY 전략

 

Hibernate:     
    select
        member0_.id as id1_1_0_,
        member0_.age as age2_1_0_,
        member0_.name as name3_1_0_,
        member0_.TEAM_ID as TEAM_ID4_1_0_ 
    from
        Member member0_ 
    where
        member0_.id=?
Hibernate: 
    select
        team0_.id as id1_3_0_,
        team0_.name as name2_3_0_ 
    from
        Team team0_ 
    where
        team0_.id=?
Member{id=121, name='memberA', age=0, team=Team{id=120, name='teamA'}}

 

 Member 엔티티 뿐만 아니라, Team 엔티티를 getter로 호출하기 때문에, 바로 쿼리문이 하나 더 추가되었습니다.

 

 

2.EAGER 전략

 

Hibernate: 
    select
        member0_.id as id1_1_0_,
        member0_.age as age2_1_0_,
        member0_.name as name3_1_0_,
        member0_.TEAM_ID as TEAM_ID4_1_0_,
        team1_.id as id1_3_1_,
        team1_.name as name2_3_1_ 
    from
        Member member0_ 
    left outer join
        Team team1_ 
            on member0_.TEAM_ID=team1_.id 
    where
        member0_.id=?
Member{id=119, name='memberA', age=0, team=Team{id=118, name='teamA'}}

 

EAGER는 한번에 영속성 컨텍스트로 모두 조회하기 때문에, Member와 Team 엔티티가 한번에 조회됩니다.

 

 

* team 객체 이름 출력하기

 

Team team = new Team();
team.setName("teamA");
em.persist(team);

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

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

Member savedMember = em.find(Member.class, member.getId());
System.out.println(savedMember.getTeam().getClass());

System.out.println(savedMember.getTeam().getName());

tx.commit();

 

Member 엔티티를 참조해 Team 엔티티의 클래스명과, 이름을 출력합니다.

 

 

1. LAZY 전략의 경우

 

Hibernate: 
    select
        member0_.id as id1_1_0_,
        member0_.age as age2_1_0_,
        member0_.name as name3_1_0_,
        member0_.TEAM_ID as TEAM_ID4_1_0_ 
    from
        Member member0_ 
    where
        member0_.id=?
class domain.Team$HibernateProxy$7xxb9Ynt
Hibernate: 
    select
        team0_.id as id1_3_0_,
        team0_.name as name2_3_0_ 
    from
        Team team0_ 
    where
        team0_.id=?
teamA

 

LAZY이므로 연관관계에 있는 Team 객체는 프록시 객체로 조회됩니다. 프록시 객체라는 것을 조회할 때, Team 객체의 칼럼명이 조회되지 않았기 때문에 쿼리가 발생하지 않습니다. 그 이후, 칼럼인 이름을 호출했기 때문에 조회 쿼리가 나갑니다.

 

 

2. EAGER 전략의 경우

 

Hibernate: 
    select
        member0_.id as id1_1_0_,
        member0_.age as age2_1_0_,
        member0_.name as name3_1_0_,
        member0_.TEAM_ID as TEAM_ID4_1_0_,
        team1_.id as id1_3_1_,
        team1_.name as name2_3_1_ 
    from
        Member member0_ 
    left outer join
        Team team1_ 
            on member0_.TEAM_ID=team1_.id 
    where
        member0_.id=?
class domain.Team
teamA

 

EAGER인 경우, 바로 영속성 컨텍스트에 조회합니다. 따라서, Team 엔티티 그대로 조회되며, 쿼리는 1번만 나갑니다.

 

 

* team 클래스명, 이름, 다시 클래스명 출력하기

 

Team team = new Team();
team.setName("teamA");
em.persist(team);

Member member1 = new Member();
member1.setName("paik");
member1.setAge(28);
member1.setTeam(team);
em.persist(member1);

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

Member findMember = em.find(Member.class, member1.getId());
System.out.println("Team class 호출 : " + findMember.getTeam().getClass());

System.out.println("=======================");
System.out.println(findMember.getTeam());
System.out.println(findMember.getTeam().getName());
System.out.println("=======================");

System.out.println("Team class 호출 : " + findMember.getTeam().getClass());

tx.commit();

 

위의 테스트에 추가해서, 칼럼을 조회하고 다시 클래스명을 출력해봅니다. 결과는 어떨까요?

 

 

1. LAZY 전략

 

Hibernate: 
    select
        member0_.id as id1_0_0_,
        member0_.age as age2_0_0_,
        member0_.name as name3_0_0_,
        member0_.TEAM_ID as TEAM_ID4_0_0_ 
    from
        Member member0_ 
    where
        member0_.id=?
Team class 호출 : class domain.Team$HibernateProxy$JFlBHSXj
=======================
Hibernate: 
    select
        team0_.id as id1_1_0_,
        team0_.name as name2_1_0_ 
    from
        Team team0_ 
    where
        team0_.id=?
Team{id=55, name='teamA'}
teamA
=======================
Team class 호출 : class domain.Team$HibernateProxy$JFlBHSXj

 

 LAZY로 설정하면, em.find()로 호출 했을 때, Proxy 객체로 Team을 가지고 옵니다. 따라서 member로 Team을 조회할 때 Proxy에서 객체를 참조하기 위해 쿼리가 발생한다. 또한, Team이 출력되었다고 해서 Team 프록시가 실제 엔티티로 영속성 컨테스트에 저장되지 않습니다. 한번 프록시는 같은 트랜잭션 내에서 계속 같은 프록시객체입니다.

 

 

2. EAGER 전략

 

Hibernate: 
    select
        member0_.id as id1_0_0_,
        member0_.age as age2_0_0_,
        member0_.name as name3_0_0_,
        member0_.TEAM_ID as TEAM_ID4_0_0_,
        team1_.id as id1_1_1_,
        team1_.name as name2_1_1_ 
    from
        Member member0_ 
    left outer join
        Team team1_ 
            on member0_.TEAM_ID=team1_.id 
    where
        member0_.id=?
Team class 호출 : class domain.Team
=======================
Team{id=57, name='teamA'}
teamA
=======================
Team class 호출 : class domain.Team

 

EAGER로 설정하면, em.find()로 호출 했을 때 Team 엔티티가 영속성 컨텍스트에 같이 조회됩니다. 따라서 따로 쿼리도 나가지 않습니다.

 

 

Member를 조회하는데, 꼭 Team내부의 정보를 알 필요성이 있을까요?

=> 한번에 조인해서 가지고 오느냐, 아니면 최대한 미루다가 따로 조회쿼리를 날리느냐 차이 일 뿐입니다.

 

 

JPA 사용 주의점


1. 가급적 지연 로딩만 사용한다.

 

예상치 못한 SQL이 발생하기 때문입니다. EAGER이면, 내가 Member만 조회한다고 명시적으로는 썼어도 연관되어있는 Team까지도 자동으로 조회가 됩니다. 또한 연관관계가 적게 있으면 자동으로 같이 조회되는 엔티티가 적어서 상관이 없지만, 다른 연관관계가 늘어나면 join이 엄청나게 엄청나게 길어져 성능이 나빠집니다.

 

 

2. 즉시 로딩은 즉시 N + 1 문제를 발생시킨다.

 

JPQL은 실무에서 많이 사용하기 때문에 JPQL과 충돌하는 것을 주의해야 합니다.

 

Team team = new Team();
team.setName("teamA");
em.persist(team);

Member member1 = new Member();
member1.setName("paik");
member1.setAge(28);
member1.setTeam(team);
em.persist(member1);

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

List<Member> members = em.createQuery("select m From Member m", Member.class)
	.getResultList();
tx.commit();

 

em.createQuery() 문법이 JPQL입니다. JPQL은 가장 먼저 SQL 쿼리로 "select * from member"로 변환됩니다. 그런데, EAGER 문법이기 때문에 Member뿐만 아니라 Team도 "즉시"로딩을 합니다. "select * from Team where team_id = ?"의 쿼리가 추가적으로 나갑니다. JPQL로 Member의 리스트를 조회하기 때문에, Team 조회 쿼리는 Member의 갯수만큼 나가게됩니다. 이것이 N+1 문제입니다. 즉, 최초 Member를 조회하는 쿼리 1개에 Member의 갯수만큼 N번의 쿼리가 나가서 N+1입니다.

 

Hibernate: 
    /* select
        m 
    From
        Member m */ select
            member0_.id as id1_0_,
            member0_.age as age2_0_,
            member0_.name as name3_0_,
            member0_.TEAM_ID as TEAM_ID4_0_ 
        from
            Member member0_
Hibernate: 
    select
        team0_.id as id1_1_0_,
        team0_.name as name2_1_0_ 
    from
        Team team0_ 
    where
        team0_.id=?
Hibernate: 
    select
        team0_.id as id1_1_0_,
        team0_.name as name2_1_0_ 
    from
        Team team0_ 
    where
        team0_.id=?
Hibernate: 
    select
        team0_.id as id1_1_0_,
        team0_.name as name2_1_0_ 
    from
        Team team0_ 
    where
        team0_.id=?

 

현재 Team만 EAGER이지만 EAGER인 연관관계가 여러개 있다면?

 

=> N개만큼의 추가적인 쿼리가 발생하는 N+1문제가 발생하는 N + 1 문제에 더해서 MN + 1 문제가 발생합니다. 즉, EAGER는 의도치 않은 쿼리조회가 늘어납니다.

 

=>@ManyToOne, @OneToOne은 mappedBy 기본이 EAGER이다.

(@OneToMany, @ManyToMany는 기본이 LAZY이다.)

 

 

대안 : FETCH JOIN

 

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_.id as id1_0_0_,
            team1_.id as id1_1_1_,
            member0_.age as age2_0_0_,
            member0_.name as name3_0_0_,
            member0_.TEAM_ID as TEAM_ID4_0_0_,
            team1_.name as name2_1_1_ 
        from
            Member member0_ 
        inner join
            Team team1_ 
                on member0_.TEAM_ID=team1_.id

 

List<Member> 를 JPQL로 조회해도 JOIN FETCH를 이용하면 쿼리 한번에 Member와 Team 모두 조회할 수 있습니다. Member와 Team을 for루프로 조회해도 더이상 쿼리가 나가지 않습니다.

 

 

* 참고

 

자바 ORM 표준 JPA 프로그래밍 - 기본편

반응형

'Spring 정리 > Spring JPA' 카테고리의 다른 글

N + 1 문제  (0) 2021.09.30
영속성 전이(CASCADE)와 고아객체  (0) 2021.09.14
N:M 테이블 관계 설계하기  (0) 2021.09.13
연관관계 매핑  (0) 2021.09.10
N:1 테이블 관계 설계하기  (0) 2021.09.07