TIL

TIL 2024-05-09 N+1, 그리고 Fetch join

wonow_ 2024. 5. 9. 14:24

 
JPA 를 사용하면서 지연로딩과 즉시로딩에 대한 개념을 알고 있었다.
근데 얘네 덕분에 N+1이 발생한다는 사실을 알았다.
사실 n+1에 대해서만 쪼오오끔 알구.. 쿼리dsl로 넘어 갔었어서 정확히 몰랐었다 ㅎㅎ… 이번 프로젝트에소 JPA 위주로 사용하니까 짚고 가려고 한다
 

즉시로딩

@Entity
public class Board {

    @Id
    private Long id;
}

@Entity
public class Post {

    @Id
    private Long id;

    @ManyToOne
    @JoinColumn(name = "board_id")
    private Board board;
    
}

 
이런 식의 엔티티들이 있다고 치자
 

@Service
@RequiredArgsConstructor
public class PostService {

    private final PostRepository postRepository;

    public void getPost(Long postId) {

        Post post = postRepository.findById(postId);
    }
}

 
여기서 post를 조회하는 쿼리는 어떻게 나갈까?
 

select
    p, b
from
    Post p
inner join
    p.team t
where p.id = :postId

 
이런 식으로 나간다.
 
어.. 나는 포스트만 조회하고 싶은데... 필요 없는 Team 까지 join을 해서 불러온다.
즉 로딩시간의 낭비가 조금이라도 생기는 것
이게 즉시로딩이다
 

지연로딩

그럼 FetchType Lazy를 설정하자!! 이게 지연로딩을 할 수 있게 해준다.

@Entity
public class Board {

    @Id
    private Long id;
}

@Entity
public class Post {

    @Id
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "board_id")
    private Board board;
    
}

 

@Service
@RequiredArgsConstructor
public class PostService {

    private final PostRepository postRepository;

    public void getPost(Long postId) {

        Post post = postRepository.findById(postId);
    }
}

 
이 findById 메서드를 호출하면 쿼리는 어떻게 나갈까?
 

select
    p
from
    Post p
where p.id = :postId

 
이런 식으로 나가게 된다.
post에 있던 board 까지 안불러 온다는 것
와우 그럼 해결 됐다!!! 룰루랄라
 
할 수 있는데
 

@Service
@RequiredArgsConstructor
public class PostService {

    private final PostRepository postRepository;

    public void getPost(Long postId) {

        Post post = postRepository.findById(postId);
        Board board = post.getBoard(); // getter 가 있다고 가정
    }
}

 
이렇게 board 를 불러오는 코드를 추가하면
 

select
    p, b
from
    Post p
inner join
    p.team t
where p.id = :postId

 
다시 이런 쿼리가 나가게 된다.
 
만약에 Post를 여러개 조회하고 싶으면?

@Service
@RequiredArgsConstructor
public class PostService {

    private final PostRepository postRepository;

    public void getPostList() {

        List<Post> postList = postRepository.findAll();
        // Board board = post.getBoard(); // getter 가 있다고 가정
    }
}

 
이건 Lazy를 설정했기에 쿼리가 한번만 나간다.
 
근데 Post 안에 있는 Board 를 불러오고 싶다면?
 

@Service
@RequiredArgsConstructor
public class PostService {

    private final PostRepository postRepository;

    public void getPostList() {

        List<Post> postList = postRepository.findAll();
        
        for (Post post : PostList) {
        
        	post.getBoard();
        }
    }
}

 
post의 갯수만큼 추가적인 쿼리가 계속 나간다.
 
N+1 문제가 발생하는 것이다.
 

여기까지 정리

1. JPA는 기본적으로 연관관계에 있는 모든 엔티티를 조회할 수 있게 쿼리를 날리려고 한다.
2. 그래서 실무에서는 쿼리의 성능을 위해 FetchType을 기본적으로 Lazy로 설정한다.
3. 근데 단일 엔티티만 끌고 온 상황에서, 참조를 조회하고 싶으면 N+1 이 발생한다.
 

어떻게 해결?

Fetch join 을 사용하면된다.
 
Fetch join은 해당 엔티티의 연관관계를 모두 불러오게 해준다.
 
아 그러면 엔티티 단일만 필요할때는 Fetch join 사용하지 말고
엔티티 연관관계를 써야 되는 메서드는 Fetch join 하면 되겠다
라고 생각하면 된다.
 

public interface PostRepository extends JPARepository<Post, Long> {

    @Query("selet p from Post p join fetch p.board b where p.id = :postId")
    Post findByIdFetch(@Param("postId") Long id);
}

 
음.. 이런 식으로 작성하면 된다.
 
다른 방법도 있는데, Batch Size나 등등.. 근데 결국 fetch 를 많이 사용한다고 한다.