6 min read

<JPA> 알고쓰는 즉시로딩과 지연로딩

지난번 포스팅의 Proxy를 활용해 JPA의 즉시로딩과 지연로딩에 대해서 알아봅시다.

Member와 Team이 엔티티가 존재합니다.만약 Member를 조회할 때 Team 엔티티를 함께 조회하는 방식이 즉시로딩이고,Team 엔티티를 Proxy객체로 조회하는것이 지연로딩 방식입니다.아래 코드를 보면 직접 확인해봅시다.

@Getter
@Entity
public class Member {
    @Id
    @GeneratedValue
    private Long id;

    private String name;

    //Member를 em.find 할 시 Team 엔티티는 Proxy로 조회
	//지연로딩 사용 시 Eager -> Lazy로 변경하면됨
    @ManyToOne(fetch = FetchType.Eager)
    @JoinColumn(name = "team_id")
    private Team team;
}

그리고 아래의 코드를 실행시켜 줍시다.

	Team team = new Team();
	team.setTeamName("starbucks");
	em.persist(team);

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

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

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

            		System.out.println("findMember.getTeam().getClass() = " + findMember.getTeam().getClass());

    System.out.println("=======================");
    findMember.getTeam().getTeamName(); // proxy 객체 초기화
    System.out.println("=======================");
	tx.commit();

아래의 결과가 나오기전 당연히 insert문이 두개 나간뒤,select문의 조인문법을 통해member와 team을 함께 디비로부터 가져오는것을 확인할 수 있습니다.그리고 findMember의 team 객체 또한 Proxy가 아닌 실제 객체임을 확인할 수 있습니다.

이번에는 동일한 코드를 Lazy로 변경해서 실행시켜보겠습니다.동일하게 insert문 두개가 날라간뒤,select문이 Member만 조회하는것을 확인할 수 있습니다.그리고 나서 findMember의 team객체의 class타입을 찍어보면 익숙한 하이버네이트가 만들어낸 프록시 객체임을 확인할 수 있습니다.마지막으로 findMember.getTeam().getTeamName(); 코드를 보시면 해당 프록시 객체의 메서드를 호출해 실제 Entity생성후, 실제 엔티티의 참조값을 프록시에 넣어주는 초기화 과정이 일어나는것 또한 확인 할 수 있습니다.

이제 지연로딩과 즉시로딩의 차이점을 알게 되었습니다.그러면 실제 프로젝트를 진행할 때 무조건 지연로딩을 사용해야한다고 합니다.지금부터 왜 즉시로딩이 문제를 가지는지 확인해보겠습니다.

JPA의 1 + N 문제

우선 팀과 멤버 엔티티 모두를 각각 2개씩 총 4개의 insert문이 발생하도록 설정합시다.그리고 JPQL이라는 JPA에서 사용하는 SQL 문장으로 모든 멤버를 가져와 봅시다.이후 해당 코드를 Eager,Lazy로 실행시켜 봅시다.

	Team team = new Team();
	team.setTeamName("starbucks");
	em.persist(team);

	Team team2 = new Team();
	team2.setTeamName("starbucks2");
	em.persist(team2);

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

	Member member2 = new Member();
	member2.setName("brido2");
	member2.setTeam(team2);
	em.persist(member2);

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

	System.out.println("======CreateQuery N+1=========");
	List<Member> members = em.createQuery("select m From Member m", Member.class).getResultList();
            System.out.println("===============================");

	tx.commit();

3개의 쿼리가 발생한것이 Eager이고 Lazy의 경우 한개만 발생한것을 확인할수 있습니다.그러면 Eager에서 3개라는 쿼리는 어떻게 생긴걸까요?3개의 쿼리를 분석해보면 1개의 모든 member를 가져오는 쿼리,그리고 해당 member들의 각각의 team에 대한 정보를 갖고오는 2개의 쿼리입니다.즉,갖고오는 member마다의 team객체가 다를경우 모든 team에 select 쿼리를 발생시켜야 하기에 발생하는 현상입니다.

그러면 3개의 쿼리가 실제로 보기엔 그렇게 많아 보이지도 않는데 뭐가 문제일까요?member가 가지는 엔티티가 지금은 team 하나밖에 없어서 간단해보일뿐입니다. 테이블이 점점 많아지고 연관관계가 복잡해지기 시작하면 과연 몇개의 쿼리를 더 발생시킬까요?그래서 실제 프로젝트를 진행하실때는 Eager로 해버리면 수많은 쿼리가 발생하기 때문에 성능이 정말 안나옵니다.필자도 김영한님의 강의를 들었음에도 Eager로 모든 연관관계를 설정해서 프로젝트를 진행해본적이 있습니다.게시판 페이지 하나를 띄우는데 3초가 걸립니다.그러니 우리 모두 Lazy를 사용합시다.

자,그러면 Lazy를 사용할경우 member에 대한 쿼리 한개만 나가는데 만약 member를 조회할 경우 team을 조회해야만 하는 경우 어떻게 해결할까라는 의문이 드실것입니다.이에 대한 해답은 Fetch join이나 엔티티 그래프 기능 등이 있습니다.이는 추후에 포스팅에서 다룰 예정입니다.

마지막으로 Lazy를 모든 연관관계에서 사용해라라고 했는데,각각의 연관관계가 서로 다른 fetch 전략을 가집니다.이를 정리해드리겠습니다.

  • @XToOne 연관관계의 경우 기본값이 Eager입니다.변경해주셔야 합니다.
  • @XToMany 연관관계의 경우 기본값이 Lazy입니다.

합리적으로 추론해보면 JPA를 만드신분들도 @XToOne으로 가져올 경우 @XToMany처럼 List형태로 가져오지 않으니까 디폴트값으로 Eager를 주셨지 않을까 싶습니다.이렇게 기억하시면 조금 더 합리적이지 않을까 싶습니다.

지금까지 JPA의 지연로딩과 즉시로딩 그리고 왜 지연로딩을 사용해야만하는가에 대해 알아보았습니다.다음 포스팅에서는 Proxy를 활용한 마지막 주제 영속성 전이(Cascade)에 대해 알아보도록 하겠습니다.

Ref : 김영한님 - JPA 기본편