N+1 정복기 기본편3

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

이번 포스팅에선 fetch join, entityGraph를 사용할때 중복데이터를 왜 제거해야 하는지 알아보도록 하겠습니다.(근데 entityGraph에선 학습테스트때 중복데이터가 발생하지 않더라고요.. 왜 그런지는 아직은 잘 모르겠습니다..)
이전 N+1 정복기 기본편, N+1 정복기 기본편2에서 모두 fetch join일 때 쿼리에 distinct를 붙였습니다. 왜 distinct키워드를 붙여야 하는지, 붙이지 않으면 어떤 문제가 발생할 수 있는지 학습테스트를 통해 알아보겠습니다.

카테시안 곱(Cartesian Product)

fetch join에서는 inner join, EntityGraph에서는 outer join이 일어납니다. 두 join 모두 카테시안 곱(Cartesian Product)가 발생해 데이터가 중복되는 문제가 발생합니다.

fetch join

학습테스트

10개의 team에 각각 2개의 member를 넣고 모든 team을 조회하는 학습테스트를 통해 카테시안 곱을 확인해보겠습니다.

Team3

@Entity
public class Team3 {

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

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

    @OneToMany(fetch = FetchType.LAZY, cascade = {PERSIST, REMOVE})
    @JoinColumn(name = "team_id")
    private List<Member3> member3s = new ArrayList<>();

    protected Team3() {
    }

    public Team3(String name, List<Member3> member3s) {
        this.name = name;
        this.member3s = member3s;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public List<Member3> getMembers() {
        return member3s;
    }
}

Repository

@Query(value = "SELECT t FROM Team3 t join fetch t.member3s")
List<Team3> findAllWithFetchJoin();

학습 테스트

@BeforeEach
void setUp() {
    for (int i = 0; i<10; ++i) {
        Member3 member = new Member3("member" + i);
        Member3 member2 = new Member3("member2" + i);
        List<Member3> members = List.of(member, member2);

        Team3 team = new Team3("team" + i, members);
        team3Repository.save(team);

        em.flush();
        em.clear();
    }
}

@DisplayName("카테시안 곱이 발생하는 fetch join")
@Test
void fetchJoin_CartesianProduct() {
    List<Team3> teams = team3Repository.findAllWithFetchJoin();

    for (Team3 team : teams) {
        assertThat(team.getMembers()).hasSize(2);
    }

    assertThat(teams).hasSize(20);
}

분명 팀은 10개인데, 2개의 멤버가 들어간 20개의 팀이 조회되는 것을 알 수 있습니다. 즉 데이터가 중복으로 조회되는 문제가 발생합니다.

sql의 DISTINCT를 사용해 중복 데이터 제거

repository의 @Query에 DISTINCT를 사용하면 중복되는 데이터를 제거할 수 있습니다.

Repository

@Query(value = "SELECT DISTINCT t FROM Team3 t join fetch t.member3s")
List<Team3> findAllWithDistinctFetchJoin();

학습 테스트

@DisplayName("sql에 distinct를 사용해 중복 데이터 제거")
@Test
void fetchJoin_Distinct() {
    List<Team3> teams = team3Repository.findAllWithDistinctFetchJoin();

    for (Team3 team : teams) {
        assertThat(team.getMembers()).hasSize(2);
    }

    assertThat(teams).hasSize(10);
}

Java의 Set 자료구조를 이용해 중복 데이터 제거

Team엔티티에서 Member를 Set에 저장하면 중복 데이터를 제거할 수 있습니다.

Team3

@Entity
public class Team3 {

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

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

    @OneToMany(fetch = FetchType.LAZY, cascade = {PERSIST, REMOVE})
    @JoinColumn(name = "team_id")
    private Set<Member3> member3s = new HashSet<>();

    protected Team3() {
    }

    public Team3(String name, Set<Member3> member3s) {
        this.name = name;
        this.member3s = member3s;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public Set<Member3> getMembers() {
        return member3s;
    }
}

학습 테스트

@DisplayName("java의 set을 사용해 중복 데이터 제거")
@Test
void fetchJoin_Set() {
    List<Team3> teams = team3Repository.findAllWithDistinctFetchJoin();

    for (Team3 team : teams) {
        assertThat(team.getMembers()).hasSize(2);
    }

    assertThat(teams).hasSize(10);
}

EntityGraph

Team4

@Entity
public class Team4 {

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

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

    @OneToMany(fetch = FetchType.LAZY, cascade = {PERSIST, REMOVE})
    @JoinColumn(name = "team_id")
    private List<Member4> member4s = new ArrayList<>();

    protected Team4() {
    }

    public Team4(String name, List<Member4> member4s) {
        this.name = name;
        this.member4s = member4s;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public List<Member4> getMembers() {
        return member4s;
    }
}

Repository

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

학습 테스트

@BeforeEach
void setUp() {
    for (int i = 0; i < 10; ++i) {
        Member4 member = new Member4("member" + i);
        Member4 member2 = new Member4("member2" + i);
        List<Member4> members = List.of(member, member2);

        Team4 team = new Team4("team" + i, members);
        team4Repository.save(team);

        em.flush();
        em.clear();
    }
}

@DisplayName("entityGraph 카테시안 곱으로 인한 중복 데이터 확인")
@Test
void entityGraph_CartesianProduct() {
    List<Team4> teams = team4Repository.findAll();

    for (Team4 team : teams) {
        assertThat(team.getMembers()).hasSize(2);
    }

    assertThat(teams).hasSize(10);
}

이상하게 entityGraph에선 DISTINCT, Set을 사용하지 않아도 중복 데이터가 발생하지 않았다..
왜 그런지 아직은 잘 모르겠다. 추후 알게되면 추가하겠습니다..

출처

https://jojoldu.tistory.com/165

profile
안녕하세요!

0개의 댓글