서론
다른 사람이 짠 JPA Query를 보면서 N + 1 문제가 발생하는 코드를 발견하였고, 이를 해결하고 말로만 듣던 N + 1 문제가 정확히 어떤 것을 의미하는지를 알아보겠습니다.
JPA의 동작 원리
먼저, N + 1의 문제를 확인하기 전에 알면 이해에 도움되는 간단한 JPA의 동작 원리에대해 알아보겠습니다.
기본적인 동작은 위와 같이 작동합니다. 하지만, 여기서 Fetch 전략에 따라 연관 Entity 처리 방식이 바뀌게 됩니다.
Fetch 전략의 기본 값은 EAGER입니다.
EAGER
조회한 Entity에서 연관 Entity가 있다면 즉시 로딩하는 전략입니다.
먼저, 해당 Entity를 가져와고 연관된 Entity가 있다면 확인 후 개별적으로 Query를 보내게 됩니다.
LAZY
조회한 Entity에서 연관된 Entity가 있다면 Proxy 객체를 만들어 저장합니다.
LAZY 전략은 EAGER 전략과 다르게 직접 DB에 접근하여 연관 Entity를 가져오지 않고, Proxy 객체를 만들어 저장합니다.
Proxy 객체란?
실제 Entity 객체 대신에 만들어진 가짜 Entity 객체라고 보면 됩니다.
불필요한 데이터 로딩을 지연시키는 역할을 하며, 실제 데이터를 필요로 하면 DB에 조회하는 방식입니다.
DB 구조
사용되는 DB 구조는 다음과 같습니다.
N + 1 발생 구간
EAGER
즉시 로딩 전략인 EAGER는 위 JPA 동작 원리에서 봤다시피 Entity 조회 시 바로 발생합니다.
@Test
@DisplayName("댓글 ID로 조회 성공")
void findByCommentId() {
commentRepository.findByCommentId(1L);
}
Comment를 가져오면서 연관된 Entity였던 Board와 AppUser가 같이 Loading 되는 것을 볼 수 있습니다.
여기서 가장 중요한 점은 연관된 Entity가 JOIN을 통해 가져온 것이 아닌 개별 쿼리로 작동했다는 것입니다.
LAZY
필요한 시점에서만 DB에 조회해 가져오는 LAZY 전략은 조회 시에는 N + 1 문제가 발생하지 않습니다.
하지만, 반복문 내에서 연관된 Entity를 가져오기 시작한다면 N + 1 문제가 발생하기 시작합니다.
@Test
@DisplayName("모든 댓글 조회 성공")
void findAllBy() {
List<Comment> comments = commentRepository.findAllBy();
for (Comment comment : comments) {
log.info("Comment uploader name = {}", comment.getUser().getNickname());
}
}
연관 Entity였던 AppUser 객체의 닉네임이 필요해 getUser().getNickName()을 호출하게 되어 추가 쿼리가 발생한 것을 볼 수 있습니다.
해결 방법
위와 같이 Fetch 전략에서 EAGER를 남발하게 되면 연관 Entity에서 연관 Entity를 타고 들어가는 현상을 경험하실 수 있게 됩니다. 또한, 양방향 매핑이라면 그만큼 더 쿼리를 호출하게 됩니다.
우선, 필요한 연관 Entity가 아니라면 꼭 LAZY 전략을 습관화 하도록 합시다.
JPQL
첫 번째 방법으로는 JPQL을 활용하는 것입니다. JPQL은 개발자가 직접 쿼리를 짤 수 있는 장점이 존재하기 때문에 Join 문을 사용하여 쉽게 해결할 수 있습니다.
특정 컬럼명만 가져오거나, SQL 문이 복잡한 상황이라면 적극적으로 사용할 수 있는 방법입니다.
@Query("SELECT c, u, b FROM Comment c" +
" JOIN FETCH c.user u" +
" JOIN FETCH c.board b")
List<Comment> findAllBy();
EntityGraph
연관 Entity를 단순 Join을하여 가져오기 좋은 방법입니다.
JPQL과 다르게 SQL 문을 조작할 수 없으며, 꼭 가져오고자 하는 연관 Entity의 변수명을 attributePaths에 적어야합니다.
@EntityGraph(attributePaths = {"user", "board"})
List<Comment> findAllBy();
결과
'Project 하면서 알아가는 것들' 카테고리의 다른 글
MultipleBagException의 발생 원인과 해결 방법 (0) | 2025.03.25 |
---|---|
@Transactional의 readonly와 OSIV는 뭐 하는 친구일까? (0) | 2025.03.19 |
[Spring Boot + Python] Kafka를 사용해보자 (4) | 2024.09.30 |
Kafka와 RabbitMQ를 알아보자 (0) | 2024.08.20 |
[Nextjs] Tiptap 사용법과 커스텀마이징 기능 구현 (1) | 2024.02.28 |