[Back-end] N+1 문제

Geun·2022년 3월 20일
0

Back-end

목록 보기
30/74

N+1

N+1은 JPA(Java Persistence API)를 사용하면서 연관관계를 맺는 엔티티를 사용한다면 한번 쯤 부딪힐 수 있는 문제이다.
N+1문제 발생 시 성능에 큰 영향을 줄 수 있다.

N+1 문제

연관 관계에서 발생하는 이슈로 연관 관계가 설정된 엔티티를 조회할 경우에 조회된 데이터의 개수만큼 연관 관계의 조회 쿼리가 추가로 발생하여 데이터를 읽어오게 된다.
이것을 N+1 문제라고 부른다.

지연로딩(LAZY), 즉시로딩(EAGER)

지연 로딩의 경우, Member 엔티티를 조회할 때 Team 엔티티는 프록시 객체로 가져온다.
후에 실제 Team 객체를 사용하는 시점에 초기화 된다. DB에 쿼리가 나간다.

예를 들어 getTeam()으로 Team을 조회하면 프록시 객체가 조회된다.
getTeam.getXXX()으로 팀의 필드에 접근할 때 쿼리가 나간다.

이렇게 지연로딩을 사용한다면 SELECT 쿼리가 각각 따로 2번 나간다.
네트워크를 2번 타며 조회가 이루어진다는 뜻이다.

즉시 로딩인 경우 프록시 객체가 아니라 하나의 쿼리로 Team 객체까지 실제 객체를 가져온다.

주의할 것

실무에서는 가급적 지연로딩만 사용한다고 한다.
즉시로딩을 적용하면 예상하지 못한 SQL이 발생하고 N+1 문제가 생긴다.

@ManyToOne, @OneToOne과 같이 @XXXToOne 어노테이션들은 기본이 즉시 로딩으로 되어있다.

@OneToMany와 @ManyToMany는 기본이 지연 로딩이다.

원인

  • 즉시 로딩으로 데이터를 가져오는 경우

  • 지연 로딩으로 데이터를 가져온 이후 가져온 데이터에서 하위 엔티티를 다시 조회하는 경우

앨범 엔티티

@Entity
public class Album {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @Column(nullable = false)
    private String albumTitle;

    @Column(nullable = false)
    private String locales;

    // @OneToMany(mappedBy = "album", cascade = CascadeType.ALL, fetch = FetchType.EAGER) // 2번 상황
    @OneToMany(mappedBy = "album", cascade = CascadeType.ALL, fetch = FetchType.LAZY) // 1번 상황
    private List<Song> songs = new ArrayList<>();
}

노래 엔티티

@Entity
public class Song {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @Column(nullable = false)
    private String title;

    @Column(nullable = false)
    private int track;

    @Column(nullable = false)
    private int length;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "album_id")
    private Album album;
}

하위 엔티티를 조회하지 않는 경우

@Test
public void N1_쿼리테스트_1() throws Exception{
    List<Album> albums = albumRepository.findAll();
}

즉시로딩하면 N+1 문제가 발생한다.

하위 엔티티를 조회하는 경우

@Test
@Transactional // 테스팅에서 LAZY 전략시 필수
public void N1_쿼리테스트_2() throws Exception{
    List<Album> albums = albumRepository.findAll(); // (1) N+1 발생하지 않음
    for (Album album : albums) {
        System.out.println(album.getSongs().size()); // (2) Song에 접근 N+1 발생.
    }
}

지연로딩, 즉시로딩 시 N+1 문제가 발생한다.

N+1 문제 해결방법

패치 조인(Fetch Join)

쿼리로 미리 테이블을 조인해서 가져온다. LAZY, EAGER 두 개에 해당되는 해결방법

@Query("select DISTINCT a from Album a join fetch a.songs")
List<Album> findAllJoinFetch();

@Test
@Transactional // 테스팅에서 LAZY 전략시 사용해야 동작
public void FetchJoin_테스트() throws Exception{
    List<Album> albums = albumRepository.findAllJoinFetch();
    for (Album album : albums) {
        System.out.println(album.getSongs().size()); // Song에 접근 !
    }
}

패치 조인의 단점

  • JPA가 제공하는 Pageable 기능을 사용할 수 없다.
  • 1:N 관계가 2개인 엔티티에 패치조인 사용할 수 없다.

Batch Size 조절

설정한 Size만큼 데이터를 미리 로딩한다.
JPA의 페이징 API 기능처럼 개수가 고정된 데이터를 가져올 때 함께 사용하면 유용하게 사용 가능하다.
글로벌 패치 전략을 EAGER로 변경해야하는 단점이 있다.

Java
@BatchSize(size = 5)
@OneToMany(mappedBy = "album", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
private List<Song> songs = new ArrayList<>();


참고자료

https://velog.io/@woo00oo/N-1-%EB%AC%B8%EC%A0%9C
https://wwlee94.github.io/category/blog/spring-jpa-n+1-query/
https://incheol-jung.gitbook.io/docs/q-and-a/spring/n+1

0개의 댓글