티스토리 뷰
XXXtoOne 일 경우 기본 FetchType은 EAGER 이고 XXXtoMany 일 경우 FetchType은 LAZY이다.
비즈니스 로직에서 단순히 Member 정보만 사용하는데 Team을 함께 조회하면 아무리 연관관계가 걸려있다해도 손해이다.
JPA는 이 문제를 지연로딩(LAZY)을 사용해서 해결한다.
즉시로딩 부터 살펴보자.
즉시로딩(FetchType.EAGER)
Member.class
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn
private Team team;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Team getTeam() {
return team;
}
public void changeTeam(Team team) {
this.team = team;
this.team.getMembers().add(this);
}
}
Team.class
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "team")
List<Member> members = new ArrayList<>();
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Member> getMembers() {
return members;
}
}
TestCode
@Slf4j
@SpringBootTest
public class LoadingTest {
@Autowired
EntityManager em;
@Test
@Transactional
void loadingTest() {
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member = new Member();
member.setName("memberA");
member.changeTeam(team);
em.persist(member);
em.flush();
em.clear();
Member findMember = em.find(Member.class, member.getId());
log.info("teamClass: {}",findMember.getTeam().getClass());
log.info("teamName: {}", findMember.getTeam().getName());
}
}
실행결과
select
member0_.id as id1_0_0_,
member0_.name as name2_0_0_,
member0_.team_id as team_id3_0_0_,
team1_.id as id1_1_1_,
team1_.name as name2_1_1_
from
member member0_
left outer join
team team1_
on member0_.team_id=team1_.id
where
member0_.id=?
teamClass: class com.example.jpatest.Team
teamName: teamA
- Team 객체는 실제 객체이다
- 즉시로딩을 사용하면 조인을 사용하여 SQL 한번에 함께 조회한다
지연로딩(FetchType.LAZY)
위 코드에서 Member.class의 FetchType만 LAZY로 수정해준다.
Member.class
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn
private Team team;
...
}
실행결과
select
member0_.id as id1_0_0_,
member0_.name as name2_0_0_,
member0_.team_id as team_id3_0_0_
from
member member0_
where
member0_.id=?
teamClass: class com.example.jpatest.Team$HibernateProxy$voVNNFTP
select
team0_.id as id1_1_0_,
team0_.name as name2_1_0_
from
team team0_
where
team0_.id=?
teamName: teamA
- Team 객체는 프록시 객체이다
- Team 객체를 실제로 사용할때, Team 객체가 조회된다 -> Member와 Team을 같이 사용한다면 SELECT 쿼리가 따로따로 두번 나간다.
지연로딩의 매커니즘
- 로딩되는 시점에 LAZY 로딩 설정이 되어있는 Team 엔티티는 프록시 객체로 가져온다
- 후에 실제 객체를 사용하는 시점에(Team을 사용하는 시점) 초기화 된다 -> 쿼리가 나간다
오 그럼 Member와 Team을 모두 사용할때는 즉시로딩을 사용하면 되겠네?
하지만 즉시로딩은 예상하지 못한 SQL이 발생하므로 실무에선 지연로딩을 권장한다.
즉시로딩 문제점
- 즉시로딩을 적용하면 예상하지 못한 SQL이 발생한다
- @ManyToOne이 5개 있는데, 전부 EAGER로 설정되어 있으면 조인이 5번 발생한다 -> 실무에선 테이블 수가 더 많다
- 즉시로딩은 JPQL에서 N+1 문제를 일으킨다
- 실무에선 복잡한 쿼리를 풀기위해 JPQL을 자주 사용한다
- em.find()는 PK를 정해놓고 DB에서 가져오기 때문에 JPA 구현체 내부에서 최적화를 할 수 있다(한방쿼리)
- 하지만, JPQL에선 입력 받은 query string이 그대로 SQL로 변환된다.
- "select m from Member m" 쿼리는 Member만 조회한다
- Member를 쭉 조회하다가 Team이 즉시로딩이면 Member 반환 시점에 다 조회가 되어야한다
- 따라서 Member를 다 가져오고 Member와 연관된 Team을 다시 다 가져온다
- 코드로 멤버가 2명, 팀도 2개인 상태에서 모든 멤버를 조회해보자
JPQL 즉시로딩
TestCode
@Test
@Transactional
void jpql() {
Team team1 = new Team();
team1.setName("teamA");
em.persist(team1);
Team team2 = new Team();
team2.setName("teamB");
em.persist(team2);
Member member1 = new Member();
member1.setName("memberA");
member1.changeTeam(team1);
em.persist(member1);
Member member2 = new Member();
member2.setName("memberB");
member2.changeTeam(team2);
em.persist(member2);
em.flush();
em.clear();
List<Member> members = em.createQuery("select m from Member m", Member.class).getResultList();
}
실행결과
select
member0_.id as id1_0_,
member0_.name as name2_0_,
member0_.team_id as team_id3_0_
from
member member0_
select
team0_.id as id1_1_0_,
team0_.name as name2_1_0_
from
team team0_
where
team0_.id=?
select
team0_.id as id1_1_0_,
team0_.name as name2_1_0_
from
team team0_
where
team0_.id=?
- 먼저 Member를 조회해서 가져온다
- 그리고 나서 Member들의 Team을 채우기 위해 각각 쿼리를 날려서 가져온다
- 멤버가 수천, 수만명이고 각각 팀이 다를 경우 쿼리양은 엄청날 것이다...
위처럼 N+1의 문제는 쿼리를 1개 날렸는데, 그것 때문에 추가 쿼리가 N개 나간다는 의미이다.
결론
- LAZY 로딩 전략을 기본으로 가져가자
- 그럼 Member, Team을 다 사용할때는 Team 객체를 실제 사용할때마다 매번 쿼리를 날려서 조회해야되나?
- 이런 경우를 위해서 JPQL의 fetch join 이나 엔티티 그래프 기능이 존재한다 -> 대부분 fetch join으로 해결한다
참고
자바 ORM 표준 JPA 프로그래밍(김영한)
'개발 > Spring' 카테고리의 다른 글
[Spring] - API Gateway 만들기 #2 (0) | 2022.07.13 |
---|---|
[Spring] - API Gateway 만들기 #1 (0) | 2022.07.13 |
[Spring] - p6spy 사용하기 (0) | 2022.06.19 |
[Spring] - 테스트 코드에서 롬복 사용하기 (0) | 2022.06.19 |
[Spring] - RequestContextHolder (0) | 2022.06.06 |