스프링-@BatchSize 사용법

이진우·2023년 10월 11일
0

스프링 학습

목록 보기
18/41

BatchSize가 필요한 이유

Team은 1대 다의 1을 담당하고 있고, Member는 1대 다의 다를 가정하고 있고
Team 에는 @OneToMany를 사용하고 있다.

 Team teamA=new Team();
            teamA.setName("팀A");
            em.persist(teamA);

            Team teamB=new Team();
            teamB.setName("팀B");
            em.persist(teamB);

            for(int i=0;i<48;i++){
                Team team=new Team();
                team.setName("팀"+(i+1));
                em.persist(team);
            }

            for(int i=0;i<50;i++){
                Member member=new Member();
                member.setUsername("회원"+(i+1));
                member.setTeam(teamA);
                em.persist(member);
            }
            for(int i=0;i<50;i++){
                Member member=new Member();
                member.setUsername("회원"+(i+51));
                member.setTeam(teamB);
                em.persist(member);
            }

            em.flush();
            em.clear();
            System.out.println("======================");
            List<Team> teams=em.createQuery("select t from Team t", Team.class).getResultList();
            for (Team team : teams) {
                System.out.println("team:"+team.getName());
                for(Member mem:team.getMembers()){
                    System.out.println(mem.getUsername());
                }
            }
            tx.commit();

위와 같은 코드가 있다.

위는 길긴 하지만 SystemOut.println("============"); 이전은 그냥 데이터를 insert 한 것 뿐이므로 ===== 이후 부터만 보면 된다.

위에 대한 실행 결과를 보면

 /* select
      t 
  from
      Team t */ select
          team0_.id as id1_3_,
          team0_.name as name2_3_ 
      from
          Team team0_
team:팀A
Hibernate: 
  select
      members0_.TEAM_ID as team_id5_0_0_,
      members0_.id as id1_0_0_,
      members0_.id as id1_0_1_,
      members0_.age as age2_0_1_,
      members0_.TEAM_ID as team_id5_0_1_,
      members0_.type as type3_0_1_,
      members0_.username as username4_0_1_ 
  from
      Member members0_ 
  where
      members0_.TEAM_ID=?
회원1
회원2
회원3
회원4
회원5
. . .
회원50
team:팀B
Hibernate: 
  select
      members0_.TEAM_ID as team_id5_0_0_,
      members0_.id as id1_0_0_,
      members0_.id as id1_0_1_,
      members0_.age as age2_0_1_,
      members0_.TEAM_ID as team_id5_0_1_,
      members0_.type as type3_0_1_,
      members0_.username as username4_0_1_ 
  from
      Member members0_ 
  where
      members0_.TEAM_ID=?
회원51
회원52
회원53
회원54
...
회원100
team:팀1
Hibernate: 
  select
      members0_.TEAM_ID as team_id5_0_0_,
      members0_.id as id1_0_0_,
      members0_.id as id1_0_1_,
      members0_.age as age2_0_1_,
      members0_.TEAM_ID as team_id5_0_1_,
      members0_.type as type3_0_1_,
      members0_.username as username4_0_1_ 
  from
      Member members0_ 
  where
      members0_.TEAM_ID=?
team:팀2
Hibernate: 
  select
      members0_.TEAM_ID as team_id5_0_0_,
      members0_.id as id1_0_0_,
      members0_.id as id1_0_1_,
      members0_.age as age2_0_1_,
      members0_.TEAM_ID as team_id5_0_1_,
      members0_.type as type3_0_1_,
      members0_.username as username4_0_1_ 
  from
      Member members0_ 
  where
      members0_.TEAM_ID=?
team:팀3
Hibernate: 
  select
      members0_.TEAM_ID as team_id5_0_0_,
      members0_.id as id1_0_0_,
      members0_.id as id1_0_1_,
      members0_.age as age2_0_1_,
      members0_.TEAM_ID as team_id5_0_1_,
      members0_.type as type3_0_1_,
      members0_.username as username4_0_1_ 
  from
      Member members0_ 
  where
      members0_.TEAM_ID=?

이렇게 쿼리가 나간다.
이렇게 쿼리가 나가는 이유를 설명해보자면 Team 과 Member는 다대일 관계인데 팀입장에서 Member는 @OneToMany관계이다.

이는 기본적으로 LAZY 로딩 관계이고, 팀을 select해서 가져온다면 팀에 대한 정보만 가져오고 Member에 대한 정보는 가지고 오지 않는다.

추후에 Member 에 대한 정보를 사용할때 그제서야 그 Member가 속해 있는 팀에 대한 Members 즉 컬렉션에 대한 정보를 가지고 온다.

그래서 이를 통해 이전에 Member에 의해 한번 조회되었던 적이 있던 Team 은 그에 속해 있는 Member 들을 조회할때는 쿼리가 필요하지 않지만, 새로운 팀을 가진 Member를 조회할때는 다시 쿼리를 내주어야 했다.

다대일에서는 이 문제를 단순히 Fetch JOIN으로 해결할 수 있었다.

하지만 일대다(컬렉션)에서도 그럴까??

일대다(컬렉션) 페치 조인에서의 문제점

List<Team> teams=em.createQuery("select t from Team t join fetch t.members", Team.class).getResultList();

단순히 이렇게만 바꾸어보자!

콘솔창에 팀의 정보가 다르게 찍힌다. 즉 Team 의 갯수만큼 TeamA라고 찍히는게 아니라 이전과는 다르게 TeamA의 정보가 50번씩(Team이 소유한 Member의 수만큼) 찍힌다. 이전에는 한번 이였다. 이는 굉장히 비효율 적이다

그 이유는 아래 그림에 있다.

fetch JOIN을 하면서 JOIN 된 테이블에서 Team을 찾고 그에 속해 있는 Member를 출력하면 위 그림에서는 2번 반복이 될 것이다. 우리의 경우 JOIN 된 테이블에서 팀A에 대한 레코드가 50개이므로 팀A의 이름 50번, 그 팀에 속한 Member 의 이름 50번 총 50*50=2500번이 찍힌다.

이처럼 컬렉션 페치 조인을 사용하기에 team 의 정보를 가지고 올 떄 부담스럽다.
이 과정은 페이징 처리에서 매우 부담스럽다.
팀 A 에 대한 페이징을 하는데 팀 A 에 대한 정보는 여러개 나오기 때문이다.
페이징 처리를 안할떄는 distinct 와 페치 조인을 같이 써주면 된다.
암튼 페이징 처리를 사용하려면
그렇다면 Fetch JOIN대신 성능을 높일 수 있는 방법이 필요하다.

성능을 높이기 위한 방법

반대편에서 FetchJOIN 쓰기

일대다는 기본적으로 다 쪽에서 Fetch JOIN으로 접근할 수 있다. 다에서는 일대다 Fetch JOIN 중복과 달리 아래 그림과 같이 중복되는 레코드는 없다.

그러면 1대다는 무조건 Fetch JOIN을 다에서만 쓰면 해결되는 문제인가???

그것은 아니다. 일쪽에서 끝장을 봐야 하는 것도 있다고 한다.

출처:https://www.inflearn.com/questions/952684/%EC%BB%AC%EB%A0%89%EC%85%98%EC%9D%98-%ED%95%84%EC%9A%94%EC%84%B1%EC%97%90-%EB%8C%80%ED%95%B4-%EC%A7%88%EB%AC%B8-%EC%A2%80-%EB%93%9C%EB%A6%AC%EA%B2%A0%EC%8A%B5%EB%8B%88%EB%8B%A4

이런 경우 사용할 수 있는게 BatchSize이다.

BatchSize 사용

    @OneToMany(mappedBy = "team")
    @BatchSize(size = 3)
    private List<Member> members=new ArrayList<>();

위와 같이 수동으로 직접 설정해서 사용할수도 있고, default 값을 설정할 수 도 있다.

이제 위의 코드를 다시한번 실행시켜 보자

BatchSize 쿼리 결과

  /* select
        t 
    from
        Team t */ select
            team0_.id as id1_3_,
            team0_.name as name2_3_ 
        from
            Team team0_
team:팀A
Hibernate: 
    /* load one-to-many jpa_jpql_dionisos198.Team.members */ select
        members0_.TEAM_ID as team_id5_0_1_,
        members0_.id as id1_0_1_,
        members0_.id as id1_0_0_,
        members0_.age as age2_0_0_,
        members0_.TEAM_ID as team_id5_0_0_,
        members0_.type as type3_0_0_,
        members0_.username as username4_0_0_ 
    from
        Member members0_ 
    where
        members0_.TEAM_ID in (
            ?, ?, ?
        )
회원1
회원2
회원3
...

회원50
team:팀B
회원51
...
회원98
회원99
회원100
team:팀1
team:팀2
Hibernate: 
    /* load one-to-many jpa_jpql_dionisos198.Team.members */ select
        members0_.TEAM_ID as team_id5_0_1_,
        members0_.id as id1_0_1_,
        members0_.id as id1_0_0_,
        members0_.age as age2_0_0_,
        members0_.TEAM_ID as team_id5_0_0_,
        members0_.type as type3_0_0_,
        members0_.username as username4_0_0_ 
    from
        Member members0_ 
    where
        members0_.TEAM_ID in (
            ?, ?, ?
        )
team:팀3
team:팀4
team:팀5
Hibernate: 
    /* load one-to-many jpa_jpql_dionisos198.Team.members */ select
        members0_.TEAM_ID as team_id5_0_1_,
        members0_.id as id1_0_1_,
        members0_.id as id1_0_0_,
        members0_.age as age2_0_0_,
        members0_.TEAM_ID as team_id5_0_0_,
        members0_.type as type3_0_0_,
        members0_.username as username4_0_0_ 
    from
        Member members0_ 
    where
        members0_.TEAM_ID in (
            ?, ?, ?
        )
team:팀6
team:팀7
team:팀8

위의 코드에 대한 부연설명을 해보자면
Team A 에 있는 member의 메서드를 호출하면=>
BatchSize만큼 팀 갯수의 members를 모두 가져온다.

예를 들면 BatchSize가 위와 같이 3일때

member에 대한 정보를 그 팀에 대한 members(팀 A의 members)와 그 다음 팀에 대한 members(팀 B의 members), 그 다음 팀에 대한 members(팀 1의 members) 이렇게 BatchSize만큼 팀A에 속한 member의 메서드가 호출될때 가지고 온다.

즉, 팀에 대한 select 문과 Member 에 대한 더 적어진 Member에 대한 쿼리로 이제 맨 위와 달리 member 의 메서드를 호출하면 Member 의 In쿼리로 TEAM ID를 BatchSize 만큼 불러온다.

위를 통해 team의 ID가 BatchSize만큼 차례대로 증가해서 팀의 BatchSize만큼의 Member를 한번에 다 끌어오는 것을 알 수 있다.

조금더 직관적으로 BatchSize(size=25)로 설정시

======================
Hibernate: 
    /* select
        t 
    from
        Team t */ select
            team0_.id as id1_3_,
            team0_.name as name2_3_ 
        from
            Team team0_
team:팀A
Hibernate: 
    /* load one-to-many jpa_jpql_dionisos198.Team.members */ select
        members0_.TEAM_ID as team_id5_0_1_,
        members0_.id as id1_0_1_,
        members0_.id as id1_0_0_,
        members0_.age as age2_0_0_,
        members0_.TEAM_ID as team_id5_0_0_,
        members0_.type as type3_0_0_,
        members0_.username as username4_0_0_ 
    from
        Member members0_ 
    where
        members0_.TEAM_ID in (
            ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
        )
회원2
회원3
회원4
...
회원49
회원50
team:팀B
회원1
회원51
...
회원99
회원100
team:팀1
team:팀2
team:팀3

...
team:팀23
team:팀24
Hibernate: 
    /* load one-to-many jpa_jpql_dionisos198.Team.members */ select
        members0_.TEAM_ID as team_id5_0_1_,
        members0_.id as id1_0_1_,
        members0_.id as id1_0_0_,
        members0_.age as age2_0_0_,
        members0_.TEAM_ID as team_id5_0_0_,
        members0_.type as type3_0_0_,
        members0_.username as username4_0_0_ 
    from
        Member members0_ 
    where
        members0_.TEAM_ID in (
            ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
        )
team:팀25
team:팀26
...

위에 대한 부연설명을 하자면

팀A에 속해 있는 Members 에서 Members에 속해 있는 member의 메서드가 호출되면
BatchSize(25)만큼의 team에 해당하는 members를 모조리 가져오므로 팀A,팀B,팀1,팀2...팀 23까지의 Members는 모두 가져오고 팀 24의 Member의 메서드가 호출될때 또 그다음 25개까지의 Team에 해당하는 Members를 모두 가져온다.

맨위의 쿼리와 비교해서 쿼리 양 자체가 작아진 것이 파악이 가능하다.

직관적 예제

Team teamA=new Team();
            teamA.setName("팀A");
            em.persist(teamA);

            Team teamB=new Team();
            teamB.setName("팀B");
            em.persist(teamB);
            
            Team teamC=new Team();
            teamC.setName("팀C");
            em.persist(teamC);
            
            Team teamD=new Team();
            teamD.setName("팀D");
            em.persist(teamD);
            
            Team teamE=new Team();
            teamE.setName("팀E");
            em.persist(teamE);
            
            for(int i=0;i<4;i++){
                Member member=new Member();
                member.setUsername("멤버"+(i+1));
                member.setTeam(teamA);
                em.persist(member);
            }
            for(int i=0;i<3;i++){
                Member member=new Member();
                member.setUsername("멤버"+(i+5));
                member.setTeam(teamB);
                em.persist(member);
            }
            for(int i=0;i<2;i++){
                Member member=new Member();
                member.setUsername("멤버"+(i+8));
                member.setTeam(teamC);
                em.persist(member);
            }
            for(int i=0;i<1;i++){
                Member member=new Member();
                member.setUsername("멤버"+(i+10));
                member.setTeam(teamD);
                em.persist(member);
            }
            Member member=new Member();
            member.setUsername("멤버11");
            member.setTeam(teamE);
            em.persist(member);
            
            em.flush();
            em.clear();

            List<Team> teams=em.createQuery("select t from Team t", Team.class).getResultList();
            for (Team team : teams) {
                System.out.println("team:"+team.getName());
                for(Member mem:team.getMembers()){
                    System.out.println(mem.getUsername());
                }
            }
            
            tx.commit();

이게 기본이라면

BatchSIze를 쓰지 않았을 경우

 
 /* select
      t 
  from
      Team t */ select
          team0_.id as id1_3_,
          team0_.name as name2_3_ 
      from
          Team team0_
team:팀A
Hibernate: 
  select
      members0_.TEAM_ID as team_id5_0_0_,
      members0_.id as id1_0_0_,
      members0_.id as id1_0_1_,
      members0_.age as age2_0_1_,
      members0_.TEAM_ID as team_id5_0_1_,
      members0_.type as type3_0_1_,
      members0_.username as username4_0_1_ 
  from
      Member members0_ 
  where
      members0_.TEAM_ID=?
멤버1
멤버2
멤버3
멤버4
team:팀B
Hibernate: 
  select
      members0_.TEAM_ID as team_id5_0_0_,
      members0_.id as id1_0_0_,
      members0_.id as id1_0_1_,
      members0_.age as age2_0_1_,
      members0_.TEAM_ID as team_id5_0_1_,
      members0_.type as type3_0_1_,
      members0_.username as username4_0_1_ 
  from
      Member members0_ 
  where
      members0_.TEAM_ID=?
멤버5
멤버6
멤버7
team:팀C
Hibernate: 
  select
      members0_.TEAM_ID as team_id5_0_0_,
      members0_.id as id1_0_0_,
      members0_.id as id1_0_1_,
      members0_.age as age2_0_1_,
      members0_.TEAM_ID as team_id5_0_1_,
      members0_.type as type3_0_1_,
      members0_.username as username4_0_1_ 
  from
      Member members0_ 
  where
      members0_.TEAM_ID=?
멤버8
멤버9
team:팀D
Hibernate: 
  select
      members0_.TEAM_ID as team_id5_0_0_,
      members0_.id as id1_0_0_,
      members0_.id as id1_0_1_,
      members0_.age as age2_0_1_,
      members0_.TEAM_ID as team_id5_0_1_,
      members0_.type as type3_0_1_,
      members0_.username as username4_0_1_ 
  from
      Member members0_ 
  where
      members0_.TEAM_ID=?
멤버10
team:팀E
Hibernate: 
  select
      members0_.TEAM_ID as team_id5_0_0_,
      members0_.id as id1_0_0_,
      members0_.id as id1_0_1_,
      members0_.age as age2_0_1_,
      members0_.TEAM_ID as team_id5_0_1_,
      members0_.type as type3_0_1_,
      members0_.username as username4_0_1_ 
  from
      Member members0_ 
  where
      members0_.TEAM_ID=?
멤버11

BatchSize 3을 설정한 경우

 /* select
        t 
    from
        Team t */ select
            team0_.id as id1_3_,
            team0_.name as name2_3_ 
        from
            Team team0_
team:팀A
Hibernate: 
    /* load one-to-many jpa_jpql_dionisos198.Team.members */ select
        members0_.TEAM_ID as team_id5_0_1_,
        members0_.id as id1_0_1_,
        members0_.id as id1_0_0_,
        members0_.age as age2_0_0_,
        members0_.TEAM_ID as team_id5_0_0_,
        members0_.type as type3_0_0_,
        members0_.username as username4_0_0_ 
    from
        Member members0_ 
    where
        members0_.TEAM_ID in (
            ?, ?, ?
        )
멤버1
멤버2
멤버3
멤버4
team:팀B
멤버5
멤버6
멤버7
team:팀C
멤버8
멤버9
team:팀D
Hibernate: 
    /* load one-to-many jpa_jpql_dionisos198.Team.members */ select
        members0_.TEAM_ID as team_id5_0_1_,
        members0_.id as id1_0_1_,
        members0_.id as id1_0_0_,
        members0_.age as age2_0_0_,
        members0_.TEAM_ID as team_id5_0_0_,
        members0_.type as type3_0_0_,
        members0_.username as username4_0_0_ 
    from
        Member members0_ 
    where
        members0_.TEAM_ID in (
            ?, ?
        )
멤버10
team:팀E
멤버11
profile
기록을 통해 실력을 쌓아가자

0개의 댓글