[JPA] N+1 문제, Fetch Join
팀 프로젝트에서 글 검색 api를 짜다가 N+1문제를 처음 접하게 되어 n+1문제를 fetch join으로 해결한 과정을 기록 한 글이다.
우선 엔티티는 4개가 있다.
- User
- Post
- PostTechStack
- Tag
- User와 Post는 1:N의 관계이고 양방향 연관관계를 가진다.
- Post와 Tag는 N:M의 관계이고, 다대다를 가운데 PostTechStck 엔티티를 둬서 풀어줬다.
- Post는 PostTechStack과 1:N의 관계이고 양방향 연관관계를 가진다.
- Tag는 PostTechStack과 1:N의 관계이고 양방향 연관관계를 가진다.
User
@Getter
@NoArgsConstructor
@Table(uniqueConstraints = {
@UniqueConstraint(columnNames = {
"name"
})
})
@Entity
public class User extends BaseTimeEntity {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Long id;
@NotBlank
private String name;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Role role;
@NotBlank
private String socialLoginId;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private AuthProvider authProvider;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
private List<Post> posts = new ArrayList<>();
}
Post
@Getter
@NoArgsConstructor
@Entity
public class Post extends BaseTimeEntity {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "post_id")
private Long id;
private Long likeCount;
private Long viewCount;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL)
private List<PostTechStack> postTechStacks = new ArrayList<>();
@Enumerated(EnumType.STRING)
private PostStatus status;
@NotBlank
private String title;
@Lob
@NotBlank
private String content;
}
PostTechStack
@Getter
@NoArgsConstructor
@Entity
public class PostTechStack extends BaseTimeEntity {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "post_tech_stack_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id")
private Post post;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "tag_id")
private Tag tag;
}
Tag
@Getter
@NoArgsConstructor
@Table(uniqueConstraints = {
@UniqueConstraint(columnNames = {
"name"
})
})
@Entity
public class Tag extends BaseTimeEntity {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "tag_id")
private Long id;
@Column(nullable = false)
@NotBlank
private String name;
@OneToMany(mappedBy = "tag", cascade = CascadeType.ALL)
private List<PostTechStack> postTechStacks = new ArrayList<>();
private Tag(String name){
this.name = name;
}
}
아래 코드는 사용자가 작성한 글을 불러오는 컨트롤러이다.
- User 정보를 조회한다.
- getPosts()로 Post 리스트를 조회하고 DTO로 변환 후 반환한다.
- Post의 태그 정보를 찾기 위해 getPostTechStacks()로 postTechStack 리스트를 조회한다.
- TagResponseDto.postTechStackToDto()호출로 Tag를 조회하고 DTO로 변환한다.
- PostTechStack.getTag().getName()으로 태그 이름을 담은 TagResponseDto를 만든다.
이제 전체 실행된 쿼리를 봐보자.
지연 로딩에서 쿼리가 발생하는 때는 실제 그 객체의 조회가 필요한 시점이다. 그래서 getXXX()만 할 때는 쿼리가 나가지 않고, 객체의 필드에 접근할 때 쿼리가 나간다.
1) User 조회
User를 조회하는 쿼리가 1번 발생한다.
2) Post조회
User를 조회할 때 posts는 지연 로딩이기 때문에 stream()으로 DTO로 변환하면서 실제 객체 조회가 필요한 시점에 Post에 대한 쿼리가 나간다.
즉, N+1문제가 발생한다.
1번(기본 조회 쿼리) + N번(예상치 못하게 추가적으로 발생하는 쿼리) = N+1
지금은 User 1번 + Post 1번으로 2번 밖에 조회 쿼리가 나가지 않지만,
만약 User가 10만 개 있다면? 각각 User마다 Post가 있는지 확인하기 위해 쿼리를 보내면서 많은 DB 조회가 발생하게 될것이다.
여기서만 끝나는것이 아니다.
3) PostTechStack 조회
태그 정보를 조회하기 위해 PostTechStack.getTag()로 PostTechStack의 필드에 접근하게 되고 여기서 또 N+1문제가 발생한다.
4) Tag 조회
객체지향적으로 설계하고자 여러 곳에서 양방향 연관관계를 설정했는데, 이로 인해서
N+1문제가 계속 발생했다.
엔티티 연관관계 매핑을 잘못해서 발생한 문제라고 생각되어, 양방향 연관관계를 단방향 연관관계로 바꾸고 fetch join을 사용 하도록 코드를 수정했다.
User엔티티에 posts두지 않고 Post조회가 필요한 경우 PostTechStack에서 post와 tag를 fetch join으로 한번에 구해오기
User
@Getter
@NoArgsConstructor
@Table(uniqueConstraints = {
@UniqueConstraint(columnNames = {
"name"
})
})
@Entity
public class User extends BaseTimeEntity {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Long id;
@NotBlank
private String name;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Role role;
@NotBlank
private String socialLoginId;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private AuthProvider authProvider;
}
Post
@Getter
@NoArgsConstructor
@Entity
public class Post extends BaseTimeEntity {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "post_id")
private Long id;
private Long likeCount;
private Long viewCount;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
@OneToMany(mappedBy = "post")
private List<Comment> comments = new ArrayList<>();
@Enumerated(EnumType.STRING)
private PostStatus status;
@NotBlank
private String title;
@Lob
@NotBlank
private String content;
}
- PostTechStackRepository.findAllByUser로 PostTechStack을 한 번에 조회해 온다.
- 가져온 PostTechStack 데이터를 PostResponseDto로 변환해서 반환한다.
이제 실행된 쿼리를 봐보면!
1) User 조회 1번
2) PostTechStack 조회 1번
참고로 jpa에서 fetch join은 ToOne은 몇 개든지 사용 가능하다.
하지만 ToMany는 1개만 사용 가능하다.
https://jojoldu.tistory.com/457
지금까지 N+1문제는 즉시로딩의 경우에만 발생하는줄 알았다.
근데 지연로딩으로 설정하더라도 발생할수 있는 문제임을 처음 알았다.
엔티티를 설계할때 우선 단방향 매핑을 잘하고 필요한 경우에만 양방향 연관관계로 가져가자!
Author And Source
이 문제에 관하여([JPA] N+1 문제, Fetch Join), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@gkdud583/JPA-N1-문제-Fetch-Join저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)