0. N:1 환경설정
@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Parent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
}
@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Child {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@ManyToOne(fetch = FetchType.EAGER) // default: EAGER
private Parent parent;
}
@BeforeEach로 DB 구성
총 parent 2개, 각 child 2개씩
영속성 컨텍스트(EntityManager) 캐시에 영향받지 않는 쿼리를 발생시키기 위하여 영속성 컨텍스트를 비워줌
final int CHILD_COUNT = 2;
@BeforeEach
void before() {
Parent parent1 = parentRepository.save(
Parent.builder()
.name("testName1")
.build()
);
for(int i = 0; i < CHILD_COUNT; i++)
childRepository.save(
Child.builder()
.parent(parent1)
.build()
);
Parent parent2 = parentRepository.save(
Parent.builder()
.name("testName2")
.build()
);
for(int i = 0; i < CHILD_COUNT; i++)
childRepository.save(
Child.builder()
.parent(parent2)
.build()
);
em.flush();
em.clear();
System.out.println("******************************* Test Start *******************************");
}
Test용 print 함수 정의
<T> void print(T data) {
System.out.println("[" + data + "]");
}
1. N:1 관계에서 EAGER 로딩 사용
1-1. 단일 객체 조회 시
@Test
void test() {
Child child = em.find(Child.class, 1L); // 맨 처음 진행되는 테스트라서 그냥 1L로 대충 설정하였음..
}
Hibernate:
select
child0_.id as id1_0_0_,
child0_.parent_id as parent_i2_0_0_,
parent1_.id as id1_1_1_
from
child child0_
left outer join
parent parent1_
on child0_.parent_id=parent1_.id
where
child0_.id=?
발생하는 쿼리
- JPA 내부적으로 최적화하여 left outter join 쿼리가 발생하는 것을 볼 수 있음
발생할 수 있는 문제
- EAGER 로딩 되는 객체가 많으면 join되는 테이블이 많아져, 성능 저하 문제를 일으킬 수 있다.
해결방법
- EAGER 로딩을 사용하지 않고 LAZY 로딩을 사용한다.
1-2. 다중 객체 조회 시
@Test
void test() {
List<Child> children = childRepository.findAll();
// 또는 JPQL 직접 사용
List<Child> children = em.createQuery("select c from Child c", Child.class).getResultList();
}
Hibernate:
select
child0_.id as id1_0_,
child0_.parent_id as parent_i2_0_
from
child child0_
Hibernate:
select
parent0_.id as id1_1_0_
from
parent parent0_
where
parent0_.id=?
Hibernate:
select
parent0_.id as id1_1_0_
from
parent parent0_
where
parent0_.id=?
발생하는 쿼리
- child 객체를 모두 조회하고 이후에 parent 필드의 EAGER 로딩을 위한 추가 쿼리가 나가는 것을 볼 수 있음
발생할 수 있는 문제
- 그 유명한 N + 1 문제가 발생한다.
해결방법
- N + 1 문제의 해결 방법은 맨 아래에서.
2. N:1 관계에서 LAZY 로딩 사용
2-1. 단일 객체 조회 시
@Test
void test() {
Child child = em.find(Child.class, 1L);
Parent parent = child.getParent();
print(parent.getId());
print(parent.getName());
}
Hibernate:
select
child0_.id as id1_0_0_,
child0_.parent_id as parent_i2_0_0_
from
child child0_
where
child0_.id=?
[1]
Hibernate:
select
parent0_.id as id1_1_0_,
parent0_.name as name2_1_0_
from
parent parent0_
where
parent0_.id=?
[testName1]
발생하는 쿼리
- LAZY 로딩이기 때문에 parent 필드를 실질적으로 참조할 때 쿼리가 발생한다.
- 단순히 parent의 id를 참조할 때는 쿼리가 나가지 않는다. (FK를 통해 바로 참조할 수 있기 때문인 것 같다)
- 하지만 id 필드 외에 다른 필드의 참조를 시도하면 쿼리가 발생한다.
발생할 수 있는 문제
- 준영속 상태인 객체의 프록시 객채로 초기화되어 있는 필드를 참조하면 org.hibernate.LazyInitializationException: could not initialize proxy - no Session 문제가 발생할 수 있다.
해결방법
- @Transactional 을 사용하여 트랜잭션 유지시키기
2-2. 다중 객체 조회 시
@Test
void test0() {
List<Child> children = childRepository.findAll();
for(var c : children) {
Parent parent = c.getParent();
print(parent.getId());
print(parent.getName());
}
}
Hibernate:
select
child0_.id as id1_0_,
child0_.parent_id as parent_i2_0_
from
child child0_
[1]
Hibernate:
select
parent0_.id as id1_1_0_,
parent0_.name as name2_1_0_
from
parent parent0_
where
parent0_.id=?
[testName1]
[1]
[testName1]
[2]
Hibernate:
select
parent0_.id as id1_1_0_,
parent0_.name as name2_1_0_
from
parent parent0_
where
parent0_.id=?
[testName2]
[2]
[testName2]
발생하는 쿼리
- LAZY 로딩이기 때문에 parent 필드를 실질적으로 참조할 때 쿼리가 발생한다.
발생할 수 있는 문제
- EAGER 로딩과 동일하게 N + 1 문제가 발생한다.
해결 방법
- N + 1 문제의 해결 방법은 맨 아래에서.
3. N:1 양방향 관계 설정 후 Parent에서 EAGER 로딩 사용
Parent 필드에 N:1 양방향 관계 적용
@OneToMany(mappedBy = "parent", fetch = FetchType.EAGER)
@Default
private Set<Child> children = new HashSet<>();
3-1. 모든 Parent 조회
@Test
void test() {
List<Parent> parents = parentRepository.findAll();
}
Hibernate:
select
parent0_.id as id1_1_,
parent0_.name as name2_1_
from
parent parent0_
Hibernate:
select
children0_.parent_id as parent_i3_0_0_,
children0_.id as id1_0_0_,
children0_.id as id1_0_1_,
children0_.name as name2_0_1_,
children0_.parent_id as parent_i3_0_1_
from
child children0_
where
children0_.parent_id=?
Hibernate:
select
children0_.parent_id as parent_i3_0_0_,
children0_.id as id1_0_0_,
children0_.id as id1_0_1_,
children0_.name as name2_0_1_,
children0_.parent_id as parent_i3_0_1_
from
child children0_
where
children0_.parent_id=?
발생하는 쿼리
- Parent를 모두 조회하고 이후에 Children을 조회하는 것을 볼 수 있음
발생할 수 있는 문제
- N + 1 문제가 발생한다.
해결방법
- N + 1 문제의 해결 방법은 맨 아래에서.
예상 결과
- 현재 상황에서 LAZY 로딩을 적용하더라도 결국에는 N + 1 문제가 발생할 것으로 보인다.
N + 1 문제 해결 방법
1. @BatchSize(size = ?) 적용
EAGER 로딩을 사용하면 @BatchSize가 동작하지 않으므로 EAGER 로딩 사용은 전제 조건에서 배제된다.
@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Parent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// EAGER 로딩을 사용하면 @BatchSize는 무시됨
@OneToMany(mappedBy = "parent", fetch = FetchType.LAZY)
@BatchSize(size = 100)
@Default
private Set<Child> children = new HashSet<>();
}
전체 조회
@Test
void test() {
List<Parent> parents = parentRepository.findAll();
for(var p : parents) {
System.out.println(p.getChildren().size());
}
}
발생 쿼리
Hibernate:
select
parent0_.id as id1_1_,
parent0_.name as name2_1_
from
parent parent0_
2022-08-09 17:01:02.765 TRACE 22384 --- [ Test worker] o.h.type.descriptor.sql.BasicExtractor : extracted value ([id1_1_] : [BIGINT]) - [1]
2022-08-09 17:01:02.769 TRACE 22384 --- [ Test worker] o.h.type.descriptor.sql.BasicExtractor : extracted value ([name2_1_] : [VARCHAR]) - [testParent1]
2022-08-09 17:01:02.770 TRACE 22384 --- [ Test worker] o.h.type.descriptor.sql.BasicExtractor : extracted value ([id1_1_] : [BIGINT]) - [2]
2022-08-09 17:01:02.770 TRACE 22384 --- [ Test worker] o.h.type.descriptor.sql.BasicExtractor : extracted value ([name2_1_] : [VARCHAR]) - [testParent2]
Hibernate:
select
children0_.parent_id as parent_i3_0_1_,
children0_.id as id1_0_1_,
children0_.id as id1_0_0_,
children0_.name as name2_0_0_,
children0_.parent_id as parent_i3_0_0_
from
child children0_
where
children0_.parent_id in (
?, ?
)
2022-08-09 17:01:02.788 TRACE 22384 --- [ Test worker] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [BIGINT] - [1]
2022-08-09 17:01:02.789 TRACE 22384 --- [ Test worker] o.h.type.descriptor.sql.BasicBinder : binding parameter [2] as [BIGINT] - [2]
2022-08-09 17:01:02.795 TRACE 22384 --- [ Test worker] o.h.type.descriptor.sql.BasicExtractor : extracted value ([id1_0_0_] : [BIGINT]) - [1]
2022-08-09 17:01:02.796 TRACE 22384 --- [ Test worker] o.h.type.descriptor.sql.BasicExtractor : extracted value ([name2_0_0_] : [VARCHAR]) - [testChild]
2022-08-09 17:01:02.796 TRACE 22384 --- [ Test worker] o.h.type.descriptor.sql.BasicExtractor : extracted value ([parent_i3_0_0_] : [BIGINT]) - [1]
2022-08-09 17:01:02.797 TRACE 22384 --- [ Test worker] o.h.type.descriptor.sql.BasicExtractor : extracted value ([parent_i3_0_1_] : [BIGINT]) - [1]
2022-08-09 17:01:02.798 TRACE 22384 --- [ Test worker] o.h.type.descriptor.sql.BasicExtractor : extracted value ([id1_0_1_] : [BIGINT]) - [1]
2022-08-09 17:01:02.807 TRACE 22384 --- [ Test worker] o.h.type.descriptor.sql.BasicExtractor : extracted value ([id1_0_0_] : [BIGINT]) - [2]
2022-08-09 17:01:02.808 TRACE 22384 --- [ Test worker] o.h.type.descriptor.sql.BasicExtractor : extracted value ([name2_0_0_] : [VARCHAR]) - [testChild]
2022-08-09 17:01:02.808 TRACE 22384 --- [ Test worker] o.h.type.descriptor.sql.BasicExtractor : extracted value ([parent_i3_0_0_] : [BIGINT]) - [1]
2022-08-09 17:01:02.809 TRACE 22384 --- [ Test worker] o.h.type.descriptor.sql.BasicExtractor : extracted value ([parent_i3_0_1_] : [BIGINT]) - [1]
2022-08-09 17:01:02.810 TRACE 22384 --- [ Test worker] o.h.type.descriptor.sql.BasicExtractor : extracted value ([id1_0_1_] : [BIGINT]) - [2]
2022-08-09 17:01:02.812 TRACE 22384 --- [ Test worker] o.h.type.descriptor.sql.BasicExtractor : extracted value ([id1_0_0_] : [BIGINT]) - [3]
2022-08-09 17:01:02.813 TRACE 22384 --- [ Test worker] o.h.type.descriptor.sql.BasicExtractor : extracted value ([name2_0_0_] : [VARCHAR]) - [testChild]
2022-08-09 17:01:02.813 TRACE 22384 --- [ Test worker] o.h.type.descriptor.sql.BasicExtractor : extracted value ([parent_i3_0_0_] : [BIGINT]) - [2]
2022-08-09 17:01:02.813 TRACE 22384 --- [ Test worker] o.h.type.descriptor.sql.BasicExtractor : extracted value ([parent_i3_0_1_] : [BIGINT]) - [2]
2022-08-09 17:01:02.814 TRACE 22384 --- [ Test worker] o.h.type.descriptor.sql.BasicExtractor : extracted value ([id1_0_1_] : [BIGINT]) - [3]
2022-08-09 17:01:02.815 TRACE 22384 --- [ Test worker] o.h.type.descriptor.sql.BasicExtractor : extracted value ([id1_0_0_] : [BIGINT]) - [4]
2022-08-09 17:01:02.817 TRACE 22384 --- [ Test worker] o.h.type.descriptor.sql.BasicExtractor : extracted value ([name2_0_0_] : [VARCHAR]) - [testChild]
2022-08-09 17:01:02.818 TRACE 22384 --- [ Test worker] o.h.type.descriptor.sql.BasicExtractor : extracted value ([parent_i3_0_0_] : [BIGINT]) - [2]
2022-08-09 17:01:02.818 TRACE 22384 --- [ Test worker] o.h.type.descriptor.sql.BasicExtractor : extracted value ([parent_i3_0_1_] : [BIGINT]) - [2]
2022-08-09 17:01:02.819 TRACE 22384 --- [ Test worker] o.h.type.descriptor.sql.BasicExtractor : extracted value ([id1_0_1_] : [BIGINT]) - [4]
2
2
1-2. 각 Parent의 Child 개수를 200개로 늘리고 @BatchSize(size = 5) 다시 수행
발생 쿼리
Hibernate:
select
parent0_.id as id1_1_,
parent0_.name as name2_1_
from
parent parent0_
Hibernate:
select
children0_.parent_id as parent_i3_0_1_,
children0_.id as id1_0_1_,
children0_.id as id1_0_0_,
children0_.name as name2_0_0_,
children0_.parent_id as parent_i3_0_0_
from
child children0_
where
children0_.parent_id in (
?, ?
)
200
200
발생한 쿼리를 통해 알 수 있는 점
- @BatchSize를 이용하면 내부적으로 IN절을 사용하여 N+1 문제를 해결할 수 있다.
- @BatchSize의 size를 지정하여도 내부적으로 몇 개의 쿼리가 나가는지는 정확히 알 수 없다. (짐작만 가능)
- @BatchSize를 이용하기 까다로운 경우 직접 IN 절을 사용하여 최적화를 시도할 수 있다.
2. Fetch Join 사용
JPQL 직접 작성
@Repository
public interface ParentRepository extends JpaRepository<Parent, Long> {
@Query("select distinct p from Parent p join fetch p.children")
List<Parent> findAllUsingFetch();
}
전체 조회
@Test
void test() {
List<Parent> parents = parentRepository.findAllUsingFetch();
for(var p : parents) {
System.out.println(p.getChildren().size());
}
}
발생 쿼리
Hibernate:
select
distinct parent0_.id as id1_1_0_,
children1_.id as id1_0_1_,
parent0_.name as name2_1_0_,
children1_.name as name2_0_1_,
children1_.parent_id as parent_i3_0_1_,
children1_.parent_id as parent_i3_0_0__,
children1_.id as id1_0_0__
from
parent parent0_
inner join
child children1_
on parent0_.id=children1_.parent_id
200
200
발생한 쿼리를 통해 알 수 있는 점
- Fetch Join을 이용하면 내부적으로 inner join이 발생하여 한 번에 N+1 문제를 해결할 수 있다.
- 다만 OneToMany 또는 ManyToMany의 경우 중복 데이터가 발생할 수 있다.
- 따라서 distinct를 사용하여 중복 데이터를 제거해줘야 한다.
- 반면에 OneToOne, ManyToOne은 데이터 중복이 발생하지 않는다.
EntityGraph 방식은 추후 정리 예정
'JPA' 카테고리의 다른 글
OneToOne 정리 (0) | 2022.08.29 |
---|---|
JPA fetch join 대상의 별칭 사용과 주의사항 (0) | 2022.08.16 |
fetch join과 paging 시 OOM 문제 (OneToOne은 괜찮지) (0) | 2022.08.10 |
JPA 동시성 문제 해결하기 (낙관적 락, 비관적 락) (0) | 2022.08.01 |
JOIN vs Fetch JOIN with JPA (0) | 2022.06.24 |