[JPA] N + 1 문제 & 해결방법

ichubtou·2024년 1월 14일
0

JPA N+1 문제


JPA N+1 문제란?

  • 예를들어 Team이라는 테이블과 User라는 테이블이 아래와 같이 있을때 Team을 조회하는 경우
  • Team을 조회하는 쿼리 1개
  • 각 Team에 소속된 Members를 조회하는 쿼리 N개 (N은 Team의 개수)

발생 이유

  • TeamRepository 인터페이스에서 findAll() 메서드로 조회할때 Jpql이 동작하기 때문
  • jpql은 객체지향 쿼리로 엔티티의 객체와 필드이름을 가지고 쿼리를 만들게 되는데, findAll() 이 동작할때 다음과 같이 쿼리가 작성됨 "select t from Team t"
  • 위의 쿼리는 Team 엔티티만을 조회하므로 이후 각 팀의 members를 조회하기 위해 추가적인 쿼리 실행
  • 그렇기 때문에 각 팀의 members를 찾기 위해 다시 쿼리가 나감

User Table

@Entity(name = "users")
@Getter
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long userId;

    private String firstName;

    private String lastName;

    @ManyToOne
    @JoinColumn(name = "team_id")
    private Team team;
}

Team Table
@Entity
@Getter
public class Team {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long teamId;

    private String name;

    @OneToMany(mappedBy = "team")
    private List<User> users = new ArrayList<>();
}

Data
INSERT INTO team (team_id, name)
VALUES(1, 'A'), (2, 'B'), (3, 'C');

INSERT INTO users (user_id, first_name, last_name, team_id)
VALUES(1, 'A', 'A', 1),
      (2, 'B', 'B', 2),
      (3, 'C', 'C', 3);

N+1 문제가 일어나는 상황


Fetch 모드 → EAGER(즉시 로딩) TeamRepository에서 findAll 호출

//Team Table
@OneToMany(mappedBy = "team", fetch = FetchType.EAGER)

Service
public void getTeams() {
        log.info("--------------------FindTeam--------------------");
        List<Team> all = teamRepository.findAll();
        log.info("--------------------end--------------------");
}

Log
INFO 2491 --- [nio-8080-exec-1] com.test.test.team.TeamService           : --------------------FindTeam--------------------
[Hibernate]
    select
        team0_.team_id as team_id1_0_,
        team0_.name as name2_0_
    from
        team team0_
[Hibernate]
    select
        users0_.team_id as team_id4_1_0_,
        users0_.user_id as user_id1_1_0_,
        users0_.user_id as user_id1_1_1_,
        users0_.first_name as first_na2_1_1_,
        users0_.last_name as last_nam3_1_1_,
        users0_.team_id as team_id4_1_1_
    from
        users users0_
    where
        users0_.team_id=?
[Hibernate]
    select
        users0_.team_id as team_id4_1_0_,
        users0_.user_id as user_id1_1_0_,
        users0_.user_id as user_id1_1_1_,
        users0_.first_name as first_na2_1_1_,
        users0_.last_name as last_nam3_1_1_,
        users0_.team_id as team_id4_1_1_
    from
        users users0_
    where
        users0_.team_id=?
[Hibernate]
    select
        users0_.team_id as team_id4_1_0_,
        users0_.user_id as user_id1_1_0_,
        users0_.user_id as user_id1_1_1_,
        users0_.first_name as first_na2_1_1_,
        users0_.last_name as last_nam3_1_1_,
        users0_.team_id as team_id4_1_1_
    from
        users users0_
    where
        users0_.team_id=?
INFO 2491 --- [nio-8080-exec-1] com.test.test.team.TeamService           : --------------------end--------------------

해당 로그처럼 팀을 전체 조회한 후 각각 팀의 user를 조회하여 N+1 문제가 일어났다.



Fetch 모드 → LAZY(지연 로딩) TeamRepository에서 findAll 호출

//Team Table
@OneToMany(mappedBy = "team", fetch = FetchType.LAZY)

Service
public void getTeams() {
        log.info("--------------------FindTeam--------------------");
        List<Team> all = teamRepository.findAll();
        log.info("--------------------end--------------------");
}

Log
INFO 2527 --- [nio-8080-exec-2] com.test.test.team.TeamService           : --------------------FindTeam--------------------
[Hibernate]
    select
        team0_.team_id as team_id1_0_,
        team0_.name as name2_0_
    from
        team team0_
INFO 2527 --- [nio-8080-exec-2] com.test.test.team.TeamService           : --------------------end--------------------

해당 상황에서는 N+1 문제가 발생하지 않는 것처럼 보인다.

그러나 team의 users를 사용하려고 하면 문제가 발생한다.


Service

public void getTeamsUseUsers() {
        log.info("--------------------FindTeamUseUsers--------------------");
        List<Team> all = teamRepository.findAll();
        for (Team team : all) {
            List<User> users = team.getUsers();
            for (User user : users) {
                user.getUserId();
            }
        }
        log.info("--------------------end--------------------");
    }

Log
INFO 2556 --- [nio-8080-exec-1] com.test.test.team.TeamService           : --------------------FindTeamUseUsers--------------------
[Hibernate]
    select
        team0_.team_id as team_id1_0_,
        team0_.name as name2_0_
    from
        team team0_
[Hibernate]
    select
        users0_.team_id as team_id4_1_0_,
        users0_.user_id as user_id1_1_0_,
        users0_.user_id as user_id1_1_1_,
        users0_.first_name as first_na2_1_1_,
        users0_.last_name as last_nam3_1_1_,
        users0_.team_id as team_id4_1_1_
    from
        users users0_
    where
        users0_.team_id=?
[Hibernate]
    select
        users0_.team_id as team_id4_1_0_,
        users0_.user_id as user_id1_1_0_,
        users0_.user_id as user_id1_1_1_,
        users0_.first_name as first_na2_1_1_,
        users0_.last_name as last_nam3_1_1_,
        users0_.team_id as team_id4_1_1_
    from
        users users0_
    where
        users0_.team_id=?
[Hibernate]
    select
        users0_.team_id as team_id4_1_0_,
        users0_.user_id as user_id1_1_0_,
        users0_.user_id as user_id1_1_1_,
        users0_.first_name as first_na2_1_1_,
        users0_.last_name as last_nam3_1_1_,
        users0_.team_id as team_id4_1_1_
    from
        users users0_
    where
        users0_.team_id=?
INFO 2556 --- [nio-8080-exec-1] com.test.test.team.TeamService           : --------------------end--------------------

해결 방법


  • Fetch Join
  • @BatchSize

Fetch Join


  • 해결하는 방법에는 우리가 필요시에 연관된 엔티티까지 한번에 조회하는 join쿼리가 필요

    SELECT * FROM team AS t JOIN member AS m ON t.team_id = m.team_id 

  • JPQL에서 성능 최적화를 위해 기존 SQL 조인 종류가 아닌 fetch join를 제공

  • N+1 문제를 해결하기 위해 연관된 엔티티를 한 번에 조회하는 Fetch Join 쿼리를 사용 가능

  • 연관된 엔티티나 컬렉션을 SQL 한번에 조회가 가능

  • 아래와 같이 Team과 연관된 Users를 함께 조회


Repsitory

    @Query("select t from Team t join fetch t.users")

fetch join은 지연로딩으로 설정해놓아도 우선순위를 가지기 때문에 즉시 로딩으로 동작


Log

INFO 3213 --- [nio-8080-exec-4] com.test.test.team.TeamService           : --------------------UseFetchJoin--------------------
[Hibernate]
    select
        team0_.team_id as team_id1_0_0_,
        users1_.user_id as user_id1_1_1_,
        team0_.name as name2_0_0_,
        users1_.first_name as first_na2_1_1_,
        users1_.last_name as last_nam3_1_1_,
        users1_.team_id as team_id4_1_1_,
        users1_.team_id as team_id4_1_0__,
        users1_.user_id as user_id1_1_0__
    from
        team team0_
    inner join
        users users1_
            on team0_.team_id=users1_.team_id
INFO 3213 --- [nio-8080-exec-4] com.test.test.team.TeamService           : --------------------end--------------------

@BatchSize


  • JPA의 성능 개선을 위해 하이버네이트의 옵션 중 하나
  • 연관된 엔티티를 조회할 때 지정된 size 만큼 SQL의 IN절을 사용해서 조회

Team

@OneToMany(mappedBy = "team", fetch = FetchType.LAZY)
@BatchSize(size = 3)
private List<User> users = new ArrayList<>();

Log
INFO 3552 --- [nio-8080-exec-1] com.test.test.team.TeamService           : --------------------FindTeamUseUsers--------------------
[Hibernate]
    select
        team0_.team_id as team_id1_0_,
        team0_.name as name2_0_
    from
        team team0_
[Hibernate]
    select
        users0_.team_id as team_id4_1_1_,
        users0_.user_id as user_id1_1_1_,
        users0_.user_id as user_id1_1_0_,
        users0_.first_name as first_na2_1_0_,
        users0_.last_name as last_nam3_1_0_,
        users0_.team_id as team_id4_1_0_
    from
        users users0_
    where
        users0_.team_id in (
            ?, ?, ?
        )
INFO 3552 --- [nio-8080-exec-1] com.test.test.team.TeamService           : --------------------end--------------------

BatchSize 사용시 주의점

0개의 댓글