김찬진의 개발 블로그

23/12/27 [지연로딩과 즉시로딩] 본문

1일1배움/JPA (김영한 님)

23/12/27 [지연로딩과 즉시로딩]

kim chan jin 2023. 12. 28. 00:40
@Entity
public class Member {

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

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

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
    
    // getter, setter 생략
}
@Entity
public class Team {

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

    private String name;

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();
    
    // getter, setter 생략
}

 

위와 같이 Member : Team = N : 1 상황이라고 가정하자

(참고, Member 가 FK 를 가지고 있기 때문에 Team : Member = 1 : N 라고 하지 않았음을 주의)

 

Member 를 조회(find)할 때 Team 까지 조회하게 되는데

만약 비즈니스 로상 Team 을 사용할 일이 거의 없다면 Team 을 프록시로 만드는 것이 효율적임

다음과 같이 Member 엔티티 클래스의 team 필드의 애노테이션에 (fetch = FetchType.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;
    
    // getter, setter 생략
}

 

 

 

반면 만약 비즈니스 로직 상 Team 를 사용이 빈번하다면 Team 을 프록시로 만드는 것이 비효율적임

왜냐하면 만약 Team 을 지연로딩하지 않았다면 Member 조회할 때 SELECT - JOIN 쿼리를 한번에 보낼 수 있었을텐데

Team 을 지연로딩함으로써 즉, Team 을 프록시로 만듦으로써

Member 조회할 때에는 Member 에 대해서만 SELECT 쿼리 보내고

이후 Team 이 실제로 사용되는 그 시점에 Team 에 대한 SELECT 쿼리를 다시 보내야 하기 때문에 리소스 낭비이다

그래서 다음과 같이 Member 엔티티 클래스의 team 필드의 애노테이션에 (fetch = FetchType.EAGER) 추가

@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;
    
    // getter, setter 생략
}

 

 

 

하지만 즉시 로딩은 사용하지 말자

1. 가급적 지연 로딩 사용하자

 

2. 즉시 로딩 적용하면 예상치 못한 SQL 발생

 

3. 즉시 로딩은 JPQL 에서 N+1 발생

예시를 들면 다음과 같다

멤버 kim 은 teamA 소속이다

멤버 park 은 teamB 소속이다

영속화한 이후에 flush(), clear() 해서 1차 캐시에 아무것도 없다

System.out.println("=========== teamA ===========");
Team teamA = new Team();
teamA.setName("teamA");
em.persist(teamA); // INSERT

System.out.println("=========== teamB ===========");
Team teamB = new Team();
teamB.setName("teamB");
em.persist(teamB); // INSERT

System.out.println("=========== kim ===========");
Member kim = new Member();
kim.setUsername("kim");
kim.setTeam(teamA);
em.persist(kim); // INSERT

System.out.println("=========== park ===========");
Member park = new Member();
park.setUsername("teamB");
park.setTeam(teamB);
em.persist(park); // INSERT

System.out.println("=========== flush and clear ===========");
em.flush(); // INSERT 4개 모아서 날림
em.clear(); // 1차 캐시 비움

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

tx.commit();

 

JPQL 작성하면 일단 SQL 로 번역되어 쿼리가 날아가서 List<Member> 반환 -> 1

근데 Member 의 team 필드의 애노테이션을 보니 (fetch = FetchType.EAGER) 즉, 즉시로딩

그럼 각 Member 의 team 필드를 채우기 위한 SQL 이 추가적으로 나가야 함

이 경우 Member 2명의 team 필드를 채워야 하므로

"SELECT * FROM TEAM WHERE TEAM_ID = ?" SQL 이 2번 날아감 -> N

 

N+1 해결하기 위한 방법은?

1. join fetch

@ManyToOne, @OneToOne 은 기본이 즉시로딩이므로 이러한 연관관계 매핑은 지연로딩으로 설정

그럼 일단 N+1 은 발생안함

(@OneToMany, @ManyToMany 는 기본이 지연로딩)

지연로딩 걸어둔 상태 + JPQL join fetch 사용하여 동적으로 필요한 것만 쿼리할 수 있도록

 

2. entity graph 

나중에

 

3. batch size

나중에

Comments