프로그래밍/Spring

[ JPA ] 지연로딩과 즉시로딩

Yanoo 2022. 1. 20. 00:32
728x90
반응형

<인프런의 김영한님의 강의를 보고 정리한 내용입니다>

 

🎈 지연로딩과 즉시로딩

Member를 조회할 때 Team도 같이 조회를 해야할까?

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Entity
public class Member 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;

    @ManyToMany
    @JoinColumn(name = "MEMBER_PRODUCT")
    private List<Product> products = new ArrayList<>();

    public Team getTeam() {
        return team;
    }

    // .. 이하 생략
}
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;

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();

    public List<Member> getMembers() {
        return members;
    }

    // ... 이하 생략
}

이렇게 Member 엔티티와 Team 엔티티가 있을 때, Member에서 getUsername()을 호출하게 된다면 Team에 대한 쿼리도 나가게 될 것이다.

 

이를 해결하기 위한 것이 지연로딩이다.

@ManyToOne에 lazy를 추가하면 되는데

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Entity
public class Member extends BaseEntity {

    @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;

    @ManyToMany
    @JoinColumn(name = "MEMBER_PRODUCT")
    private List<Product> products = new ArrayList<>();

    public Team getTeam() {
        return team;
    }

    // 이하 생략
}

보면

@ManyToOne(fetch = FetchType.LAZY)를 추가했다.

 

이렇게 해서 해결되는 이유는 https://yanoo.tistory.com/126 에서 언급했듯 프록시로 조회하는 법이 있는데, 페치타입을 Lazy로 하면 프록시로 조회하기 때문이다. 정확한 결과를 확인하기 위해서

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

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);

            Member member = new Member();
            member.setUsername("user1");
            member.setTeam(team);
            em.persist(member);

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

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

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

이렇게 테스트를 했을 때 Team은 프록시로 조회되어야 하는 것이 맞다. 확인을 해보면,

프록시로 조회된 것을 알 수 있다.

 

이제 반대로 즉시로딩인데, 이것은 어차피 Team과 Member 모두를 자주 사용한다면 모두 같이 조회하는 것이다.

xxxxToOne은 기본값이 EAGER이기에 그대로 사용하면 되는데 똑같은 코드로 결과를 확인해 보면,

조인으로 한 번에 다 조회하고 Team 객체가 프록시가 아니라 본 객체임을 알 수 있다.

 

결론적으로 말하면, 지연로딩을 써야하는데 이유는,

일단 예상하지 못한 SQL이 실행될 수 있다.

그리고 JPQL에서 N+1문제를 일으키기 때문이다.

 

N+1 문제는 JPA에서 자주 등장하는 문제인데, 예를 봐보면

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
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);

            Team team1 = new Team();
            team1.setName("teamB");
            em.persist(team1);

            Member member = new Member();
            member.setUsername("user1");
            member.setTeam(team);
            em.persist(member);

            Member member1 = new Member();
            member1.setUsername("user2");
            member1.setTeam(team1);
            em.persist(member1);

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

            List<Member> members = em.createQuery("select m from Member m", Member.class).getResultList();
            
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        } finally {
            em.close();
        }
        emf.close();
    }
}

보면 Team이 2개 Member가 2개이다. 그리고 각각 다른 팀에 속한다.

여기서 멤버들을 조회하는 쿼리문을 생성했을 때, SQL문은 어떻게 될까?

보면 member만 조회하는 select문만 나가야하는데, 보면 member 수가 2명 이기에 2명의 각 팀에 대한 쿼리 2개가 더 나갔다. 그래서 이를 N+1 문제라고 한다.

@ManyToOne(fetch = FetchType.LAZY)

이렇게 추가하게 되면

멤버를 조회하는 select문만 나가는 것을 확인할 수 있다.

 

사실 N+1문제가 생기는 이유가 더 있는데 그것을 해결하는 법은 패치조인, 어노테이션 사용법, 배치사이즈 사용법 등이 있다.

xxxxToOne은 기본이 EAGER라 LAZY로 바꾸는걸 추천한다.


 

728x90
반응형