728x90
만약 Member의 username만 사용하는 비즈니스라면 Team정보까지 불러오는 것은 추가적인 비용이 들어 손해이다. 이런 문제 때문에 JPA는 지연 로딩이라는 것을 지원한다.
지연 로딩
@Entity
public class Member {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn
private Team team;
}
- fetch = FetchType.LAZY
- 연관 관계 설정 어노테이션의 옵션으로 프록시 객체로 조회한다.
- 이전의 프록시는 해당 객체에 대해 직접적인 값을 사용할 때 DB에서 조회했다. 즉 프록시 객체로 조회하면 team을 출력하거나 응답하는등의 작업을 하지 않는다면 team은 조회되지 않는다.
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member1 = new Member();
member1.setUsername("member1");
member1.setTeam(team);
em.persist(member1);
em.flush();
em.clear();
Member m = em.find(Member.class, member1.getId());
System.out.println("m = " + m.getTeam().getClass());
System.out.println("=============");
m.getTeam().getName();
System.out.println("=============");
tx.commit();
- 위의 코드에서 em.find()를 사용하고 team에 대한 작업이 없는 시점까지는 Team 정보를 가져오는 쿼리가 날아가지 않는다.
- 이후 m.getTeam().getName()과 같이 직접 호출하게되면 해당 쿼리가 날아간다.
- 결과도 m은 $HibernateProxy로 프록시 객체인 것을 확인할 수 있고 이후에 직접 조회하는 시점에 TEAM에 쿼리가 날아가는 것을 볼 수 있다.
- 정리하면 Member를 조회하면 당장은 조회하지 않는 Team은 프록시로 남긴다. 이후 실제 Team을 사용하는 시점에 DB조회를 해서 초기화된다.
- 주의할 점은 getTeam()에서 초기화가 일어나는 것이 아닌, getName()과 같이 실제 데이터를 조회할 때 초기화가 일어난다.
즉시 로딩
지연 로딩과 반대로 Member와 Team 모두 자주 사용하여 함께 조회해야 하는 경우 지연 로딩처럼 쿼리가 두 번씩 나가면 성능과 비용에서 손해를 본다. 그래서 사용하는 것이 EAGER로딩이다.
@Entity
public class Member {
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn
private Team team;
}
- 마찬가지로 fetch = FetchType.EAGER 이라는 옵션을 통해 설정한다.
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member1 = new Member();
member1.setUsername("member1");
member1.setTeam(team);
em.persist(member1);
em.flush();
em.clear();
Member m = em.find(Member.class, member1.getId());
System.out.println("m = " + m.getTeam().getClass());
System.out.println("=============");
m.getTeam().getName();
System.out.println("=============");
tx.commit();
- join으로 한 번에 조회하는 것을 볼 수 있다. 한 번에 조회했기 때문에 m.getTeam().getClass()에서도 프록시가 아닌 진짜 정보가 나오는 것도 확인할 수 있다.
- 즉시 로딩의 경우 두 가지 선택을 할 수 있다.
- Member를 가지고 올 때 EAGER로 걸린 Entity를 join하여 쿼리 한 번에 가지고 온다. → 웬만한 JPA 쿼리는 이렇게 가져오려 한다.
- 일단 Member를 가져온 다음 EAGER로 되어있는 Entity를 확인해서 한 번 더 쿼리를 날린다. → Member, Team에 em.find()를 해서 각각 두 번의 쿼리를 보내는 방식으로 성능이 좋지 않다.
즉시 로딩 주의 사항
- 실무에서는 지연 로딩만 사용하는 것을 권장한다.
- 즉시 로딩의 경우 서로 참조된 경우 무한 참조가 발생할 수도 있다.
예상치 못한 SQL 발생 문제
- 즉시 로딩을 사용하면 예상하지 못한 SQL이 발생할 수 있다.
- 해당 문제의 경우 join이 한 두개로 복잡하지 않으면 상관없지만 데이터가 많아지고 테이블이 많아짐에 따라 join 때문에 성능에 부담이 된다.
- 만약 데이터가 10개면 find()할 때 10개를 모두 join하면서 생각하지도 못한 매우 큰 쿼리가 나갈 수도 있다.
N+1 문제
- 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.
- 여기서 1은 최초 쿼리, N은 최초 쿼리의 결과 개수이다. 즉 최초에 보낸 쿼리 한 번에 해당 결과 개수만큼 추가적으로 쿼리가 발생하는 것이다.
@Entity
public class Member {
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn
private Team team;
}
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member1 = new Member();
member1.setUsername("member1");
member1.setTeam(team);
em.persist(member1);
em.flush();
em.clear();
List<Member> members = em.createQuery("select m from Member m", Member.class).getResultList();
tx.commit();
- em.find() 대신 JPQL을 사용할 경우 member만 가지고 온다. 이후 member를 가지고 오니 team에 즉시 로딩이 걸려있는 것을 확인하고 select * from Team where team_id = ?로 관련된 team을 조회하는 쿼리가 함께 나간다.
- EAGER로 설정했는데도 두 번의 쿼리가 발생했다.
- JPQL에서 발생하는 이유는 find()의 경우 PK를 찍어서 가져오는 것이라 JPA가 내부적으로 최적화를 하기 때문이다. 하지만 JPQL은 내가 작성한 코드의 쿼리가 그대로 날아가기 때문에 Member만 조회한다.
- 이후 Member를 가지고와서 확인하니 즉시 로딩으로 설정되어 있고 즉시 로딩은 가져올 때 무조건 값이 전부 들어있어야 한다. 따라서 EAGER로 되어있는 TEAM을 조회하기 위해 N개의 쿼리가 다시 나간다. 여기서 Member가 10개라면 Member조회 쿼리 한 번 나가고 각 10개 Member의 Team을 다시 조회하는 쿼리가 나간다.
Team teamA = new Team();
teamA.setName("teamA");
em.persist(teamA);
Team teamB = new Team();
teamB.setName("teamB");
em.persist(teamB);
Member member1 = new Member();
member1.setUsername("member1");
member1.setTeam(teamA);
em.persist(member1);
Member member2 = new Member();
member2.setUsername("member2");
member2.setTeam(teamB);
em.persist(member2);
em.flush();
em.clear();
List<Member> members = em.createQuery("select m from Member m", Member.class).getResultList();
tx.commit();
- 멤버 2명을 추가해서 확인해보면 위의 설명대로라면 Member조회 쿼리 한 번에 Team을 각각 조회하는 2번의 쿼리가 추가적으로 나가야한다.
- 이는 멤버를 가지고 왔더니 멤버가 2개가 있었고 각각 다른 팀을 가지고 있는 것이다. 다른 팀이면 영속성 컨텍스트에서 다시 가져올 수도 없으니 각각 가져오기 위해 각 팀을 가져올 때마다 쿼리가 한 번씩 추가로 발생하여 2번의 쿼리가 발생하는 것이다.
- Member조회 이후 Team에 대한 쿼리가 추가로 2번 발생했다. 즉 난 한 번의 쿼리를 날려도 추가 쿼리 N번이 날아간 것이다.
@Entity
public class Member {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn
prviate Team
team;
}
- 이렇게 지연 로딩을 사용하면 Team을 사용하지 않아 프록시로 되어있기 때문에 쿼리 하나로 끝난다. 그래서 실무에서는 지연 로딩만 사용하기를 권장하는 것이다.(혹시 모르기 때문)
N+1 해결 방법
지연 로딩
- 기본적으로 모든 연관 관계는 fetch = FetchType.LAZY로 설정한다.
- 특히 @ManyToOne, @OneToOne은 default가 즉시 로딩이라 지연 로딩으로 설정한다.
fetch join
- 런타임에 동적으로 내가 원하는 정보만 선택해서 가져오는 방법으로 join을 이용하여 쿼리를 한 번만 날린다.
- 상황에 따라 Member만 가져오거나 Team까지 필요하면 한 번에 불러온다
- 쿼리가 여러번 나갈거 같은 쿼리를 한 방 쿼리로 변환하는 것
- SQL의 join과는 다른 것으로 JPQL에서 성능 최적화를 위해 제공하는 기능이다.
- 연관된 Entity, 컬렉션을 SQL 한 번에 함께 조회하는 기능이다.
- 아래와 같이 명령어로 “join fetch” 를 사용하면 된다.
Team teamA = new Team();
teamA.setName("teamA");
em.persist(teamA);
Team teamB = new Team();
teamB.setName("teamB");
em.persist(teamB);
Member member1 = new Member();
member1.setUsername("member1");
member1.setTeam(teamA);
em.persist(member1);
Member member2 = new Member();
member2.setUsername("member2");
member2.setTeam(teamB);
em.persist(member2);
em.flush();
em.clear();
List<Member> members = em.createQuery("select m from Member m join fetch m.team", Member.class).getResultList();
tx.commit();
- “select m from Member m join fetch m.team” 이게 fetch join을 사용한 것이다.
- 지연 로딩을 사용하더라도 Team 데이터를 조회하면 쿼리는 계속 나간다.
- fetch join을 하면 한 번에 Team 정보까지 모두 가져오게 된다. 이미 쿼리 한 번에 값이 채워져 있기 때문에 순회하면서 데이터를 조회해도 전혀 문제가 없다.
어노테이션, 배치 사이즈로 해결하는 방법이 있지만 기본적으로 지연 로딩과 fetch join을 사용한다.
지연 로딩 활용
- 실무는 반드시 지연 로딩을 사용해야 한다.
- 위의 그림에서 회원과 팀, 주문과 상품은 자주 묶어서 조회하고, 회원과 주문은 가끔만 사용한다고 가정을 했을 때 회원과 팀, 주문과 상품은 즉시 로딩으로, 회원과 주문은 지연 로딩을 사용하면 된다.
- 회원을 조회하면 팀은 즉시 로딩이기 때문에 쿼리 한 번으로 조회가 가능하다. 하지만 주문 내역은 지연 로딩으로 직접 사용할 때 가져온다.
- 만약 회원 조회 시에 주문 내역 프록시를 한 번 조회했다면?
→ 해당 주문 내역에 연관된 상품도 즉시 로딩으로 한 번에 가져온다.
728x90
'Java > JPA' 카테고리의 다른 글
[JPA] 영속성 전이와 고아 객체 (0) | 2023.06.25 |
---|---|
[JPA] 프록시 (0) | 2023.06.25 |
[JPA] 다양한 연관 관계 매핑(다대일, 일대다, 일대일, 다대다) (0) | 2023.06.25 |
[JPA] 연관 관계 (0) | 2023.06.24 |
[JPA] Entity 매핑 (0) | 2023.06.24 |