일단 이걸 이해할려면 JPA 프록시 이걸 알아야된다.
CS질문에서 많이 나오는 질문이기도 하고....
실무에서도 꼭 알아야 할 개념이기도 하다고 해서, 이번건 아주 중요하다.
@OneToMany(mappedBy = "team", fetch = FetchType.LAZY)
private List<Member> members = new ArrayList<>();
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "TEAM_ID")
private Team team;
어노테이션을 보면, fetch = FetchType.~
이란 속성이 있다.
FetchType
JPA가 하나의 Entity를 조회할 때, 연관관계에 있는 객체들을 어떻게 가져올 것이냐를 나타내는 설정값이다.
JPA는 JPQL을 객체와 필드를 보고 쿼리문으로 변환하여 DB에 보낸다.
그럼 다른 테이블과 연관매핑을 하게 되는데, 총 4개의 어노테이션을 사용한다.
@OneToOne
: 1대1
@OneToMany
: 1대다
@ManyToOne
: 다대1
@ManyToMany
: 다대다
그럼 이 연관된 테이블 데이터를 한번에 가져올까...? 혹은 한개씩 따로 가져올까...? 를 설정하는게 FetchType이다.
방식은 총 두가지다.
일단!!
@ManyToOne
(다대1)
@OneToOne
(일대일)
이 둘은 기본이 즉시 로딩이다.
@Entity
@Getter @Setter
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;
}
Member라는 엔티티는 이렇다.
즉시로딩 EAGER
데이터를 조회할 때, 연관된 모든 객체의 데이터까지 한 번에 불러오는 것
현재 Member는 Team이란 테이블하고 @ManyToOne
(다대1)로 매핑이 되어 있는 상태다.
fetch = FetchType.EAGER
로 즉시로딩으로 설정되어 있으니까,
Member를 가져올 때, 그 Team의 데이터를 같이 한 번에 가져오게 된다.
Member findMember = em.find(Member.class, member.getId());
그래서 그냥 단순하게 Member를 찾을려고 해도...
Hibernate:
select
m1_0.MEMBER_ID,
t1_0.TEAM_ID,
t1_0.name,
m1_0.USERNAME
from
Member m1_0
left join
Team t1_0
on t1_0.TEAM_ID=m1_0.TEAM_ID
where
m1_0.MEMBER_ID=?
저기 보면 left join (왼쪽조인)을 해서 Team의 데이터까지 전부 다 가져오게 된다.
@OneToMany
(1대다)
@ManyToMany
(다대다)
이 둘은 기본이 지연 로딩이다.
@Entity
@Getter @Setter
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "team", fetch = FetchType.LAZY)
private List<Member> members = new ArrayList<>();
}
Team의 엔티티는 이렇다.
지연로딩 LAZY
데이터를 조회할 때, 필요한 시점에 연관된 객체의 데이터를 불러오는 것이다.
현재 Team은 Member란 테이블하고 @OneToMany
(1대다)로 매핑이 되어 있는 상태다.
fetch = FetchType.LAZY
로 지연로딩으로 설정되어 있으니까,
Team을 가져올 때, 정말로 그 Team의 데이터만 일단 가져오게 된다.
Team findTeam = em.find(Team.class, 1L);
이런식으로 팀을 일단 찾아오면.....
Hibernate:
select
t1_0.TEAM_ID,
t1_0.name
from
Team t1_0
where
t1_0.TEAM_ID=?
이런 쿼리를 만들고 보낸다.
지금 이 쿼리 만으로는 속해있는 Member의 정보를 알 수 없다.
List<Member> members = findTeam.getMembers();
for (Member m : members) {
System.out.println("m = " + m.getUsername());
}
그럼 이렇게 속해있는 Member를 찾을려고 선언하면....?
Hibernate:
select
m1_0.TEAM_ID,
m1_0.MEMBER_ID,
m1_0.USERNAME
from
Member m1_0
where
m1_0.TEAM_ID=?
이런 쿼리를 만들어서 보낸다.
"어....? 그럼 Team 정보는 어떤 방식으로 들어와요...?"
그래서 맨 위에 JPA 프록시 이 개념을 먼저 알고 와야 편하다는 뜻 이었다.
현재 Team의 Entity는 프록시 객체로 들어와 있는 상태다.
그리고 Team에 대한 정보를 찾을려고 하면, 쿼리가 발생한다.
근데 여러 블로그에서도 그렇고, 내가 듣고 있는 강의에서도 강사님께서 이런 말씀을 하셨다.
"가급적 지연 로딩만 사용(특히 실무에서)"
왜 다들 이렇게 말할까?
여려가지 이유가 있다.
위에 엔티티들 중, 즉시로딩이라고 가정하자.
난 Member만 가져와서 일단 사용해야 되는데, 쓸데없는 Team의 데이터까지 가져오게 된다.
그럼 쓸데없이 더 큰 데이터를 가져오게 된다.
그러니까 일단 디폴트로 지연로딩(LAZY)로 다들 사용하라는 거다.
물론 난 면접을 안다녀서 모르는데.....
CS질문으로 많이 나오는 듯 하다.
N+1 문제
내가 생각한 조회 쿼리만 나가야 하는데, 나오지 않아도 되는 조회 쿼리가 N개가 더 발생
흠... 뭔가 이것만 보면 이해가 될듯 말듯 싶은데....
한번 예시로 바로 보자.
Id | 이름 | 소속 팀 |
---|---|---|
1L | Member1 | TeamA |
2L | Member2 | TeamA |
3L | Member3 | TeamB |
4L | Member4 | TeamB |
Id | 팀 이름 |
---|---|
1L | TeamA |
2L | TeamB |
Member와 Team에 이런 데이터가 있다고 가정하자.
물론 현재는 즉시로딩(EAGER)로 설정한 상태다.
List<Member> findMember1 = em.createQuery("select m from Member m join m.team t", Member.class)
.getResultList();
// SELECT * FROM member JOIN team t ON t.id = m.team
그럼 이런 쿼리들이 보내진다.
Hibernate:
/* select
m
from
Member m
join
m.team t */ select
m1_0.MEMBER_ID,
m1_0.team_TEAM_ID,
m1_0.USERNAME
from
Member m1_0
join
Team t1_0
on t1_0.TEAM_ID=m1_0.team_TEAM_ID
Hibernate:
select
t1_0.TEAM_ID,
t1_0.name
from
Team t1_0
where
t1_0.TEAM_ID=?
Hibernate:
select
t1_0.TEAM_ID,
t1_0.name
from
Team t1_0
where
t1_0.TEAM_ID=?
1. SELECT * FROM member JOIN team t ON t.id = m.team
2. SELECT * FROM team t WHERE t.id = 1
3. SELECT * FROM team t WHERE t.id = 2
이렇게 총 3개의 쿼리가 나간다.
난 그냥 Member 목록들을 조회하고 싶었을 뿐인데... 왜 이렇게 나가냐면
Member들을 조회하다 보니까, 속해있는 Team의 종류가 총 두개다.
그래서 각각 Team을 또 쿼리를 만들어서 각각 조회를 한거다.
그러니까 Team의 갯수가 N이 되는거고, 내가 Member 목록들을 조회한 쿼리가 한개니까
N+1개의 쿼리가 만들어지는거다...
이 N+1의 문제를 해결하는 방법은 간단하게 지연 로딩으로 설정하면 된다.
그래서 실무에서는 무조건!!! 기본으로 LAZY로 설정하라는 뜻이였다.
근데, 지연로딩도 N+1 문제를 만든다.
나중에 그 멤버 조회하면 또 쿼리를 만들어서 던진다.
나중에 한번 블로그를 더 쓸것 같지만, 여기서 일단 알아보자.
List<Member> findMember1 =
em.createQuery("select m from Member m join fetch m.team t", Member.class)
.getResultList();
이런식으로 하면 된다.
JPA에는 Batch Size라는 기능이 있다.
JPA에서 지연로딩을 할 때, 한번에 최대 Batch Size만큼의 Entity를 where절에 "in"으로 가져온다.
spring:
jpa:
properties:
hibernate:
default_batch_fetch_size: 100
이런식으로 전체 다 설정해주거나 (Spring Boot 한정)
@ManyToOne(fetch = FetchType.LAZY)
@BatchSize(size = 100)
private Team team;
이런식으로 한 매핑 관계에서만 어노테이션으로 적용해도 된다.
이 방식은 지연 로딩 방식에서 사용하는게 일반적이다.
근데 이 BatchSize를 너무 작게 잡으면, 그만큼 더 쿼리를 날리게 되고,
너무 크게 잡으면 메모리를 많이 잡아먹는다.
일반적으로 100 ~ 1000개 사이로 잡는듯 싶다.