๐ŸŒป JAVA/์ž๋ฐ” ORM ํ‘œ์ค€ JPA ํ”„๋กœ๊ทธ๋ž˜๋ฐ

JPA - Fetch Join์ด ๊ณผ์—ฐ ๋งŒ๋Šฅ์ธ๊ฐ€? (N+1, Pagination)

iseunghan 2024. 4. 18. 23:28
๋ฐ˜์‘ํ˜•

๋“ค์–ด๊ฐ€๊ธฐ ์ „

์ด์ „ ์‹œ๊ฐ„์— ์•Œ์•„๋ดค๋˜ N+1 ํ•ด๊ฒฐ๋ฒ•์— ์ด์–ด์„œ FetchJoin์„ ์ด์šฉํ•ด์„œ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ Fetch Join์ด๋ผ๊ณ  ๋‹ค ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์€ ์•„๋‹™๋‹ˆ๋‹ค. ์ด๋ฒˆ ์‹œ๊ฐ„์—๋Š” Fetch Join์„ ์‚ฌ์šฉํ–ˆ์„ ๋•Œ ์–ด๋– ํ•œ ์‚ฌ์ด๋“œ ์ดํŽ™ํŠธ๊ฐ€ ์žˆ๋Š”์ง€ ์•Œ์•„๋ณด๊ณ  ๊ทธ ํ•ด๊ฒฐ์ฑ…์— ๋Œ€ํ•ด์„œ ์•Œ์•„๋ด…๋‹ˆ๋‹ค.

1. FetchJoin, EntityGraph ์‚ฌ์šฉ ์‹œ Pagination์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๋‹ค.

FetchJoin๊ณผ EntityGraph ๋‘˜ ๋‹ค ๋™์ผํ•œ ์ฆ์ƒ์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. Fetch Join๋งŒ ํ…Œ์ŠคํŠธ๋ฅผ ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

@Query(
        value = "select t from Team t join fetch t.members",
        countQuery = "select count(t) from Team t"
)
List<Team> findTeamsFetchJoin(Pageable pageable);
@DisplayName("๋ชจ๋“  ํŒ€์„ ํ•  ๋•Œ, ํŽ˜์ด์ง€๋„ค์ด์…˜์ด ์•ˆ๋œ๋‹ค.")
@Test
void team_findAll_Pagination_test() {
    clearPersistenceContext();

    System.out.println("----------team_findAll_test start-----------");
    List<Team> teamList = teamRepository.findTeamsFetchJoin(PageRequest.of(0, 1));
    assertThat(teamList).hasSize(1);
    System.out.println("----------team_findAll_test mid-----------");
    teamList.stream()
            .map(Team::getMembers)
            .map(List::stream)
            .forEach(memberStream -> memberStream
                    .map(Member::getName)
                    .forEach(System.out::println)
            );
    System.out.println("----------team_findAll_test end-----------");
}

๋กœ๊ทธ๋ฅผ ์‚ดํŽด๋ณด๋ฉด LIMIT ์ฟผ๋ฆฌ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š๊ณ , firstResult/maxResults specified with collection fetch; applying in memory ๋ผ๋Š” ๊ฒฝ๊ณ ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.

์ด ๋ง์€ ์ฆ‰์Šจ, Full Scanํ•ด์„œ ์ „๋ถ€ ๋‹ค ๋“ค๊ณ ์™€์„œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‹จ์—์„œ ๋ฉ”๋ชจ๋ฆฌ ์ƒ์— ์˜ฌ๋ ค๋‘๊ณ  ํŽ˜์ด์ง€๋„ค์ด์…˜ ์ฒ˜๋ฆฌ๋ฅผ ํ–ˆ๋‹ค๋Š” ๋œป์ž…๋‹ˆ๋‹ค. ๋งŒ์•ฝ ๋ช‡์ฒœ๋งŒ๊ฑด์ด๋ผ๋ฉด ์–ด๋–ป๊ฒŒ ๋ ๊นŒ์š”? OOM(OutOfMemory)์ด ๋ฐœ์ƒํ•ด์„œ ์žฅ์• ๊ฐ€ ๋ฐœ์ƒํ•  ๊ฐ€๋Šฅ์„ฑ์ด ๋งค์šฐ ํฝ๋‹ˆ๋‹ค!

๊ทธ๋ ‡๋‹ค๋ฉด ์–ด๋–ป๊ฒŒ ํ•ด๊ฒฐํ•ด์•ผ ํ• ๊นŒ์š”? ๋ฉ”๋ชจ๋ฆฌ ์ƒ์— ์˜ฌ๋ ค๋‘๊ณ  Pagination์„ ํ•œ๋‹ค๋Š”์ ์€ ๋„ˆ๋ฌด๋‚˜ ์น˜๋ช…์ ์ด๊ธฐ ๋•Œ๋ฌธ์— N+1์˜ ์ด์ ์„ ํฌ๊ธฐํ•˜๋Š”๊ฒŒ ๋งž๋‹ค๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด๊ฑด ์ƒํ™ฉ์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์งˆ ์ˆ˜ ์žˆ์„ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  1. ๋งŒ์•ฝ Pagination์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค๋ฉด, FetchJoin or EntityGraph๋กœ N+1 ํ•ด๊ฒฐ
  2. ๋งŒ์•ฝ Pagination์„ ์ค‘์š”์‹œ ํ•œ๋‹ค๋ฉด, N+1 ํฌ๊ธฐ (ํ•˜์ง€๋งŒ Batch Size๋ฅผ ์ด์šฉํ•œ๋‹ค๋ฉด N+1์— ๋Œ€ํ•ด ์ตœ์†Œํ•œ์˜ ์„ฑ๋Šฅ์„ ๋ณด์žฅ์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค)

์•„๋ž˜์—์„œ๋Š” ํ›„์ž์˜ ๋ฐฉ๋ฒ•์„ ์‚ดํŽด๋ด…๋‹ˆ๋‹ค.

ํ•ด๊ฒฐ๋ฐฉ๋ฒ• - Fetch Join ์ œ๊ฑฐ

์ผ๋‹จ Fetch Join์„ ํฌ๊ธฐํ•ด์•ผํ•˜๋Š” ๊ฒƒ์€ ๋ถˆ๊ฐ€ํ”ผํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— findAll(Pageable) ๋ฉ”์†Œ๋“œ๋ฅผ ์ด์šฉํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

/**
 * Returns a {@link Page} of entities meeting the paging restriction provided in the {@link Pageable} object.
 *
 * @param pageable the pageable to request a paged result, can be {@link Pageable#unpaged()}, must not be
 *          {@literal null}.
 * @return a page of entities
 */
Page<T> findAll(Pageable pageable);
@DisplayName("๋ชจ๋“  ํŒ€์„ ์กฐํšŒํ•  ๋•Œ, ๊ธฐ๋ณธ์ œ๊ณต findAll + Pageable์€ Limit ์ฟผ๋ฆฌ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.")
@Test
void team_default_findAll_Pagination_test() {
    clearPersistenceContext();

    System.out.println("----------team_findAll_test start-----------");
    Page<Team> result = teamRepository.findAll(PageRequest.of(0, 1));
    List<Team> teamList = result.getContent();
    assertThat(teamList).hasSize(1);
    System.out.println("----------team_findAll_test mid-----------");
    teamList.stream()
            .map(Team::getMembers)
            .map(List::stream)
            .forEach(memberStream -> memberStream
                    .map(Member::getName)
                    .forEach(System.out::println)
            );
    System.out.println("----------team_findAll_test end-----------");
}

๋„ค ์ •์ƒ์ ์œผ๋กœ offset ์ฟผ๋ฆฌ๋ฅผ ํ†ตํ•ด pagination์„ ์ฒ˜๋ฆฌํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋ ‡๋‹ค๋ฉด fetch join์„ ์ œ๊ฑฐํ•œ @Query๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด Pagination์ด ์ž˜ ๋™์ž‘ํ•˜๋Š”์ง€ ๋ณผ๊นŒ์š”?

@Query(
        value = "select t from Team t",
        countQuery = "select count(t) from Team t"
)
List<Team> findTeamsWithoutFetchJoin(Pageable pageable);
@DisplayName("๋ชจ๋“  ํŒ€์„ ์กฐํšŒํ•  ๋•Œ, Fetch Join ์ œ๊ฑฐ + Pageable์€ Limit ์ฟผ๋ฆฌ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.")
@Test
void team_findAll_Exclusive_FetchJoin_with_Pagination_test() {
    clearPersistenceContext();

    System.out.println("----------team_findAll_test start-----------");
    Page<Team> result = teamRepository.findTeamsWithoutFetchJoin(PageRequest.of(0, 1));
    List<Team> teamList = result.getContent();
    assertThat(teamList).hasSize(1);
    System.out.println("----------team_findAll_test mid-----------");
    teamList.stream()
            .map(Team::getMembers)
            .map(List::stream)
            .forEach(memberStream -> memberStream
                    .map(Member::getName)
                    .forEach(System.out::println)
            );
    System.out.println("----------team_findAll_test end-----------");
}

์ง์ ‘ ๋งŒ๋“  ์ฟผ๋ฆฌ๋„ ์ •์ƒ์ ์œผ๋กœ Pagination์ด ๋™์ž‘ํ•˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. (์ถ”๊ฐ€ ํŒ: Batch Size๋ฅผ ์ด์šฉํ•œ๋‹ค๋ฉด N+1์— ๋Œ€ํ•ด ์ตœ์†Œํ•œ์˜ ์„ฑ๋Šฅ์„ ๋ณด์žฅ์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค)

Limit ๊ฒฐ์ •๋˜๋Š” ์‹œ์ 

SimpleJpaRepository - 692 line

SimpleJpaRepository์—์„œ ํŽ˜์ด์ง€ ์š”์ฒญ์ด ์žˆ์œผ๋ฉด offSet๊ณผ MaxResults๋ฅผ ์ง€์ •ํ•ด์ฃผ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ ์ •๋ณด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ์‹ค์ œ ์ฟผ๋ฆฌ๋ฅผ ๋‚ ๋ฆด ๋•Œ Offset๊ณผ First or Top์„ ์ด์šฉํ•ด์„œ Limit๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. (์ž์„ธํ•œ ๊ธ€์€ ๋‹ค์Œ์„ ์ฐธ์กฐ)

๋ฒˆ์™ธ - ~ToOne ๊ด€๊ณ„์—์„œ๋Š” ํŽ˜์ด์ง• ์ฒ˜๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅํ•˜๋‹ค.

์œ„์—์„œ Fetch Join + Pagination์„ ์‚ฌ์šฉํ•˜๋ฉด ์•ˆ๋˜๋Š” ์ผ€์ด์Šค๋“ค์€ ๋ชจ๋‘ ~ToMany ๊ด€๊ณ„์ผ ๋•Œ ์ž…๋‹ˆ๋‹ค. ์•„๋ž˜์—์„œ ์†Œ๊ฐœ๋“œ๋ฆฌ๋Š” ๊ฒƒ์€ ๊ทผ๋ณธ์ ์ธ ํ•ด๊ฒฐ๋ฐฉ๋ฒ•์€ ์•„๋‹ˆ์ง€๋งŒ, ~ToOne ๊ด€๊ณ„์—์„œ๋Š” ํŽ˜์ด์ง• ์ฒ˜๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅํ•˜๋‹ค๋Š” ๊ฒƒ์„ ํ…Œ์ŠคํŠธํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

@Query(value = "select m from Member m join fetch m.team")
List<Member> findMembersFetchJoin(Pageable pageable);
@DisplayName("~ToOne๊ด€๊ณ„์—์„œ๋Š” ํŽ˜์ด์ง•์ฒ˜๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅํ•˜๋‹ค")
@Test
void member_findAll_Pagination_test() {
    clearPersistenceContext();

    System.out.println("----------member_findAll_test start-----------");
    List<Member> memberList = memberRepository.findMembersFetchJoin(PageRequest.of(0, 2));
    assertThat(memberList).hasSize(2);
    System.out.println("----------member_findAll_test mid-----------");
    memberList.stream()
            .map(Member::getTeam)
            .map(Team::getName)
            .forEach(System.out::println);
    System.out.println("----------member_findAll_test end-----------");
}

๋ณด์‹œ๋Š” ๊ฒƒ๊ณผ ๊ฐ™์ด ~ToOne ๊ด€๊ณ„์—์„œ๋Š” ๋ฌธ์ œ ์—†์ด ์กฐํšŒ๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

2. MultipleBagFetchException - ํ•˜๋‚˜์˜ ์—”ํ‹ฐํ‹ฐ์—์„œ 2๊ฐœ ์ด์ƒ์˜ ์ปฌ๋ ‰์…˜์„ ์กฐํšŒํ•  ๋•Œ (~ToMany ๊ด€๊ณ„)

์œ„ ERD ์ฒ˜๋Ÿผ Team์ด members, sponsors๋ฅผ ๊ฐ–๋Š” ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค.

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Team {
        ...

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

    @OneToMany(mappedBy = "team", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private List<Sponsor> sponsors = new ArrayList<>();
}
public interface TeamRepository extends JpaRepository<Team, Long> {
    ...

    @Query(value = "select t from Team t join fetch t.members join fetch t.sponsors")
    Page<Team> findTeamsFetchJoinTwoCollection(Pageable pageable);
}
@DisplayName("๋‘˜ ์ด์ƒ์˜ ์ปฌ๋ ‰์…˜์„ ํŽ˜์น˜์กฐ์ธํ•  ์ˆ˜ ์—†๋‹ค.")
@Test
void Over_Two_collection_fetchJoin_Not_Allowed() throws Exception {
    System.out.println("--------Over_Two_collection_fetchJoin_Not_Allowed START-------------");
    // when
    List<Team> result = teamRepository.findTeamsFetchJoinTwoCollection(PageRequest.of(0, 3)).getContent();
    assertThat(teams).hasSize(3);

    // then
    System.out.println("--------Over_Two_collection_fetchJoin_Not_Allowed MID-------------");
    teams.forEach(team -> {
        System.out.println(team.getName());
        team.getMembers().stream().map(Member::getName).forEach(System.out::println);
        team.getSponsors().stream().map(Sponsor::getName).forEach(System.out::println);
    });

    System.out.println("--------Over_Two_collection_fetchJoin_Not_Allowed END-------------");
}

๋‘˜ ์ด์ƒ์˜ ์ปฌ๋ ‰์…˜์„ Fetch Join ํ•˜๋ ค๋‹ˆ cannot simultaneously fetch multiple bags ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ์–ด๋–ป๊ฒŒ ํ•˜๋ฉด MultipleBagFetchException์„ ํ”ผํ•  ์ˆ˜ ์žˆ์„๊นŒ์š”?

ํ•ด๊ฒฐ๋ฐฉ๋ฒ• 1. Set ์ž๋ฃŒํ˜•์œผ๋กœ ๋ณ€๊ฒฝ

๋‘˜ ์ด์ƒ์˜ ์ปฌ๋ ‰์…˜์„ ์กฐํšŒํ•˜๋ฉด ์ปฌ๋ ‰์…˜X์ปฌ๋ ‰์…˜ ์ฆ‰ ์นดํ…Œ์‹œ์•ˆ๊ณฑ์ด ๋ฐœ์ƒํ•˜๊ธฐ ๋•Œ๋ฌธ์— JPA์—์„œ ์˜ˆ์™ธ๋ฅผ ๋˜์ง€๊ฒŒ ํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ์ค‘๋ณต์ด ๋ฐœ์ƒํ•˜์ง€ ์•Š๊ธฐ ์œ„ํ•ด Set ์ž๋ฃŒํ˜•์œผ๋กœ ๋ณ€๊ฒฝํ•˜๋ฉด ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

@OneToMany(mappedBy = "team", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private Set<Member> members = new LinkedHashSet<>();

@OneToMany(mappedBy = "team", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private Set<Sponsor> sponsors = new LinkedHashSet<>();

์ˆœ์„œ๋ฅผ ๋ณด์žฅํ•ด์ฃผ๊ธฐ ์œ„ํ•ด LinkedHashSet์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

@Query(value = "select t from Team t join fetch t.members join fetch t.sponsors")
List<Team> findTeamsFetchJoinTwoCollection();

๋™์ผํ•œ ํ…Œ์ŠคํŠธ๋ฅผ ๋Œ๋ ค๋ณด๋ฉด ์ •์ƒ์ ์œผ๋กœ Fetch Join์ด ์ˆ˜ํ–‰ํ•˜์—ฌ N+1 ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ฃผ์˜ํ• ์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค. Fetch Join์€ ๊ธฐ๋ณธ์ ์œผ๋กœ Inner Join์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— ๋งŒ์•ฝ sponsor๋‚˜ member๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์ง€ ์•Š๋Š” team์ด ์žˆ๋‹ค๋ฉด? ์•„๋งˆ ํ…Œ์ŠคํŠธ๋ฅผ ๊นจ์ง€๊ฒŒ ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค. ํ•œ๋ฒˆ ํ™•์ธํ•ด๋ณผ๊นŒ์š”?

@BeforeEach
void setup() {
        ...

    Sponsor sponsor21 = Sponsor.builder().name("sponsor21").build();
    Sponsor sponsor22 = Sponsor.builder().name("sponsor22").build();
    Team team2 = Team.builder().name("team2").build();
    team2.addMember(Member.builder().name("member2-1").build());
    team2.addMember(Member.builder().name("member2-2").build());
//    team2.addSponsor(sponsor21);
//    team2.addSponsor(sponsor22);

    ...
}

ํ…Œ์ŠคํŠธ์šฉ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฝ์ž…ํ•  ๋•Œ, ์ผ๋ถ€๋Ÿฌ team2์˜ sponsor๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ์ฃผ์„์ฒ˜๋ฆฌํ•˜๊ณ  ํ…Œ์ŠคํŠธ๋ฅผ ๋Œ๋ ค๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

ํ…Œ์ŠคํŠธ๊ฐ€ ๊นจ์ง€๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ์ด๋Š” ์–ด์ฐŒ๋ณด๋ฉด ๋‹น์—ฐํ•œ ๊ฒฐ๊ณผ์ž…๋‹ˆ๋‹ค. Inner Join์„ ์ด์šฉํ•ด์„œ Team์„ ์กฐํšŒํ•ด์˜ค๊ธฐ ๋•Œ๋ฌธ์— ํ•ด๋‹น๋˜์ง€ ์•Š๋Š”๋‹ค๋ฉด ์กฐํšŒ๊ฐ€ ๋˜์ง€ ์•Š์„ ๊ฒƒ ์ž…๋‹ˆ๋‹ค. ๋งŒ์•ฝ member, sponsor๊ฐ€ ์—†๋Š” Team๋„ ๊ฐ€์ ธ์™€์•ผ ํ•œ๋‹ค๋ฉด inner join์ด ์•„๋‹Œ left join์„ ์‚ฌ์šฉํ•˜๋ฉด ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

@Query(value = "select t from Team t left join fetch t.members left join fetch t.sponsors")
Page<Team> findTeamsFetchJoinTwoCollection(Pageable pageable);

left join์„ ํ†ตํ•ด์„œ ๋ชจ๋“  ํŒ€์„ ๋‹ค ๊ฐ€์ ธ์˜ค๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ณผ์—ฐ ์ด ๋ฐฉ๋ฒ•์ด ์ตœ์„ ์ผ๊นŒ์š”? ์œ„ ๋ฐฉ๋ฒ•์—๋Š” Pagination์„ In Memory์—์„œ ํ•œ๋‹ค๋Š” ์ ์ด ์•„์ง ํ•ด๊ฒฐ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.

ํ•ด๊ฒฐ๋ฐฉ๋ฒ• 2. BatchSize

ํŠน์ • ์—๋Ÿฌ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋„๋ฉ”์ธ ๋ ˆ์ด์–ด์˜ ์ž๋ฃŒํ˜•์„ ๋ณ€๊ฒฝํ•˜๋Š” ๊ฒƒ์€ ์ข‹์€ ํ•ด๊ฒฐ์ฑ…์ด ์•„๋‹ˆ๋ผ๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— List ์ž๋ฃŒํ˜•์„ ์‚ฌ์šฉํ•ด์•ผํ•˜๊ณ  Pagination, ๋‘˜ ์ด์ƒ์˜ ์ปฌ๋ ‰์…˜์„ Fetch Join์„ ํ•œ๋‹ค๋Š” ๊ฐ€์ •ํ•˜์— Batch Size๋ฅผ ์ด์šฉํ•ด ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@TestPropertySource(properties = "spring.jpa.properties.hibernate.default_batch_fetch_size=10")
public class Team_MultipleBagEx_Solution2_Test {
        ...

        @DisplayName("๋‘˜ ์ด์ƒ์˜ ์ปฌ๋ ‰์…˜์„ ํŽ˜์น˜์กฐ์ธํ•  ์ˆ˜ ์—†๋‹ค -> Fetch Join(x) + BatchSize๋ฅผ ์ด์šฉํ•ด ํ•ด๊ฒฐ")
        @Test
        void Over_Two_collection_Not_Used_fetchJoin_and_Use_BatchSize() throws Exception {
            System.out.println("--------Over_Two_collection_Not_Used_fetchJoin_and_Use_BatchSize START-------------");
            // when
            List<Team> teams = teamRepository.findAll(PageRequest.of(0, 3)).getContent();
            assertThat(teams).hasSize(3);

            // then
            System.out.println("--------Over_Two_collection_Not_Used_fetchJoin_and_Use_BatchSize MID-------------");
            teams.forEach(team -> {
                System.out.println(team.getName());
                team.getMembers().stream().map(Member::getName).forEach(System.out::println);
                team.getSponsors().stream().map(Sponsor::getName).forEach(System.out::println);
            });

            System.out.println("--------Over_Two_collection_Not_Used_fetchJoin_and_Use_BatchSize END-------------");
        }
}

@TestPropertySource(properties = "spring.jpa.properties.hibernate.default_batch_fetch_size=10") ๋ฅผ ์ด์šฉํ•ด์„œ ํ•ด๋‹น ํ…Œ์ŠคํŠธ์— BatchSize๋ฅผ 10์œผ๋กœ ์„ค์ •ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

๋กœ๊ทธ๋ฅผ ์‚ดํŽด๋ณด๋ฉด, Paging์ด ์ž˜ ์ฒ˜๋ฆฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  Team์— ์†ํ•œ member, sponsor๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ ์ฟผ๋ฆฌ๊ฐ€ ๋ฐœ์ƒํ•˜์˜€๊ณ , where in ์ ˆ์„ ํ†ตํ•ด Eager Loading์„ ํ•˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋งŒ์•ฝ BatchSize๋ฅผ ์ ์šฉํ•˜์ง€ ์•Š๊ณ  ํ…Œ์ŠคํŠธ๋ฅผ ๋Œ๋ ธ๋‹ค๋ฉด ๋ช‡๋ฒˆ์˜ ์ฟผ๋ฆฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์„๊นŒ์š”? ์ „์ฒด ํŒ€์˜ ๊ฐœ์ˆ˜๊ฐ€ 3์ด๋ผ๋ฉด 4์˜ ์ฟผ๋ฆฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค.(N+1 ๋ฌธ์ œ)

4๊ฐœ์˜ ์ฟผ๋ฆฌ๊ฐ€ 3๊ฐœ๋กœ ์ค„์–ด๋“ค์—ˆ๋Š”๋ฐ 1๊ฐœ๋ฐ–์— ์ฐจ์ด๊ฐ€ ์•ˆ๋‚˜๋„ค? ๋ผ๊ณ  ์ƒ๊ฐํ•˜์‹ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์•„๋ž˜ ํ‘œ์™€ ๊ฐ™์ด ํŒ€์˜ ๊ฐœ์ˆ˜๊ฐ€ ๋งŽ์„์ˆ˜๋ก ์ฟผ๋ฆฌ ์ˆ˜๋Š” ๋ˆˆ์— ๋„๊ฒŒ ์ค„์–ด๋“ญ๋‹ˆ๋‹ค.

์ „์ฒด ํŒ€์˜ ๊ฐœ์ˆ˜ ๋ฐœ์ƒ ์ฟผ๋ฆฌ ์ˆ˜
(BatchSize ๋ฏธ์ ์šฉ)
๋ฐœ์ƒ ์ฟผ๋ฆฌ ์ˆ˜
(BatchSize=1000 ์ ์šฉ)
์ฟผ๋ฆฌ ์„ฑ๋Šฅ
3 4 3 25% ๊ฐ์†Œ
1,000 1,001 3 99.7% ๊ฐ์†Œ
10,000 10,001 21 99.8% ๊ฐ์†Œ
100,000 100,001 201 99.8% ๊ฐ์†Œ

์ตœ๋Œ€ 1000 ๋ถ„์˜ 1๋กœ ์ฟผ๋ฆฌ์ˆ˜๋ฅผ ์ค„์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. Fetch Join์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค๋ฉด Fetch Join์„ ์‚ฌ์šฉํ•˜๊ณ , ๋งŒ์•ฝ ๋‘˜ ์ด์ƒ์˜ ์ปฌ๋ ‰์…˜์„ ์กฐํšŒํ•ด์•ผํ•œ๋‹ค๋ฉด ๊ฐ€์žฅ ๋งŽ์€ ์ปฌ๋ ‰์…˜์— Fetch Join์„ ์ ์šฉํ•˜๊ณ  ๋‚˜๋จธ์ง€ ์ปฌ๋ ‰์…˜์— ๋Œ€ํ•ด์„œ๋Š” Batch Size๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ์ตœ์„ ์ผ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

3. FetchJoin ๋Œ€์ƒ์€ ๋ณ„์นญ์„ ์‚ฌ์šฉํ•˜์ง€ ๋ง์ž.

JPA ํ‘œ์ค€ ์ŠคํŽ™์—๋Š” Fetch Join ๋Œ€์ƒ์— ๋ณ„์นญ์ด ์—†์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ JPA ๊ตฌํ˜„์ฒด์ธ ํ•˜์ด๋ฒ„๋„ค์ดํŠธ๋Š” ๋ณ„์นญ์„ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค. ์ฆ‰ ๋ณ„์นญ์„ ์‚ฌ์šฉ์„ ํ•  ์ˆ˜ ์žˆ์ง€๋งŒ ์—ฌ๋Ÿฌ ๋ฌธ์ œ์ ๋“ค์ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์กฐ์‹ฌํ•ด์„œ ์‚ฌ์šฉํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.

Fetch Join ๋Œ€์ƒ์„ ON์ ˆ์—์„œ ์‚ฌ์šฉํ•˜๋ฉด ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.

@DisplayName("join fetch๋Š” on์ ˆ ์‚ฌ์šฉ์‹œ org.hibernate.query.SemanticException: Fetch join has a 'with' clause (use a filter instead) ๋ฐœ์ƒ")
@Test
void fetchJoin_NotAllow_On_condition() throws Exception {
    System.out.println("--------fetchJoin_NotAllow_On_condition START-------------");
    // when
    List<Team> teams = em.createQuery("select t from Team t join fetch t.members m on m.name = 'member11'", Team.class).getResultList();

    // then
    System.out.println("--------fetchJoin_NotAllow_On_condition MID-------------");
    teams.forEach(team -> {
        team.getMembers().stream().map(Member::getName).forEach(System.out::println);
        team.getSponsors().stream().map(Sponsor::getName).forEach(System.out::println);
    });
    System.out.println("--------fetchJoin_NotAllow_On_condition END-------------");
}

์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•˜๋ฉด on์ ˆ ๋Œ€์‹ ์— filter๋ฅผ ์‚ฌ์šฉํ•˜๋ผ๊ณ  ์—๋Ÿฌ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค.

๊ทธ๋Ÿผ where ์ ˆ์— ์กฐ๊ฑด์„ ๊ฑธ๋ฉด ์•ˆ๋˜๋‚˜?

์˜ˆ๋ฅผ ๋“ค์–ด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์š”๊ตฌ์‚ฌํ•ญ์ด ์žˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

  • Team์„ ์กฐํšŒํ•˜๋Š”๋ฐ ํ•ด๋‹น Team์— ์†ํ•œ ๋ชจ๋“  Member๋„ ํ•„์š”ํ•œ ์ƒํ™ฉ
  • ์ฟผ๋ฆฌ ์ตœ์ ํ™”๋ฅผ ์œ„ํ•ด Team.members์— ๋Œ€ํ•ด์„œ Fetch Join ์ ์šฉ
  • ์ด๋ฆ„์ด “member11”๋กœ ์‹œ์ž‘ํ•˜๋Š” member๋ฅผ ๊ฐ€์ ธ์™€์•ผํ•จ
  • ์œ„ ์š”๊ตฌ์‚ฌํ•ญ์œผ๋กœ ๋งŒ๋“ค์–ด์ง„ ์ฟผ๋ฆฌ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Œ
  • select t from Team t join fetch t.members m where m.name like 'member1%'

๋งŒ์•ฝ team์— ์†ํ•œ member๊ฐ€ member1, member12, member2๋ผ๊ณ  ๊ฐ€์ •ํ–ˆ์„ ๋•Œ ์œ„ ๊ฒฐ๊ณผ๋Š” member1, member12๊ฐ€ ๋‚˜์˜ฌ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ๋˜๋ฉด JPA ์ž…์žฅ์—์„œ๋Š” DB์™€ ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ์ด ๊นจ์ง€๊ฒŒ ๋˜๊ณ , ์ตœ์•…์˜ ๊ฒฝ์šฐ member2๊ฐ€ DB์—์„œ ์‚ญ์ œ๋  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

์ •๋ฆฌํ•˜์ž๋ฉด JPA์˜ ์—”ํ‹ฐํ‹ฐ๋Š” DB์˜ ๋ฐ์ดํ„ฐ์™€ ์ผ๊ด€์„ฑ์„ ์œ ์ง€ํ•ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ž„์˜๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๋นผ๊ณ  ์กฐํšŒํ•œ๋‹ค๋ฉด DB์— ํ•ด๋‹น ๋ฐ์ดํ„ฐ๊ฐ€ ์—†๋‹ค๊ณ  ํŒ๋‹จํ•˜๋Š” ๊ฒƒ๊ณผ ๋™์ผํ•ฉ๋‹ˆ๋‹ค.

๊ทธ๋Ÿผ ํ•˜์ด๋ฒ„๋„ค์ดํŠธ๋Š” ์™œ ํ—ˆ์šฉํ• ๊นŒ?

์™œ ํ•˜์ด๋ฒ„๋„ค์ดํŠธ์—์„œ๋Š” Fetch Join ๋Œ€์ƒ์˜ ๋ณ„์นญ์„ ํ—ˆ์šฉํ• ๊นŒ์š”? ๋งŒ์•ฝ ์กฐํšŒํ•˜๋Š” ๋ฐ์ดํ„ฐ๊ฐ€ DB์™€์˜ ์ผ๊ด€์„ฑ ๋ฌธ์ œ๊ฐ€ ์—†๋‹ค๋ฉด ์‚ฌ์šฉํ•ด๋„ ๋ฉ๋‹ˆ๋‹ค!

์˜ˆ๋ฅผ ๋“ค์–ด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ฟผ๋ฆฌ๋Š” ์•ˆ์ „ํ•˜๋‹ค๊ณ  ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

select m 
from Member m 
    join fetch m.team t
where t.name = 'team1'

join fetch ๋Œ€์ƒ์ธ Team์˜ ์ด๋ฆ„์„ ํ•„ํ„ฐ๋งํ•ด์„œ ๊ฐ€์ ธ์˜ค๋Š” ์ฟผ๋ฆฌ์ž…๋‹ˆ๋‹ค. ๊ฒฐ๋ก ์ ์œผ๋กœ ~ToOne ๊ด€๊ณ„์ธ ์ปฌ๋ ‰์…˜์— ๋Œ€ํ•ด์„œ ํ•„ํ„ฐ๋งํ•˜๋Š” ๊ฒƒ์€ ์•ˆ์ „ํ•˜๋‹ค๊ณ  ํ•  ์ˆ˜ ์žˆ๋Š”๊ฑฐ์ฃ . ํ•˜์ง€๋งŒ ์กฐํšŒ ์šฉ๋„๋กœ๋งŒ ์‚ฌ์šฉํ•œ๋‹ค๋Š” ๊ฐ€์ •ํ•˜์— ๋ง์”€๋“œ๋ฆฌ๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์‹ค๋ฌด์—์„œ๋Š” DB์™€ ์ผ๊ด€์„ฑ์ด ๊นจ์ง€๋”๋ผ๋„, ์กฐํšŒ ์šฉ๋„๋กœ๋งŒ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด Fetch Join ๋Œ€์ƒ์˜ ๋ณ„์นญ์„ ์‚ฌ์šฉํ•˜์—ฌ๋„ ๋ฉ๋‹ˆ๋‹ค. (๋Œ€์‹  2์ฐจ ์บ์‹œ ๋“ฑ๋“ฑ ์กฐ์‹ฌ..)

์ •๋ฆฌํ•˜์ž๋ฉด..

  • Fetch Join
    • ํŽ˜์ด์ง• API๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๋‹ค
      • ํ•ด๊ฒฐ๋ฐฉ๋ฒ•: ํŽ˜์ด์ง• ํ•„์š” ์‹œ: Fetch Join ์ œ๊ฑฐ + Batch Size, ๋ถˆ ํ•„์š” ์‹œ: Fetch Join ์‚ฌ์šฉ
    • ๋‘˜ ์ด์ƒ์˜ ์ปฌ๋ ‰์…˜์„ Fetch Join ํ•  ์ˆ˜ ์—†๋‹ค.
      • ํ•ด๊ฒฐ๋ฐฉ๋ฒ•: ๋ฐ์ดํ„ฐ๊ฐ€ ๋งŽ์€ ์ชฝ์— Fetch Join ๊ฑธ๊ณ , ๋‚˜๋จธ์ง€๋Š” Batch Size๋ฅผ ์ด์šฉํ•ด ์ตœ์†Œํ•œ์˜ ์„ฑ๋Šฅ ๋ณด์žฅ
    • Fetch Join ๋Œ€์ƒ ON, Where์ ˆ ์‚ฌ์šฉ ์กฐ์‹ฌ
      • ํ•ด๊ฒฐ๋ฐฉ๋ฒ•: Fetch Join ์‹œ ์กฐํšŒ ์šฉ๋„๋กœ๋งŒ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด OK, ์•„๋‹ˆ๋ผ๋ฉด ์ ˆ๋Œ€ ๊ธˆ์ง€

์ •๋ฆฌํ•˜๋‹ค๋ณด๋‹ˆ ๋‚ด์šฉ์ด ๋„ˆ๋ฌด ๊ธธ์–ด์กŒ๋„ค์š”.. JPA๋Š” ์ •๋ง ์•Œ์•„๋„ ๋์ด ์—†๋Š” ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค..

๋ถ€์กฑํ•œ ๊ธ€ ์ฝ์–ด์ฃผ์…”์„œ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค.

REFERENCES

MultipleBagFetchException ๋ฐœ์ƒ์‹œ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

JPA N+1 ๋ฐœ์ƒ์›์ธ๊ณผ ํ•ด๊ฒฐ๋ฐฉ๋ฒ• - Yun Blog | ๊ธฐ์ˆ  ๋ธ”๋กœ๊ทธ

JPA ๋ชจ๋“  N+1 ๋ฐœ์ƒ ์ผ€์ด์Šค๊ณผ ํ•ด๊ฒฐ์ฑ…

fetch join ๊ณผ limit ์„ ๊ฐ™์ด ์“ธ ๋•Œ ์ฃผ์˜ํ•˜์ž. (firstResult/maxResults specified with collection fetch)

fetch join ์‹œ ๋ณ„์นญ๊ด€๋ จ ์งˆ๋ฌธ์ž…๋‹ˆ๋‹ค - ์ธํ”„๋Ÿฐ | ์งˆ๋ฌธ & ๋‹ต๋ณ€

fetch join ๊ด€๋ จ ์งˆ๋ฌธ ๋“œ๋ฆฝ๋‹ˆ๋‹ค!! - ์ธํ”„๋Ÿฐ

๋ฐ˜์‘ํ˜•