“자바 ORM 표준 JPA 프로그래밍” 책으로 N+1에 대해 공부했습니다.
이후 기존 조회 로직들을 다시 테스트해보니 N+1 문제가 발생하는게 생각보다 많았습니다. 그래서 이것들을 모두 해결했고, 시리즈로 엮었습니다.
다음 코드는 Grammar 1개를 조회할때 연관된 GrammarBook과 GrammarExample까지 조회하는 service 메서드입니다.
public GrammarDto getGrammar(Long id) {
Grammar grammar = getGrammarById(id);
return GrammarDto.builder()
.id(id)
.sentence(grammar.getSentence())
.grammarBookName(grammar.getGrammarBook().getName())
.grammarExamples(GrammarExampleToGrammarExampleDtoConverter.convert(grammar.getExamples()))
.build();
}
테스트시 생성되는 쿼리는 다음과 같습니다.
Hibernate:
select
g1_0.id,
g1_0.grammar_book_id,
g1_0.sentence
from
grammar g1_0
where
g1_0.id=?
Hibernate:
select
gb1_0.id,
gb1_0.name
from
grammar_book gb1_0
where
gb1_0.id=?
Hibernate:
select
e1_0.grammar_id,
e1_0.id,
e1_0.example,
e1_0.is_answer
from
grammar_example e1_0
where
e1_0.grammar_id=?
Grammar에서 GrammarBook을 지연로딩으로 설정했고, 마찬가지로 GrammarExample에서 Grammar를 지연로딩으로 설정했습니다.
그래서 getGrammarById(id)
를 호출하면 grammar만 불러오는 쿼리를 생성합니다. 이후 grammar.getGrammarBook()
를 호출하면 연관된 grammarBook을 불러오고, grammar.getExamples()
를 호출하면 연관된 grammarExample을 불러오는 쿼리를 생성해서 총 3개의 쿼리가 발생합니다.
JPQL을 작성하면 2번의 fetch join을 해야해서 MultipleBagFetchException
이 발생할 것이라 생각해 다른 방법을 찾던 중, @EntityGraph
를 발견했습니다.
@Repository
public interface GrammarRepository extends JpaRepository<Grammar, Long> {
@EntityGraph(attributePaths = {"examples", "grammarBook"})
Optional<Grammar> findGrammarWithGrammarBookAndExampleById(Long id);
...
}
위와 같이 attributePaths
에 직접 참조할 (연관관계 지정한)필드명을 넣어주면 됩니다. @EntityGraph
는 attributePaths
에 지정한 것들은 즉시 로딩하고, 지정하지 않은 것들은 지연 로딩합니다. 이는 기본값이지만(아래 코드 참고) type
을 설정하면 바꿀수 있습니다.
//EntityGraph.java
...
EntityGraphType type() default EntityGraphType.FETCH;
...
이제 레포지토리에 작성한 메서드를 테스트해보면,
Hibernate:
select
g1_0.id,
e1_0.grammar_id,
e1_0.id,
e1_0.example,
e1_0.is_answer,
g1_0.grammar_book_id,
gb1_0.id,
gb1_0.name,
g1_0.sentence
from
grammar g1_0
left join
grammar_example e1_0
on g1_0.id=e1_0.grammar_id
join
grammar_book gb1_0
on gb1_0.id=g1_0.grammar_book_id
where
g1_0.id=?
N + 1이 발생하지 않습니다. 사실 이는 @EntityGraph
덕분이 아니라 JPA의 fetch join 특징 때문이었습니다.
JPA에서 fetch join시 OneToOne, ManyToOne으로 연관된 필드는 몇 번이든 사용 가능한데, ManyToMany, OneToMany는 한 번만 사용 가능합니다.
Grammar와 GrammarBook은 ManyToOne, Grammar와 GrammarExample은 OneToMany관계이기 때문에 MultipleBagFetchException
이 발생하지 않습니다.
이전에 GrammarBook의 Grammar와 GrammarExample을 한번에 조회할때는 반대로 GrammarBook에서 OneToMany인 Grammar를, Grammar에서 OneToMany인 GrammarExample을 fetch join해서 총 2개의 OneToMany 필드를 fetch join했기 때문에 MultipleBagFetchException
이 발생했습니다.
결과적으로 Grammar조회 문제는 JPQL이 좀 길어서 @EntityGraph
를 사용하는 것으로 마무리했습니다.
'Springboot' 카테고리의 다른 글
[트러블 슈팅]N+1 해결하기 (3) (0) | 2024.02.24 |
---|---|
[트러블 슈팅]N+1 해결하기 (2) (0) | 2024.02.24 |
[트러블 슈팅]LazyInitializationException 간단 해결법 (0) | 2024.02.24 |
[트러블 슈팅]N : 1 양방향 연관 관계에서 주인이 아닌쪽에서 참조 시 null값이 포함되는 문제 (1) | 2024.02.21 |
N : 1 단방향 연관 관계에서 더 나은 조회 쿼리 (0) | 2024.02.21 |