먼저 N+1문제란 무엇인지 알기 전에
EAGER Loading (즉시 로딩), Lazy Loading (지연 로딩)에 대해 이해하고 넘어가야만 한다.
예시를 살펴보며 이해해보자
1. Member Entity, Post Entity가 존재한다.
2. Member : Post -> 1 : N 연관관계를 맺는다.
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(mappedBy = "member", fetch = FetchType.LAZY)
private List<Post> posts = new ArrayList<>();
}
@Entity
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long post_id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
}
지금 코드는 LAZY Loading이다.
Member member = new Member();
Post post = new Post();
post.setId(2)
member.setId(1)
member.setPost(post)
// 여기서는 memberRepository에 위와 같은 값들이 DB에 저장되어 있다고 생각하자
memberRepository.findById(1);
자 만약 이러한 코드를 실행시킨다면 어떻게 될까?
memberRepository.findById(1)을 하는 순간 쿼리가 나간다.
쿼리는 단 하나의 Select 밖에 나가지 않는다.
예를 들면
SELECT * FROM MEMBER WHERE id = 1;
이유는 Lazy Loading(지연 로딩)에 있다. Lazy Loading이란 자신과 연관된 Entity를 실제로 사용할 때 연관된 Eentity를 조회하는 방식이기 때문이다. 즉 내가 연관맺은 Entity를 사용하면 나가는 것.
Member member = new Member();
Post post = new Post();
post.setId(2)
member.setId(1)
member.setPost(post)
// 여기서는 memberRepository에 위와 같은 값들이 DB에 저장되어 있다고 생각하자
memberRepository.findById(1);
// 이전과 간은 코드에서 한줄만 추가한다.
System.out.println(member.getPost())
여기서 Post라는 연관 Entity를 사용할 때 그제서야 쿼리가 나간다. 결국 아래와 같은 SELECT 쿼리가 두번 나간다고 생각하면 편하다.
SELECT * FROM MEMBER WHERE id = 1;
SELECT * FROM POST WHERE member_id = 1;
자 그렇다면 이게 왜 문제일까 생각할 수 있겠지만 그 전에 EAGER Loading을 살펴보고 얘기해보자
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(mappedBy = "member", fetch = FetchType.EAGER)
private List<Post> posts = new ArrayList<>();
}
@Entity
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long post_id;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "member_id")
private Member member;
}
이전과 간은 코드에서 FetchType만 EAGER로 바꾼 코드이다.
Member member = new Member();
Post post = new Post();
post.setId(2)
member.setId(1)
member.setPost(post)
// 여기서는 memberRepository에 위와 같은 값들이 DB에 저장되어 있다고 생각하자
memberRepository.findById(1);
// 이전과 간은 코드에서 한줄만 추가한다.
System.out.println(member.getPost())
아까와 똑같은 방식으로 member DB에서 데이터를 조회해 찾았다. 쿼리는 어떻게 될까?
memberRepository.findById(1)을 하는 순간
SELECT * FROM MEMBER m LEFT JOIN POST p ON m.id = p.member_id WHERE m.id = 1;
이런 쿼리가 나간다. 즉 연관관계가 맺어진 Entity를 즉시 로딩하는 것이다.
먼저 위의 쿼리들을 전체적으로 살펴보면 차이가 있다. SELECT를 두번 쓰느냐 한 번 쓰느냐 이것은 당장에 큰 차이가 없어보이지만 Entity의 연관관계는 무수히 많이 맺어져 있을 수 있기 때문에 나중에는 4번 5번 .. 100번이 될수도 있다. 이는 시스템의 성능 저하로 이어진다. 그렇기 때문에 JPA는 지연로딩을 제공한다. 실제 사용되기 전에는 조회를 안하면 쿼리가 더 나가지 않으니까.
그렇다면 EAGER Loading으로 모든 것을 사용면 되겠다라고 생각할 수 있지만 사용하지 않을 Entity를 굳이 조회해 성능을 저하시킬 필요가 없다.
이렇기 때문에 EAGER Loading과 LAZY Loading을 필요할때만 어떤 Loading을 쓸 것인지 적용시킨다.
EAGER Loading에서 LAZY Loading을 시키는 방법은 없기 때문에 보통은 모든 것을 LAZY Loading 시키고
필요하다면 fetch join, EntityGraph를 사용하여 연관된 Entity를 즉시 로딩 한다.
쿼리를 직접보고 시스템 성능을 생각함으로써 공부할 것이 많아진 거 같다. 또한, 이렇게 문제점들을 생각하다보니 다음에 Entity들을 설계할 때는 테이블을 어떻게 분리하는 것이 성능의 좋을지 생각해볼 수 있었다.
마치며 다음에는 EntityGraph, fetch join의 사용법을 알아보겠다.
'스프링 > JPA' 카테고리의 다른 글
[Spring] JPA에서 Entity Life의 Cycle은 어떻게 이루어지나?(면접 질문 있음) (0) | 2024.06.14 |
---|---|
[Spring] JPA Save메소드를 이용한 양방향 매핑 (Gradle) (0) | 2023.01.23 |
[Spring] JPA란 무엇인가 (1) | 2023.01.18 |