김찬진의 개발 블로그
23/12/27 [지연로딩과 즉시로딩] 본문
@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
나중에
'1일1배움 > JPA (김영한 님)' 카테고리의 다른 글
23/12/28 [JPA 값 타입 : primitive type, Wrapper type, String type / Embedded type] (0) | 2023.12.28 |
---|---|
23/12/28 [즉시로딩, 지연로딩 VS 영속성 전이, 고아 객체] (0) | 2023.12.28 |
23/12/27 [em.find() 와 em.getReference() 의 차이] (1) | 2023.12.27 |
23/12/25 [다양한 연관관계 매핑 - FK 관리 주체, 양방향 연관관계 여부에 따라] (0) | 2023.12.25 |
23/12/25 [연관관계 편의 메서드] (0) | 2023.12.25 |