본문 바로가기
☕️자바 𝗝𝗔𝗩𝗔

[JPA] 즉시로딩, 지연로딩 (JPA N+1문제)

by 비타민찌 2022. 9. 22.
728x90

앞서 프록시 포스팅에서 말했던 지연로딩에 대해 더 자세히 알아보자.

 

Team과 Member가 일대다로 연관관계를 갖고있을 때 Member를 조회할 때 마다 해당 Member의 Team도 조회해할 필요가 없다면, 이 지연로딩을 사용하면 된다. 지연로딩은 실제로 Member를 통해 Team에 대한 정보를 읽어와야할 때까지 DB에서 조회를 미루는 개념이다. Member를 조회 할때 Team객체에 프록시 객체를 넣어오고, getTeam 처럼 실제로 team을 호출해야할 때 초기화가 이뤄진다.

지연로딩으로 설정해주기 위해서는 아래와 같이 간단하게 fetch옵션을 LAZY로 설정해주면 된다.

public class Member {

    @ManyToOne(fetch = FetchType.LAZY) // 다대일(member관점) 프록시객체로 조회
    @JoinColumn(name= "TEAM_ID") // FK 명시
    private Team team;
    
    }

 

반대의 경우 (즉시로딩. Member와 Team을 자주 같이 조회해야하는 경우)에는 fetch옵션을 EAGER로 설정해주면 된다.

 

아래 코드로 즉시로딩과 지연로딩을 비교해보자.

Member member = new Member();
member.setUsername("a");

Team team = new Team();
team.setName("b");
member.setTeam(team);

em.persist(member);
em.persist(team);

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


Member findMember = em.find(Member.class, member.getId());
System.out.println("member = " + findMember.getClass()); // 멤버객체
System.out.println("team = " + findMember.getTeam().getClass()); // 프록시객체
System.out.println("team = " + findMember.getTeam().getName()); // 초기화하면서 select문

 

(1) 즉시로딩(EAGER)

@Entity
public class Member {

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

 

지연로딩 실행 코드

Member member = em.find(Member.class, "member1");
Team team = member.getTeam();   // 객체 그래프 탐색

회원과 팀, 두 테이블을 조회해야 하므로 쿼리를 2번 실행할 것 같지만 쿼리를 보면

JPA에서 즉시로딩을 최적화 해주기 때문에 join으로 team을 가져온다.

 

(2) 지연로딩(LAZY)

@Entity
public class Member {
    //...
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "TEAM_ID")
    private Team team;
    //...
}
Member member = em.find(Member.class, "member1");
Team team = member.getTeam();   //객체 그래프 탐색
team.getName();                 //실제 사용 순간

지연로딩시에는 join을 안한다.

// em.find(Member.class, "member1");
SELECT * FROM MEMBER
WHERE MEMBER_ID = 'member1'

// team.getName() 호출
SELECT * FROM TEAM
WHERE TEAM_ID = 'team1'

 

정리하자면

지연 로딩(LAZY): 연관된 엔티티를 프록시로 조회. 프록시를 실제 사용할 때 초기화하면서 데이터베이스를 조회.

즉시 로딩(EAGER): 연관된 엔티티를 즉시 조회. 하이버네이트는 가능하면 SQL 조인으로 한번에 조회.

 

이렇게보면 차이가 크지 않은 것 같지만.. 비교를 위해 JPQL도 사용해보자.

 Member member = new Member();
 member.setUsername("a");

Team team = new Team();
team.setName("b");
member.setTeam(team);

em.persist(member);
em.persist(team);


em.flush();
em.clear();
            
List<Member> members = em.createQuery("select m from Member m", Member.class).getResultList();
System.out.println(members);

 

즉시로딩(EAGER)

JPQL은 전달한 쿼리 그대로 만들어서 나가기 때문에 JPA의 최적화를 거치지 않는다. 따라서 전체조회를 위해 Member에 select문 한번, 즉시로딩을 위해 그 멤버의 team의 값을 가져오기 위해 select문 1번 총 2번의 쿼리를 날리게 된다. 지금은 member가 적지만 만약 멤버가 많아지고 모두 다른 team에 속해있다면 N + 1의 문제가 심각하게 발생할 수 있다.

N+1 문제란 Member 전체조회를 했을때 날리는 하나의 쿼리(select * from Member m) 때문에 N개의 추가 쿼리 N개가(각각 member의 team을 가져오는 select문) 발생하는 문제다.

ibernate: 
 	select
        	....
 	from
        	Member member0_
       
Hibernate: 
    select
        team0_.TEAM_ID as team_id1_7_0_,
        team0_.name as name2_7_0_ 
    from
        Team team0_ 
    where
        team0_.TEAM_ID=?

 

지연로딩(LAZY)

지연로딩시에는 예상대로 한번의 쿼리만 나간다.

Hibernate: 
 	select
        	  ....
	 from
                  Member member0_

* 책에서는 상황에 따라 즉시로딩, 지연로딩을 사용하라고 되어있지만 인프런 기본편 강의에는 가급적 모든 연관관계에 지연로딩을 default로 설정하고, 정말 필요할 때만 즉시로딩으로 바꿔줘야 바람직하다고 하신다.ㅎㅎ [엔티티 설계 시 주의점]

 

 

참고 강의

 

728x90

댓글