즉시 로딩과 지연 로딩

PPakSSam·2022년 1월 20일
0
post-thumbnail

Member를 조회할 때 꼭 Team도 함께 조회해야 할까?

단순히 Member 정보만 사용하는 비즈니스 로직에서 Team을 조회할 필요는 없지 않나?
이런 질문에서 시작되는 개념이 즉시 로딩과 지연로딩이다.

지연 로딩 LAZY를 사용해서 프록시로 조회

@Entity
public class Member {

    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

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

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

em.find()를 실행하면 member객체가 반환되며,
member.getTeam() 객체는 Team객체가 아닌 프록시 객체가 들어가게 된다.

Team team = new Team();
team.setName("TeamA");

em.persist(team);

Member member = new Member();
member.setUsername("member1");
member.setTeam(team);

em.persist(member);

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

Member findMember = em.find(Member.class, member.getId()); // (1)
Team findTeam = findMember.getTeam();
System.out.println(findTeam.getClass()); // class hellojpa.Team$HibernateProxy$SfG9Y9qu
System.out.println(emf.getPersistenceUnitUtil().isLoaded(findTeam)); // false

findTeam.getName(); // (2) 실제 team을 사용하는 시점에서 초기화 
System.out.println(emf.getPersistenceUnitUtil().isLoaded(findTeam)); // true

위의 (1) 코드가 실행될 때 나오는 쿼리는 다음과 같다.

select
        member0_.MEMBER_ID as member_i1_2_0_,
        member0_.TEAM_ID as team_id3_2_0_,
        member0_.USERNAME as username2_2_0_ 
    from
        Member member0_ 
    where
        member0_.MEMBER_ID=?

보면 알 수 있듯이 Team을 같이 조회하지 않음을 알 수 있다.

위의 (2) 코드가 실행될 때 나오는 쿼리는 다음과 같다.

select
        team0_.TEAM_ID as team_id1_4_0_,
        team0_.NAME as name2_4_0_ 
    from
        Team team0_ 
    where
        team0_.TEAM_ID=?

findTeam.getName()을 하는 시점 team의 정보를 필요로 하는 시점에서야 조회를 한다.
이 때 프록시 객체가 초기화되는데 이것을 LAZY 지연로딩 전략이라 한다.

즉시 로딩

Member와 Team을 함께 자주 사용한다면 즉시 로딩을 고려할 수 있다.

@Entity
public class Member {

    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

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

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

즉시 로딩(EAGER)은 Member 조회시 항상 Team도 조회한다.

Team team = new Team();
team.setName("TeamA");

em.persist(team);

Member member = new Member();
member.setUsername("member1");
member.setTeam(team);

em.persist(member);

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

Member findMember = em.find(Member.class, member.getId()); // (1)
Team findTeam = findMember.getTeam();
System.out.println(findTeam.getClass()); // class hellojpa.Team

위의 (1) 코드가 실행될 때 나오는 쿼리는 다음과 같다.

select
        member0_.member_id as member_i1_7_0_,
        member0_.age as age2_7_0_,
        member0_.team_id as team_id4_7_0_,
        member0_.username as username3_7_0_,
        team1_.team_id as team_id1_14_1_,
        team1_.name as name2_14_1_ 
    from
        member member0_ 
    left outer join
        team team1_ 
            on member0_.team_id=team1_.team_id 
    where
        member0_.member_id=?

지연로딩과 달리 Team의 정보까지 같이 가져옴을 알 수 있다.

즉시로딩 주의점

1. 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생한다.

예를 들어 Member가 10개정도의 객체와 연관관계가 있다면 em.find()를 할 때 join 쿼리가 10개나 나오게 된다. 이런 경우 쿼리가 엄청 길어지고 성능도 낮아진다.

2. 즉시 로딩은 JPQL에서 N + 1 문제를 일으킨다.

Team team = new Team();
team.setName("TeamA");

Team team2 = new Team();
team2.setName("TeamB");

em.persist(team);
em.persist(team2);

Member member = new Member();
member.setUsername("member1");
member.setTeam(team);

Member member2 = new Member();
member2.setUsername("member2");
member2.setTeam(team2);

em.persist(member);
em.persist(member2);

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

List<Member> members = em.createQuery("select m from Member m", Member.class)
                         .getResultList();
select
    member0_.MEMBER_ID as member_i1_2_,
    member0_.TEAM_ID as team_id3_2_,
    member0_.USERNAME as username2_2_ 
from
    Member member0_


select
    team0_.TEAM_ID as team_id1_4_0_,
    team0_.NAME as name2_4_0_ 
from
    Team team0_ 
where
    team0_.TEAM_ID=?   


select
    team0_.TEAM_ID as team_id1_4_0_,
    team0_.NAME as name2_4_0_ 
from
    Team team0_ 
where
    team0_.TEAM_ID=?   
          
  1. select m from Member m 이라는 쿼리로 2개의 member 가 리스트에 들어온다.

  2. 이 2개의 member 는 각각 team 객체가 필요하므로 2개의 select 쿼리가 나간다.

  3. N+1문제란 처음의 select m from Member m 쿼리 1개에 이 쿼리의 결과로 나온 N개의 객체에 대한 쿼리 N개가 더 나간다고 해서 N+1문제이다.

실무 관점에서 지연 로딩

1. 모든 연관관계에서 지연로딩을 사용할 것을 권장

지연 로딩을 사용한다면 Member의 정보만 필요할 때는 Member의 정보만 가져오고,
Team의 정보도 같이 필요하면 fetch 조인을 사용하여 N+1 문제를 해결할 수 있다.

그러나 즉시 로딩을 사용한다면 Member와 Team의 정보를 같이 필요로 할 때
fetch 조인을 사용하여 N+1 문제를 해결할 수 있지만,
Member의 정보만 필요로 할 때 Member의 정보만 가져올 수 없다.

2. 실무에서는 즉시로딩을 사용하지 않을 것을 권장

위의 즉시로딩 주의점에 대해 설명한 것을 근거로 사용하지 않을 것을 권장한다.

profile
성장에 대한 경험을 공유하고픈 자발적 경험주의자

0개의 댓글