1:N 관계의 테이블들이 얽혀 있을 때
게시글 조회같이 여러 건의 데이터를 전체 조회할 때 주로 발생한다.
✔️ N+1문제
게시글 목록같은 경우
전체 게시글 개수인 9개의 데이터를 가져오기 위해 쿼리문을 1번 날리고,
➡️ select * from post;
그 후에 각 게시글의 작성자를 찾기 위해 쿼리문이 n번만큼 날린다.
이때, 중복된 작성자가 있다면 1번만 쿼리문이 나가고, 익명유저는 null이니 제외된다.
➡️ select * from author where id = "213322132"
➡️ select * from author where id = "yj@naver.com"
총 3개의 쿼리문이 나간 것을 확인할 수 있다.
💡 JPA의 두 가지 로딩방식
JPA는 기본적으로 위 방식인 지연로딩 "LAZY" 방식을 지향한다.
왜냐면 "그때그때 데이터가 필요한 상황이 발생하면 데이터를 가져와, 메모리&작업 효율성을 지키겠다"는 것이다.
하지만 위의 경우처럼 게시글마다 작성자 정보를 무조건 가져와 화면에 뿌려줘야 하는 경우,
= 개발자가 봤을 때, n+1번 호출해 데이터가 모두 필요한 화면이라고 생각되면
즉시 로딩 방식을 사용하여 쿼리문 1개를 통해 한 번에 모든 데이터를 가져오는 것이 더 효율적이다.
select a.* p.* from author a left join post p on a.id = p.author_id;
즉, n+1 문제는 즉시 로딩 방식을 통해 해결할 수 있다는 뜻이 된다.
한 번 더 정리하자면, 한 번에 관련된 모든 데이터를 같이 가져오는 것을 즉시로딩 "EAGER" 방식이라 한다.
- 1:N 관계에서 N쪽은 즉시 로딩이 디폴트
- 단 건 Post객체는 항상 한 명의 작성자를 가지기 때문에 디폴트가 즉시로딩이다.
- 하지만 즉시로딩은 자원을 사용하지 않을 때 낭비가 발생할 수 있으므로 지연 로딩 방식이 권장된다.
- Post 테이블
현재 N쪽 관계를 가지기 때문에 즉시로딩이 디폴트이며, 지연로딩을 걸어줘야 한다.
즉시로딩은 로그 1개가 남고, 지연로딩은 2개가 남는다.
@OneToMany(mappedBy = "author", cascade = CascadeType.ALL, fetch=FetchType.LAZY) //Post 객체에 있는 변수명을 적어 매핑관계 표현
private List<Post> posts;
- Author 테이블
지연로딩은 지연로딩이 디폴트이지만, 가시적으로 코드에 작성해 JPA에게 알려주는 것이 일반적이다.
@ManyToOne(fetch = FetchType.LAZY) // 관계성 JPA에게 알리기
@JoinColumn(name = "author_id")
//@JoinColumn(nullable=false, name = "author_email", referencedColumnName = "email")
private Author author;
JOIN vs FETCH JOIN
N+1 문제를 해결하기 위해 즉시로딩 방식을 사용할 경우,
FETCH JOIN문을 사용하면 1번의 쿼리문으로 데이터를 모두 가져다 놓을 수 있다.
이때, JOIN문과 FETCH JOIN문의 차이를 살펴보자.
JPQL로 join과 fetch join을 사용한 두 개의 쿼리 메세지를 작성했다.
@Query("select p from Post p left join p.author")
의미 : select p.* from post p inner join author a on p.author_id = a.id whiere a.name like 'hong%';
author 데이터를 같이 가지고 오지 않아, p.author를 접근하고 싶다면 다시 n번만큼의 쿼리를 보내야 한다.
위 코드처럼 where문에 a의 특정 속성을 사용해 해당 기준을 만족하는 row만 가져오고 싶을 경우 사용.
@Query("select p from Post p left join fetch p.author")
의미 : select p.* a.* from post p inner join author a on p.author_id = a.id whiere a.name like 'hong%';
post와 author를 한 번에 같이 가지고 오기 때문에 p.author에 바로 접근할 수 있다.
fetch join을 할 경우 쿼리의 개수가 줄어드는 것을 확인해보자.
먼저 PostRepository에 JPQL을 사용해 findAllJoin()과 findAllFetchJoin() 메서드를 만들어준다.
[ PostRepository.interface ]
@Repository
public interface PostRepository extends JpaRepository<Post, Long> {
// select p.* from post p left join author a on p.author_id = a.id;
// 아래 JPQL의 join문은 quthor 객체를 통해 post를 스크리닝(필터링)하고 싶은 상황에 적합
@Query("select p from Post p left join p.author") //JPQL문 : 객체 지향의 raw query. Jpa와 다르게 컴파일 타임에서 오류를 잡아준다.
List<Post> findAllJoin();
// select p.* a.* from post p left join author a on p.author_id = a.id;
@Query("select p from Post p left join fetch p.author")
List<Post> findAllFetchJoin();
}
쿼리의 수를 세고자 하는 화면은 아래와 같다.
case 1. 일반 join
public List<PostListResDto> findAll(){
List<Post> Posts = postRepository.findAllJoin();
// List<Post> Posts = postRepository.findAll();
List<PostListResDto> PostListResDtos = new ArrayList<>();
for(Post post : Posts){
PostListResDto postListResDto = new PostListResDto();
postListResDto.setId(post.getId());
postListResDto.setTitle(post.getTitle());
postListResDto.setAuthor_email(post.getAuthor()==null?"익명유저":post.getAuthor().getEmail());
// postListResDto.setAuthor_email(post.getAuthor().getEmail()); //⭐ post객체에 있는 author_id로 Author 테이블에서 꺼내온 autohr 객체
PostListResDtos.add(postListResDto);
}
return PostListResDtos;
}
일반 join의 경우 n+1 문제가 발생해, 총 5개의 쿼리가 나가고 있다.
case 2. fetch join
public List<PostListResDto> findAll(){
List<Post> Posts = postRepository.findAllFetchJoin();
// List<Post> Posts = postRepository.findAll();
List<PostListResDto> PostListResDtos = new ArrayList<>();
for(Post post : Posts){
PostListResDto postListResDto = new PostListResDto();
postListResDto.setId(post.getId());
postListResDto.setTitle(post.getTitle());
postListResDto.setAuthor_email(post.getAuthor()==null?"익명유저":post.getAuthor().getEmail());
// postListResDto.setAuthor_email(post.getAuthor().getEmail()); //⭐ post객체에 있는 author_id로 Author 테이블에서 꺼내온 autohr 객체
PostListResDtos.add(postListResDto);
}
return PostListResDtos;
}
fetch join의 경우 한 번에 데이터를 끌고 오기 때문에, 총 1개의 쿼리가 나가고 있다.
'Back-End 공부 > Spring' 카테고리의 다른 글
[스프링] 스케줄링 cron 사용하기 (1) | 2024.02.03 |
---|---|
[스프링] dirtychecking(변경 감지)과 cascading(삽입, 삭제) (0) | 2024.01.26 |
[스프링] Paging 기능 구현 (0) | 2024.01.26 |
Spring에서 발생하는 대표적인 순환참조 문제 해결하기 (0) | 2024.01.24 |
생성자 객체 생성 방식의 단점을 극복한 builder 패턴 (1) | 2024.01.23 |