[SpringBoot] N+1 문제

728x90

LAZY 로딩과 EAGER 로딩의 차이를 먼저 알아야 한다.(지연 로딩, 즉시 로딩)


지연로딩

Team —————— Member 는 서로 OneToMany, ManyToOne 관계를 맺고 있다.

public class Team extends CoreEntity {

	// ... 중략

	@OneToMany(mappedBy = "team", cascade = {CascadeType.ALL}, fetch = FetchType.LAZY)
	private List<Member> members = new ArrayList()<>;
}
public class Member extends CoreEntity {

	// ... 중략

	@ManyToOne(fetch = FetchType.LAZY, cascade = {CascadeType.ALL})
	@JoinColumn(name = "member_idx")
	private Team team;
}

위와 같은 형태로 Team 엔티티에 연관관계를 맺고 있다.

여기서 Member 객체를 조회할 때 Team 객체를 불러오는 방식에 차이가 생긴다.

여기서는 FetchType.LAZY → 지연로딩을 사용했기 때문에 Team 객체를 프록시 객체로서 가져오게 된다.

프록시 객체란 무엇이냐? 프록시 객체 내부의 속성을 조회하기 전 까지는 DB 에 쿼리문을 날리지 않는 null 값을 가진 객체라고 생각하면 된다.

즉, Team 객체는 프록시 객체이기 때문에 우리가 직접 조회하기 전 까지는 DB 에 쿼리문을 날리지 않는다.

그래서 Member 객체를 조회할 때 select 쿼리문을 한 번만 날리고 프록시 객체를 조회할 경우에 다시 쿼리를 날린다.

즉시로딩

즉시 로딩은 말 그대로 객체를 조회할 때 연관관계를 맺은 객체까지 전부 조회하는 것을 뜻 한다.

기본적으로 N+1 문제가 발생하며, ManyToOne, OneToOne 의 경우 기본적으로 즉시 로딩이며 OneToMany, ManyToMany 는 지연 로딩으로 설정되어 있다.


N+1 문제의 원인

  1. 즉시 로딩으로 모든 연관관계 객체들을 불러올 경우 select 로 연관관계 까지 조회하므로 N+1 발생
  2. 지연 로딩으로 조회를 하고나서 연관관계 객체를 다시 조회할 경우 N+1 문제 발생
public void N1_test() {

	List<Team> teams = teamRepository.findAll();

}

만약 Member 클래스의 Team 객체와의 연관관계 속성이 즉시 로딩이였을 경우 위의 케이스에서 N+1 문제 발생

지연 로딩일 경우에도 프록시 객체를 다시 조회하는 경우에 JPQL 이 실행되어 N+1 문제가 발생한다.

public class Team extends CoreEntity {

	// ... 중략

	@OneToMany(mappedBy = "team", cascade = {CascadeType.ALL}, fetch = FetchType.LAZY)
	private List<Member> members = new ArrayList()<>;
}
@Transactional
public void N1_test() {
	
	List<Team> teams = teamRepository.findAll();

	for(Team team : teams) {
		System.Out.Println(team.getMembers.getSize());
	}
}

위의 경우 team 레포지토리에서 모든 team 의 값들을 리스트로 뽑아오고, 해당하는 각각의 팀들의 멤버를 조회하는 로직이다.

여기서 멤버 정보를 조회하면 팀 객체에 대한 조회는 이미 끝난 상태여서 JOIN 으로 쿼리가 생성이 안되다.

단지, 팀 객체에 대한 ID 로 조회할 수 밖에 없다.

그래서 where team.memberId = ? 형식으로 JPQL 쿼리를 생성하고, 이로 인해 매번 조회 쿼리가 발생

N 번 실행되는 이슈가 발생한다.

멤버를 조회할 때, 팀 객체에 대한 조회는 이미 끝난 상태이다.

그래서 JOIN 을 사용하지 못하기 때문에 JPQL 쿼리를 만들어서 반복문 횟수만큼 쿼리문을 날린다.


N+1 문제 해결법

지연 로딩에서 문제가 생기는 이유 → DB 통신은 findAll 에서 끝났는데 프록시 객체를 조회해서 다시 JPQL 쿼리를 날리기 때문에 발생한다.

즉, join 이 되지 않아서 생기는 문제라고 볼 수 있다.

그러면 join 을 해주면 해결될 문제가 아닐까? ⇒ Spring 에선 fetch join 을 통해 해결한다.

해당 레포지토리에서 네이티브 쿼리를 만들어서 해결할 수 있다.

@Query("select DISTINCT t from Team t join fetch t.members")
List<Team> findAllJoinFetch();

이렇게 하면 N+1 문제가 해결이 된다.

그러나 fetch join 도 문제가 존재한다.

첫 째로 JPA 가 제공하는 Pageable 기능 사용 X, 둘 째로 1:N 관계가 2개인 엔티티를 fetch join 불가능하다.

fetch join

sql 조인 종류가 아니고, JPQL 에서 성능 최적화를 위해 제공하는 기능으로 연관 관계를 맺고 있는 엔티티를 SQL 한 번에 함께 조회하는 기능이다.

즉시 로딩과 똑같지만 fetch join 은 쿼리로 내가 원하는 객체, 그래프를 명시적, 동적으로 뽑을 수 있다.

일반 조인 시에는 연관된 엔티티를 함께 조회해주지 않는다.

페치 조인을 사용할 때만 연관 관계인 엔티티도 함께 조회한다.

728x90