JAVA/JPA 학습기록

[JPA] 실전예제 5_지연로딩(FetchType.lazy) 적용

kth990303 2021. 5. 15. 14:27
반응형

오랜만에 올리는 JPA 포스팅이다.

지연로딩과 Cascade에 관한 내용을 자꾸 까먹어 나중에 복습용으로 보기 위해 포스팅으로 올린다.


인프런 김영한님의 강의를 듣고

스스로 요약 및 복습용으로 기록한 포스팅입니다.

틀린 내용은 댓글로 피드백 부탁드립니다 :)


실전예제 5 는 연관관계를 관리하는 방법에 대해 배운 내용을 적용하는 예제이다.

그 방법에는 아래와 같은 내용이 있다.

  1. 프록시(Proxy)를 이용하는 지연로딩(fetch = FetchType.LAZY) 적용 (이번 포스팅)
  2. 영속성 전이(cascade=CascadeType.ALL) 적용

이번에는 그 중 첫번째인 지연로딩에 대해 공부해보려 한다.

 

프록시에 대한 내용은 아래 블로그에서 볼 수 있다.

 

(기본 내용으로 보기 좋은 포스팅)

https://ict-nroo.tistory.com/131

 

[JPA] 프록시란?

프록시 질문으로 부터 프록시에 대한 학습을 시작한다. Member 엔티티를 조회할 때 Team도 함께 조회해야 할까? 실제로 필요한 비즈니스 로직에 따라 다르다. 비즈니스 로직에서 필요하지 않을 때

ict-nroo.tistory.com

(요약 및 정리본으로 참고하기 좋은 포스팅)

https://data-make.tistory.com/629

 

[JPA] 엔티티 비교, 프록시, 성능 최적화

| 엔티티 비교 -- || 영속성 컨텍스트가 같을 때 엔티티 비교 * 동일성(identical) : == 비교가 같다. * 동등성(equinalent) : equals() 비교가 같다. * 데이터베이스 동등성 : @Id인 데이터베이스 식별자가 같다

data-make.tistory.com


지연로딩이란 무엇일까?

Member 엔티티와 Team 엔티티가 있다고 하자.

Member와 Team은 N:1 연관관계 매핑이 돼있다고 하자.

그리고 현재 요구사항은 member에게 team을 부여는 하지만, 그 team에 대한 정보는 딱히 소용이 없다고 가정해보자.

(더 좋은 가정이 있을 법도 한데...)

 

그럼 우리는 아래와 같이 코드를 짤 것이다.

package hellojpa;
import javax.persistence.*;
@Entity
public class Member1 extends BaseEntity{
    @Id @GeneratedValue
    @Column(name="MEMBER_ID")
    private Long id;

    @Column(name="USERNAME")
    private String username;

    @ManyToOne
    @JoinColumn(name="TEAM_ID")
    private Team team;
}

Member 코드는 위와 같이 번호, 이름, 소속 팀을 필드로 가지고 있을 것이고,

package hellojpa;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
public class Team extends BaseEntity{
    @Id @GeneratedValue
    @Column(name="TEAM_ID")
    private Long id;
    private String name;
}

Team 코드는 위와 같이 팀번호, 팀이름을 필드로 가지고 있을 것이다. (단방향 매핑으로 한다고 가정하자. 만약, 소속 팀에 속한 멤버 명단을 출력하고 싶다면 양방향 매핑으로 바꾸기만 하면 된다.)

 

이제 메인을 실행해보자.

메인은 아래와 같이 해보자.

package hellojpa;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import java.time.LocalDateTime;
import java.util.List;
public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

        EntityManager em=emf.createEntityManager();

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

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

            Member1 m=new Member1();
            m.setUsername("kth990303");
            m.setTeam(team);
            em.persist(m);

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

            Member1 member = em.find(Member1.class, m.getId());
            System.out.println("member.getTeam().getClass() = " + member.getTeam().getClass());

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

멤버 kth990303의 소속팀은 TeamA이다.

소속팀 엔티티에서 실행해야 할 메소드는 하나도 없다. (getClass()는 Team 클래스의 메소드가 아니다.)

 

메인 메소드 실행 결과를 쿼리로 확인하기 위해 em.flush(), em.clear()로 영속성 컨텍스트를 비워주고 db에 저장시킨 후,결과를 지켜보자.

 

위 메인메소드를 실행하면 결과는 아래와 같다.

Hibernate: 
    select
        member1x0_.MEMBER_ID as member_i1_4_0_,
        member1x0_.createdBy as createdb2_4_0_,
        member1x0_.createdDate as createdd3_4_0_,
        member1x0_.lastModifiedBy as lastmodi4_4_0_,
        member1x0_.lastModifiedDate as lastmodi5_4_0_,
        member1x0_.TEAM_ID as team_id7_4_0_,
        member1x0_.USERNAME as username6_4_0_,
        team1_.TEAM_ID as team_id1_6_1_,
        team1_.createdBy as createdb2_6_1_,
        team1_.createdDate as createdd3_6_1_,
        team1_.lastModifiedBy as lastmodi4_6_1_,
        team1_.lastModifiedDate as lastmodi5_6_1_,
        team1_.name as name6_6_1_ 
    from
        Member1 member1x0_ 
    left outer join
        Team team1_ 
            on member1x0_.TEAM_ID=team1_.TEAM_ID 
    where
        member1x0_.MEMBER_ID=?
member.getTeam().getClass() = class hellojpa.Team

뭐, 나쁘지 않은 결과이다.

 

하이버네이트가 member, team에 대해 select 쿼리문을 날린 후,

join을 통해 kth990303이 teamA에 속함을 매핑해주었다.

팀의 getClass() 결과는 당연히 내가 만들어두었던 hellojpa.Team이 나올 것이다.

 

그런데, 생각해보면 

우리는 team에 관련된 쿼리문이 지금은 딱히 필요가 없는 상황이다.

member.getTeam() 정도는 member에서 teamid를 select하는 쿼리문 정도로 충분히 해결 가능(즉, 프록시에서 충분히 확인 가능)하고, 

처음부터 한꺼번에 로딩을 해놓을 필요는 없는 상황이라는 것이다.

 

따라서 우리는 지연로딩으로 바꿔서 실행할 것이다.

어떻게 바꾸냐?

@ManyToOne, @OneToOne 과 같이 연관관계 매핑에서 One으로 끝나는 어노테이션들의 기본형은 모두 즉시로딩(Eager)형이기 때문에, 이 어노테이션 옆에 (fetch = FetchType.LAZY)와 같이 추가할 것이다. (@OneToMany, @ManyToMany는 기본형이 지연로딩이라고 한다.)

 

public class Member1 extends BaseEntity{
	...
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name="TEAM_ID")
    private Team team;
}

위와 같이 추가한 후, 메인을 실행해보면 아래 결과와 같다.

Hibernate: 
    select
        member1x0_.MEMBER_ID as member_i1_4_0_,
        member1x0_.createdBy as createdb2_4_0_,
        member1x0_.createdDate as createdd3_4_0_,
        member1x0_.lastModifiedBy as lastmodi4_4_0_,
        member1x0_.lastModifiedDate as lastmodi5_4_0_,
        member1x0_.TEAM_ID as team_id7_4_0_,
        member1x0_.USERNAME as username6_4_0_ 
    from
        Member1 member1x0_ 
    where
        member1x0_.MEMBER_ID=?
member.getTeam().getClass() = class hellojpa.Team$HibernateProxy$kebEI2Fi

team에 대한 메소드가 필요가 없어서

하이버네이트가 join 또한 하지 않고, member에 대한 select 쿼리문만 날린 것을 볼 수 있다!

또한, member.getTeam().getClass() 가 hellojpa.Team이 아닌, 그에 대한 프록시임을 알 수 있다.

 

우선 프록시에 kth990303의 팀을 담아놓고,

team 클래스의 메소드가 쓰이는 순간, 프록시에서 초기화가 일어나 db에 정보를 담고 team에 대한 쿼리문이 날라갈 것이다.

 

한 번 메인 메소드를 아래와 같이 추가해보자.

 System.out.println("=============1");
 Team findTeam = member.getTeam();
 System.out.println("=============2");
 String name = findTeam.getName();
 System.out.println("name = " + name);
 System.out.println("=============3");
 
 tx.commit();

team에 대한 쿼리는

========1과 ========2 사이에 나타날까?

========2과 ========3 사이에 나타날까?

아니면 아예 나타나지 않을까?

 

결과는 아래와 같다.

Hibernate: 
    select
        member1x0_.MEMBER_ID as member_i1_4_0_,
        member1x0_.createdBy as createdb2_4_0_,
        member1x0_.createdDate as createdd3_4_0_,
        member1x0_.lastModifiedBy as lastmodi4_4_0_,
        member1x0_.lastModifiedDate as lastmodi5_4_0_,
        member1x0_.TEAM_ID as team_id7_4_0_,
        member1x0_.USERNAME as username6_4_0_ 
    from
        Member1 member1x0_ 
    where
        member1x0_.MEMBER_ID=?
member.getTeam().getClass() = class hellojpa.Team$HibernateProxy$QsNlRlMu
=============1
=============2
Hibernate: 
    select
        team0_.TEAM_ID as team_id1_6_0_,
        team0_.createdBy as createdb2_6_0_,
        team0_.createdDate as createdd3_6_0_,
        team0_.lastModifiedBy as lastmodi4_6_0_,
        team0_.lastModifiedDate as lastmodi5_6_0_,
        team0_.name as name6_6_0_ 
    from
        Team team0_ 
    where
        team0_.TEAM_ID=?
name = teamA
=============3

========1과 ========2 사이에는 member.getTeam() 메소드가 있는데, 이는 member에서 select 쿼리를 날리는 것만으로도 충분히 파악이 가능하다. 

========2과 ========3 사이에 findTeam에서의 getName()메소드가 사용되므로 이 때 team에 대해 select 쿼리를 날림을 확인할 수 있다.


지연로딩, 왜 쓰는 걸까?

실무에서 이렇게 모든 쿼리를 조회할 필요가 없는 경우가 상당히 많다고 한다.

실무에서는 테이블이 수천 개 이상으로 굉장히 많다고 한다.

여기서 즉시 로딩으로 적용한다면, 최악의 경우 테이블 N개일 때, join이 N^2번 일어나 엄청나게 많은 쿼리문을 날려 속도가 굉장히 느릴 수 있다. 당연히 이렇게 많은 정보를 한꺼번에 조회하는 경우는 거의 없다. member가 어떤 item을 샀는지 조회, member가 어느 rank인지 조회 등등 각각 필요한 상황에서만 쓰이는 경우가 대부분이다. 즉, 지연로딩으로 충분히 처리가 가능하며, 속도 향상 또한 지연로딩 방법이 훨씬 유리하다.

따라서 불필요한 sql 쿼리를 최대한 줄이기 위해 위와 같은 지연로딩을 거의 항상 사용한다고 한다.

 

또한, 즉시로딩(기본형, fetch= FetchType.Eager)은 JPQL에서 N+1 문제를 일으킨다.

N+1 문제란, 최초 쿼리 1개에, 불필요한 join으로 인한 추가 쿼리 N개를 말하는 것이다.

예를 들어 select m from Member1 m과 같은 JPQL queryString을 짰다고 하자.

난 분명 Member에 대한 정보만 필요해서 member만 select 했는데, team도 모두 select가 되고, item도, rank도 모두 select가 돼버리는 상황이 발생할 것이다.

 

따라서 이러한 상황을 방지하기 위해, 그리고 불필요한 속도 지연을 방지하기 위해 지연로딩을 사용한다.


그 외에 JPQL fetch join, Entity Graph 기능을 사용하여 지연로딩을 활용하는 방법이 있다는데,

아직 배우지 않아 잘 모르겠다.

백준으로 알고리즘 공부할 것도 많은데, JPA도 공부할 것이 산더미이다.

전역하기 전에 빨리 JPA 활용편, Spring Boot 활용 완강하고 Spring 프레임워크와 jpa에 익숙해지고 싶다.

(물론 전역 전까지 최우선순위는 백준을 통한 알고리즘 공부이다 ㅎㅎ)

 

반응형