N+1 정복기 기본편2

조현근·2022년 11월 17일
0
post-thumbnail

N+1이 발생하는 경우

이번 포스팅에선 fetch = LAZY일때 N+1이 발생하는 경우를 살펴보겠습니다.

OneToMany(fetch = LAZY)

일대다 관계를 맺는 엔티티가 있고 fetch전략이 LAZY인 경우
지연로딩 덕분에 findAll()을 해도 N+1이 발생하지 않습니다.
하지만 프록시 객체로 가져온 자식의 초기화가 일어나게 되면 N+1이 발생합니다. 이 경우를 학습테스트를 통해 살펴보겠습니다.

N+1 발생

Team

Entity
public class Team2 {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "team_id")
    private Long id;

    @Column(name = "team_name")
    private String name;

    //    @BatchSize(size = 100)
    @OneToMany(fetch = FetchType.LAZY, cascade = {PERSIST, REMOVE})
    @JoinColumn(name = "team_id")
    private List<Member2> member2s = new ArrayList<>();

    protected Team2() {
    }

    public Team2(String name, List<Member2> member2s) {
        this.name = name;
        this.member2s = member2s;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public List<Member2> getMembers() {
        return member2s;
    }
}

Member

@Entity
public class Member2 {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id")
    private Long id;

    @Column(name = "member_name")
    private String name;

    protected Member2() {
    }

    public Member2(String name) {
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }
}

TeamService

@Service
public class Team2Service {

    private final Team2Repository team2Repository;

    public Team2Service(Team2Repository team2Repository) {
        this.team2Repository = team2Repository;
    }

    @Transactional(readOnly = true)
    public List<String> findAllMembersName() {
        List<Team2> team2s = team2Repository.findAll();

        return team2s.stream()
                .flatMap(team2 -> team2.getMembers().stream())
                .map(Member2::getName)
                .collect(Collectors.toList());
    }
}

학습테스트

@DisplayName("oneToMany(fetch = LAZY)에서 N+1이 발생한다.")
@Test
void oneToMany_Lazy_N_1() {
    System.out.println("@@@ N+1 @@@");
    List<String> allMembersName = team2Service.findAllMembersName();
    System.out.println("@@@ N+1 @@@");

    assertThat(allMembersName).hasSize(teamNumber * memberNumberPerTeam);
}

쿼리

@@@ N+1 @@@
Hibernate: 
    select
        team2x0_.team_id as team_id1_7_,
        team2x0_.team_name as team_nam2_7_ 
    from
        team2 team2x0_
Hibernate: 
    select
        member2s0_.team_id as team_id3_2_0_,
        member2s0_.member_id as member_i1_2_0_,
        member2s0_.member_id as member_i1_2_1_,
        member2s0_.member_name as member_n2_2_1_ 
    from
        member2 member2s0_ 
    where
        member2s0_.team_id=?
Hibernate: 
    select
        member2s0_.team_id as team_id3_2_0_,
        member2s0_.member_id as member_i1_2_0_,
        member2s0_.member_id as member_i1_2_1_,
        member2s0_.member_name as member_n2_2_1_ 
    from
        member2 member2s0_ 
    where
        member2s0_.team_id=?
Hibernate: 
    select
        member2s0_.team_id as team_id3_2_0_,
        member2s0_.member_id as member_i1_2_0_,
        member2s0_.member_id as member_i1_2_1_,
        member2s0_.member_name as member_n2_2_1_ 
    from
        member2 member2s0_ 
    where
        member2s0_.team_id=?
@@@ N+1 @@@

Team에 속한 모든 Member의 이름을 가져오는 로직에서 N+1이 발생함을 알 수 있습니다.

fetch join으로 N+1 해결

TeamService

@Transactional(readOnly = true)
public List<String> findAllMembersNameFetchJoin() {
    List<Team2> team2s = team2Repository.findAllFetchJoin();

    return team2s.stream()
            .flatMap(team2 -> team2.getMembers().stream())
            .map(Member2::getName)
            .collect(Collectors.toList());
}

TeamRepository

@Query(value = "SELECT distinct t FROM Team2 t join fetch t.member2s")
List<Team2> findAllFetchJoin();

학습 테스트

@DisplayName("fetch join으로 N+1 해결")
@Test
void oneToMany_Lazy_N_1_Fetch_join() {
    System.out.println("@@@ fetch join @@@");
    List<String> allMembersName = team2Service.findAllMembersNameFetchJoin();
    System.out.println("@@@ fetch join @@@");

    assertThat(allMembersName).hasSize(teamNumber * memberNumberPerTeam);
}

쿼리

@@@ fetch join @@@
Hibernate: 
    select
        distinct team2x0_.team_id as team_id1_7_0_,
        member2s1_.member_id as member_i1_2_1_,
        team2x0_.team_name as team_nam2_7_0_,
        member2s1_.member_name as member_n2_2_1_,
        member2s1_.team_id as team_id3_2_0__,
        member2s1_.member_id as member_i1_2_0__ 
    from
        team2 team2x0_ 
    inner join
        member2 member2s1_ 
            on team2x0_.team_id=member2s1_.team_id
@@@ fetch join @@@

fetch join을 사용하면 N+1이 해결됨을 알 수 있습니다!

@BatchSize로 N+1 해결

Many쪽 class레벨에 @BatchSize

클래스 레벨에 @BatchSize를 붙여도 프록시 객체이기 때문에 적용이 될 줄 알았다. 하지만 N+1문제가 해결되지 않고 여러개의 쿼리가 발생하는것을 확인할 수 있었다. 왜 그런지 찾아보았지만 아직은 이유를 찾지 못했다. 다음에 학습하고 내용을 추가해보겠다.

One쪽 Many 컬렉션 필드에 @BatchSize

Team 엔티티의 List<Member> 필드

@BatchSize(size = 100)
@OneToMany(fetch = FetchType.LAZY, cascade = {PERSIST, REMOVE})
@JoinColumn(name = "team_id")
private List<Member2> member2s = new ArrayList<>();

테스트 코드

@DisplayName("클래스 레벨 @BatchSize로 N+1 해결")
@Test
void oneToMany_Lazy_N_1_BatchSize_ClassLevel() {
    System.out.println("@@@ Batch Size @@@");
    List<String> allMembersName = team2Service.findAllMembersName();
    System.out.println("@@@ Batch Size @@@");

    assertThat(allMembersName).hasSize(teamNumber * memberNumberPerTeam);
}

쿼리

@@@ Batch Size @@@
Hibernate: 
    select
        team2x0_.team_id as team_id1_7_,
        team2x0_.team_name as team_nam2_7_ 
    from
        team2 team2x0_
Hibernate: 
    select
        member2s0_.team_id as team_id3_2_1_,
        member2s0_.member_id as member_i1_2_1_,
        member2s0_.member_id as member_i1_2_0_,
        member2s0_.member_name as member_n2_2_0_ 
    from
        member2 member2s0_ 
    where
        member2s0_.team_id in (
            ?, ?, ?
        )
@@@ Batch Size @@@

@EntityGraph로 N+1 해결

TeamService

public List<String> findALlMembersNameEntityGraph() {
    List<Team2> team2s = team2Repository.findAllEntityGraph();

    return team2s.stream()
            .flatMap(team2 -> team2.getMembers().stream())
            .map(Member2::getName)
            .collect(Collectors.toList());
}

TeamRepository

@EntityGraph(attributePaths = "member2s")
@Query(value = "SELECT t FROM Team2 t")
List<Team2> findAllEntityGraph();

학습 테스트

@DisplayName("entityGraph로 N+1 해결")
@Test
void oneToMany_Lazy_N_1_EntityGraph() {
    System.out.println("@@@ Entity Graph @@@");
    List<String> allMembersName = team2Service.findALlMembersNameEntityGraph();
    System.out.println("@@@ Entity Graph @@@");

    assertThat(allMembersName).hasSize(teamNumber * memberNumberPerTeam);
}

쿼리

@@@ Entity Graph @@@
Hibernate: 
    select
        team2x0_.team_id as team_id1_7_0_,
        member2s1_.member_id as member_i1_2_1_,
        team2x0_.team_name as team_nam2_7_0_,
        member2s1_.member_name as member_n2_2_1_,
        member2s1_.team_id as team_id3_2_0__,
        member2s1_.member_id as member_i1_2_0__ 
    from
        team2 team2x0_ 
    left outer join
        member2 member2s1_ 
            on team2x0_.team_id=member2s1_.team_id
@@@ Entity Graph @@@
profile
안녕하세요!

0개의 댓글