본문 바로가기

Spring/JPA

JPA- 즉시 로딩, 지연 로딩

지연 로딩

예를 들어 Member와 Team이 서로 다대일 연관관계 매핑 되어있다고 하자.

대강 이런 상태

 이런 상황에서 기본적으로 Member를 조회하게 된다면 Member에 대한 select 쿼리가 나가고 그 안에 Team에 대한 Join 쿼리도 존재할 것이다. 코드로 보자.

 

JpaMain.class

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 {
            Member member = new Member();
            member.setUserName("member1");
            em.persist(member);

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

            member.setTeam(team);

            // DB에 쿼리 날리고 영속성 컨텍스트 비움
            em.flush();
            em.clear();

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

            System.out.println("findMember.getTeam().getClass() = " + findMember.getTeam().getClass());
            tx.commit();
        } catch (Exception e) {
            tx.rollback(); //오류 발생 시 롤백
        } finally {
            //종료
            em.close();
        }
        emf.close();
    }
}
출력>>
Hibernate: 
    select
        member0_.MEMBER_ID as member_i1_1_0_,
        member0_.createdBy as createdb2_1_0_,
        member0_.createdDate as createdd3_1_0_,
        member0_.modifiedBy as modified4_1_0_,
        member0_.modifiedDate as modified5_1_0_,
        member0_.TEAM_ID as team_id7_1_0_,
        member0_.USERNAME as username6_1_0_,
        team1_.TEAM_ID as team_id1_4_1_,
        team1_.createdBy as createdb2_4_1_,
        team1_.createdDate as createdd3_4_1_,
        team1_.modifiedBy as modified4_4_1_,
        team1_.modifiedDate as modified5_4_1_,
        team1_.name as name6_4_1_ 
    from
        Member member0_ 
    left outer join
        Team team1_ 
            on member0_.TEAM_ID=team1_.TEAM_ID 
    where
        member0_.MEMBER_ID=?
findMember.getTeam().getClass() = class hellojpa.Team

 출력 결과를 보면 Member에 대해서 Team이 left outer join 했다고 한다. 잘 모르겠지만 암튼 조인 된 듯 하다. 여기서 확인 할 수 있는 것은 한번의 Select 쿼리를 통해서 Member와 Team이 모두 조회 되었다는 것이다.

자 그렇다면 Member의 Team의 @ManyToOne의 fetch를 LAZY로 바꿔보자.

@ManyToOne(fetch = FetchType.LAZY)

Member.class

@Entity
@Getter @Setter
public class Member extends BaseEntity {

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

    @Column(name = "USERNAME", nullable = false)  // 매핑할 컬럼명이 "name". + not null 설정
    private String userName;

    @ManyToOne(fetch = FetchType.LAZY)  //지연 로딩, Member 클래스만 조회함. Member.team은 프록시 객체
    @JoinColumn(name = "TEAM_ID")
    private Team team;
		
        // 이하 생략
}

 지금 Member.team을 보면 @ManyToOne(fetch = FetchType.LAZY)로 되어있는 것을 확인 할 수 있다. 그리고 이는 다음을 의미한다.

 "Member를 조회할 때 Team은 프록시로 가져온다"

그러면 이 상태에서 다시 한번 JpaMain 클래스를 실행 해보자.

 

결과

Hibernate: 
    select
        member0_.MEMBER_ID as member_i1_1_0_,
        member0_.createdBy as createdb2_1_0_,
        member0_.createdDate as createdd3_1_0_,
        member0_.modifiedBy as modified4_1_0_,
        member0_.modifiedDate as modified5_1_0_,
        member0_.TEAM_ID as team_id7_1_0_,
        member0_.USERNAME as username6_1_0_ 
    from
        Member member0_ 
    where
        member0_.MEMBER_ID=?
findMember.getTeam().getClass() = class hellojpa.Team$HibernateProxy$lVxj7lsb

 결과를 보면 Team이 조회되지 않았다. 또한 findMember.team의 클래스를 확인해보면 프록시임을 확인 가능하다. 그렇다면 JpaMain에 다음 코드도 추가해보자.

 

추가 코드

System.out.println("=====================");
String teamName = findMember.getTeam().getName();  // 초기화, 쿼리 날아감
System.out.println("=====================");
String teamName2 = findMember.getTeam().getName();  // Team 엔티티가 영속성 컨텍스트에 이미 올라와서 쿼리 안나감
System.out.println("=====================");

결과

Hibernate: 
    select
        member0_.MEMBER_ID as member_i1_1_0_,
        member0_.createdBy as createdb2_1_0_,
        member0_.createdDate as createdd3_1_0_,
        member0_.modifiedBy as modified4_1_0_,
        member0_.modifiedDate as modified5_1_0_,
        member0_.TEAM_ID as team_id7_1_0_,
        member0_.USERNAME as username6_1_0_ 
    from
        Member member0_ 
    where
        member0_.MEMBER_ID=?
findMember.getTeam().getClass() = class hellojpa.Team$HibernateProxy$2QtKq9bM
=====================
Hibernate: 
    select
        team0_.TEAM_ID as team_id1_4_0_,
        team0_.createdBy as createdb2_4_0_,
        team0_.createdDate as createdd3_4_0_,
        team0_.modifiedBy as modified4_4_0_,
        team0_.modifiedDate as modified5_4_0_,
        team0_.name as name6_4_0_ 
    from
        Team team0_ 
    where
        team0_.TEAM_ID=?
=====================
=====================

 다시 결과를 보면 이제는 Team에 대한 select문이 생겼다. 이는 프록시 객체인 Team을 

String teamName = findMember.getTeam().getName();로 조회하는 과정에서 초기화가 일어났기 때문이다.

 초기화 과정에서 DB에 쿼리를 날려서 Team에 대한 select문이 생긴 것이고 그 후에는 Team 엔티티가 영속성 컨텍스트에 저장이 되었기 때문에 두번째 getName()에서는 쿼리가 나오지 않는 것을 확인 할 수 있다.

중간 정리

  • FetchType.LAZY로 지연로딩이 가능하다. 지연로딩을 하면 프록시로 객체를 가져온다.
  • FetchType.EAGER로 즉시 로딩이 가능하다. 즉시 로딩은 조회한 객체와 연관 객체가 같이 사용되는 경우가 많은 경우 유리하다.
  • 하지만 왠만하면 지연로딩을 사용하는 것이 좋다. 연관 객체가 많은 경우 EAGER로 조회하면 쿼리가 폭발한다.
  • @xxxToMany는 디폴트가 FetchType.LAZY이다.
  • @xxxToOne은 디폴트가 FetchType.EAGER니 꼭 FetchType.LAZY로 바꾸자.
  • JPQL에서 FetchType.EAGER는 n+1 문제를 발생시킨다.

 

 여기서 "JPQL에서 FetchType.EAGER는 n+1 문제를 발생시킨다." 이 부분만 설명해보자면 JPQL로 Member를 조회하는 경우 Member.team이 FetchType.EAGER라면 이것은 "Member가 조회될 때 Team도 반드시 내용이 있어야한다!" 라는 의미이다. 근데 문제는 JPQL은 SQL 처럼 Member를 조회하면 Member만 반환하기 때문에 JPQL로 조회하는 당시에는 Team이 조회되지 않는다. 그래서 JPA는 Member를 조회 후 Team이 EAGER인 것을 확인하면 다시 Team에 대한 Select 쿼리를 발생시킨다. 즉, 하나만 조회했는데 쿼리는 두번 나갔다 해서 이런 것을 "n+1 문제"라고 한다.

 

결론은 모든 연관관계에 지연로딩 FetchType.LAZY를 쓰자.

'Spring > JPA' 카테고리의 다른 글

JPA- 임베디드 타입  (0) 2023.03.22
JPA- Cascade와 고아 객체  (0) 2023.03.21
JPA- 프록시  (0) 2023.03.16
JPA- @MappedSuperClass  (0) 2023.03.12
JPA- 상속관계 매핑  (0) 2023.03.12