N+1 이슈에 대해서

이진영·2022년 10월 20일
0

개요


JPA를 처음 사용하는 개발자라면 항상 N+1 문제를 직면하기 마련이다.
이는 나도 마찬가지이기 때문에 게시글을 통해서 N+1 이슈에 대해서 좀 더 명확히 이해하고 가고자 해당 글을 쓴다.

N+1은 무엇❓

처음 강의를 통해서 JPA를 접했을때 강사님 또한 N+1 이슈에 대해서 설명은 해줬지만 나에게는 그저그런 문제중 하나일 줄알았다. 하지만 사용하다 보면 정말이지 많이 의도치 않는 N+1를 직면하게 된다.

그렇다면 N+1은 정확히 무엇일까?

하나의 JPA 메소드를 생성하고 해당 메소드에 대한 데이터를 가져올 때 내가 원치 않는 다른 연관 테이블에 있는 데이터를 가져오게 되는 것을 N+1 이슈라고 한다.

기존에 JPA 같은 stack이 없었다면, 아마도 직접 쿼리를 짜는 형식을 통해서 개발을 해왔을것이다. 하지만 JPA와 같은 편한 기술력이 나오면서, 너도나도 써봤을 경우가 있다. 하지만 정말 JPA는 호락호락하지 않는 것을 직면하였을거고, 해당 게시글에서 이야기 하는 것 또한 그 문제중 하나라고 생각이든다.

그렇다면 N+1문제는 언제 직면하게 되는 것일까?

@Entity @Data
public class Member {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long MemberId;
	private String name;

	@OneToMany(mappedBy = "member")
	private Set<Board> boards = Collections.emptySet();
}

@Entity @Data
public class Board {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long boardId;
	private String title;
	private String content;

	@ManyToOne
	private Member member;
}

해당 글은 간단한 테이블을 만들고 위와 같은 관계를 인지하자

또한 간단한 fetch를 지연로딩과 즉시로딩 설정이 가능하다. 이러한 부분을 토대로 설명을 드리고자 한다.


default fetch

연관관계시 자동으로 설정되는 부분이 있다.

- member
@OneToMany(mappedBy = "member")
private Set<Board> boards = Collections.emptySet();

- board
@ManyToOne
private Member member;

해당 글을 보시면 아직 fetch에 대한 설정이 안되어 있다. 조금 더 어노테이션을 들어가 코드를 봐보면

기본적으로 설정되는 부분이 있다. 이러한 부분은 각 어떤 관계냐에 따라 default는 다르다.

ManyToOne / OneToMany : Eager type
MnayToMany / OneToOne : Lazy type


즉시로딩

- member
@OneToMany(mappedBy = "member",fetch = FetchType.EAGER)
private Set<Board> boards = Collections.emptySet();

- board
@ManyToOne(fetch = FetchType.EAGER)
private Member member;

해당 설정을 마친 뒤 test를 진행해보자.

	@DisplayName("board 게시판에 있는 모든 데이터를 가져온다.")
	@Test
	public void findAll(){
		Member result = memberRepository.findById(2L).get();
	}

해당 메소드는 JPA 내부 메소드의 실행 결과이다. 해당 메소드는 내부적으로 inner join이 실행 되기 때문에 그렇게 크게 문제되는 부분이 없을 수도 있다. 하지만 우리는 이러한 기본 메소드만 사용되는 것이 아닌 JPQL을 사용될 때 문제가 발생된다.

일반적으로 JPQL 이란 findBy~ 와 같은 커스터마이징된 메소드들을 의미하며, 이러한 메소드들은 내부에서 JPQL이 사용된다.

그렇다면 직접 JPQL을 사용해보자

해당 메소드의 실행 결과를 본다면, 두 가지의 쿼리가 실행된다. 이는 fetch 전략을 eagger를 통해서 위와 같은 문제점이 발생한다.

‼해결책(간단한)

fetch join

@Query(
		"select m from Member m left join fetch m.boards"
)
Member findByCustomId(Long id);

간단히 직접쿼리를 join을 시켜주면 된다.

해당 실행 결과를 봐보면 쿼리가 더 나아가는 불편함은 없게 된다.

EntityGraph
하지만 이와 같은 방법은 다소 보기 불편하게 될 수 있다. 가정을 해보자. 위와 같은 기본적인 쿼리들을 사용했지만, 더 길어진다면 다소 하드코딩에 가깝게 발전할 수 있다. 이를 해결하기 위한 EntityGraph가 있다.

@EntityGraph(attributePaths = {"boards"})
Member findByMemberId(Long id);

위와 같은 방법으로 fetch join이 동일하게 발생하면서 좀 더 깔끔한 코드로도 해결이 가능하다.

하지만 이러한 방법에 정답은 없다.

이러한 fetch 타입에 관한 전략에 대해서는 문제점은 명확히 있다. 예를 들어 pagination이 될 수 있으며, 프로덕트 별로 다르게 적용되어야 한다.

출처
https://velog.io/@jinyoungchoi95/JPA-%EB%AA%A8%EB%93%A0-N1-%EB%B0%9C%EC%83%9D-%EC%BC%80%EC%9D%B4%EC%8A%A4%EA%B3%BC-%ED%95%B4%EA%B2%B0%EC%B1%85
https://www.popit.kr/jpa-n1-%EB%B0%9C%EC%83%9D%EC%9B%90%EC%9D%B8%EA%B3%BC-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95/

profile
내가 공부한 것들을 적는 공간

0개의 댓글