모든 GrammarBook에서 각 book마다 연관된 Grammar 개수를 조회하는 기능이 있습니다. 해당 코드는 다음과 같습니다.
@Transactional(readOnly = true)
public List<GrammarNumOfGrammarBookDto> getGrammarNumOfAllGrammarBooks() {
List<GrammarNumOfGrammarBookDto> responseDtos = new ArrayList<>();
for (GrammarBook grammarBook : this.grammarBookRepository.findAll()) {
responseDtos.add(
GrammarNumOfGrammarBookDto.builder()
.id(grammarBook.getId())
.name(grammarBook.getName())
.grammarNum(grammarBook.getGrammars().size())
.build()
);
}
return responseDtos;
}
위 코드를 테스트하면 다음의 쿼리가 발생합니다.
Hibernate:
select gb1_0.id, gb1_0.name
from grammar_book gb1_0
Hibernate:
select g1_0.grammar_book_id, g1_0.id, g1_0.sentence
from grammar g1_0
where g1_0.grammar_book_id=?
Hibernate:
select g1_0.grammar_book_id, g1_0.id, g1_0.sentence
from grammar g1_0
where g1_0.grammar_book_id=?
Hibernate:
select g1_0.grammar_book_id, g1_0.id, g1_0.sentence
from grammar g1_0
where g1_0.grammar_book_id=?
Hibernate:
select g1_0.grammar_book_id, g1_0.id, g1_0.sentence
from grammar g1_0
where g1_0.grammar_book_id=?
Hibernate:
select g1_0.grammar_book_id, g1_0.id, g1_0.sentence
from grammar g1_0
where g1_0.grammar_book_id=?
Hibernate:
select g1_0.grammar_book_id, g1_0.id, g1_0.sentence
from grammar g1_0
where g1_0.grammar_book_id=?
Hibernate:
select g1_0.grammar_book_id, g1_0.id, g1_0.sentence
from grammar g1_0
where g1_0.grammar_book_id=?
GrammarBook의 개수만큼 쿼리가 발생하고 있습니다.
GrammarBook에서 getGrammars
로 Grammar를 참조할때 지연 로딩으로 인해 N + 1 문제가 발생했습니다.
이처럼 1:N (OneToMany)양방향 연관관계에서 N쪽을 쉽게 참조하기 위해 사용하는 메서드는 지연 로딩시 N + 1 문제가 발생합니다.
반대로 ManyToOne쪽에서 해결한다면 어떨까요?
다음 코드는 GrammarBook의 모든 레코드를 조회해 Map에 grammarBookName을 key로 저장한뒤,
Grammar의 모든 레코드를 조회해 연관된 GrammarBook의 grammarBookName으로 Map의 key를 찾아, value를 1씩 증가시키도록 수정했습니다.
@Transactional(readOnly = true)
public Map<String, Integer> getGrammarNumOfAllGrammarBooks() {
Map<String, Integer> grammarNumOfGrammarBook = new HashMap<>();
for (GrammarBook grammarBook : this.grammarBookRepository.findAll()) {
grammarNumOfGrammarBook.put(grammarBook.getName(), 0);
}
for (Grammar grammar : this.grammarRepository.findAll()) {
String grammarBookName = grammar.getGrammarBook().getName();
int grammarNum = grammarNumOfGrammarBook.get(grammarBookName);
grammarNumOfGrammarBook.replace(grammarBookName, grammarNum+1);
}
return grammarNumOfGrammarBook;
}
위 메서드를 테스트해보면 다음과 같이 2개의 쿼리만 발생합니다. 이는 GrammarBookRepository와 GrammarRepository 각각의 JPA 쿼리 메서드를 한 번씩 사용했기에 의도된 결과입니다.
Hibernate:
select gb1_0.id, gb1_0.name
from grammar_book gb1_0
Hibernate:
select g1_0.id, g1_0.grammar_book_id, g1_0.sentence
from grammar g1_0
DTO대신 Map을 사용하자니 유지보수에 좋지 않아 보입니다.
그래서 group by를 사용해봤습니다.
이때, JPQL에서 DTO클래스를 직접 지정하기 위해 다음과 같이 작성했습니다. 아래 글을 참고했습니다.
@Query("select new com.wordwave.grammarbook.dto.GrammarNumOfGrammarBookDto(g.grammarBook.id, g.grammarBook.name, count(*)) from Grammar g group by g.grammarBook.id")
List<GrammarNumOfGrammarBookDto> findNumOfAllGrammarBooks();
그러면 다음과 같이 한 개의 쿼리만 실행됩니다.
Hibernate:
select g1_0.grammar_book_id, gb1_0.name, count(*)
from grammar g1_0
join grammar_book gb1_0
on gb1_0.id=g1_0.grammar_book_id
group by g1_0.grammar_book_id
[
GrammarNumOfGrammarBookDto(id=5, name=과거와 과거진행, grammarNum=10),
GrammarNumOfGrammarBookDto(id=14, name=현재와 현재진행, grammarNum=10),
GrammarNumOfGrammarBookDto(id=17, name=수동태, grammarNum=10),
GrammarNumOfGrammarBookDto(id=18, name=현재완료, grammarNum=10),
GrammarNumOfGrammarBookDto(id=19, name=미래, grammarNum=10),
GrammarNumOfGrammarBookDto(id=20, name=조동사, grammarNum=3),
GrammarNumOfGrammarBookDto(id=21, name=testbook, grammarNum=1)
]
이로써 N + 1을 해결하고, Map대신 DTO를 활용하도록 수정했습니다.
JPQL 대신 QueryDSL을 사용한다면 좀 더 간결하게 작성할 수 있고,
JPQL은 오타가 있으면 컴파일 단계에서 에러가 발생하지 않고 런타임 에러가 발생하기 때문에 안전하지 않습니다.
그래서 나중에 QueryDSL을 배워볼 예정입니다.
'Springboot' 카테고리의 다른 글
[트러블 슈팅] Spring Cloud Gateway CORS 설정하기 (0) | 2024.12.02 |
---|---|
Spring Cloud Bus & Spring Cloud Config monitor & kafka로 환경설정 무중단 자동 반영하기 (1) | 2024.11.28 |
[트러블 슈팅]N+1 해결하기 (2) (0) | 2024.02.24 |
[트러블 슈팅]N+1 해결하기 (1) (0) | 2024.02.24 |
[트러블 슈팅]LazyInitializationException 간단 해결법 (0) | 2024.02.24 |