개인 공부/JPA

JPA N+1 문제

hanyugyeong 2024. 4. 12. 16:33
반응형
SMALL

1. JPA n+1 문제는 무엇?

->JPA의 N+1 문제는 데이터를 검색할 때 발생하는 성능 문제 중 하나입니다. 이 문제는 일반적으로 다음과 같은 상황에서 발생합니다. 

1. 부모 엔티티를 검색합니다. 

2.각 부모 엔티티에 대해 연결된 자식 엔티티를 검색합니다. 

 

만약 자식 엔티티들이 많은 경우, 각 부모 엔티티에 대해 추가적인 쿼리가 실행되어야 합니다. 이러한 쿼리들이 많아지면 데이터 베이스에 대한 부하가 증가하고, 응답 시간이 길어지는 문제가 발생합니다. 

 

예를 들어, 회원(Member)과 그 회원이 속한 팀(Team)의 관계가 있을 때, 각 회원을 검색하고자 할 때 N+1 문제가 발생할 수 있습니다. 즉, 모든 회원을 검색하는 쿼리를 실행한 후, 각 회원에 대해 해당 회원이 속한 팀을 추가로 검색하는 쿼리가 실행됩니다. 이때 총 쿼리 수는 N+1이 됩니다. N은 부모 엔티티의 수를 나타내고, 1은 추가적인 쿼리를 의미합니다.

 

@Entity
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Member{

    @Id
    @Column(name="member_id")
    @GeneratedValue
    private Long id;

    private String username;

    private int age;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name="TEAM_ID")
    private Team team;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Team getTeam() {
        return team;
    }

    public void setTeam(Team team) {
        this.team = team;
    }

    @Override
    public String toString() {
        return "Member{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", age=" + age +
                ", team=" + team +
                '}';
    }

}
@Entity
@Builder
public class Team {

    @Id @GeneratedValue
    @Column(name="TEAM_ID")
    private Long id;

    private String name;
    
    public Team(Long id, String name) {
        this.id = id;
        this.name = name;
    }
    
    public Team() {

    }
    
    public Long getId() {
         return id;
     }

     public void setId(Long id) {
         this.id = id;
     }

     public String getName() {
         return name;
     }

     public void setName(String name) {
         this.name = name;
     }

    @Override
    public String toString() {
        return "Team{" +
                "id='" + id + '\'' +
                ", name='" + name + '\'' +
                '}';
    }
}

더미 데이터 생성

team 1에 속하는 맴버 10명과 team 2에 속하는 맴버 10명을 더미 데이터로 생성했습니다

 @Test
   void setup(){
       EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("jpabook");
       EntityManager entityManager = entityManagerFactory.createEntityManager();

       entityManager.getTransaction().begin();

       // 팀 생성
       Team team = entityManager.find(Team.class, 1L); // 이미 영속화된 팀 엔티티를 가져옴
       if (team == null) { // 팀이 없으면 새로 생성
           team = Team.builder().name("팀1").build();
           entityManager.persist(team);
       }

       // 회원 생성 및 팀 연결
       for(int i=0;i<10;i++){
           Member member = Member.builder().username("member"+i).age(i).team(team).build();
           entityManager.persist(member);
       }

       entityManager.getTransaction().commit();

       entityManager.close();
       entityManagerFactory.close();
   }

아래 코드로 조회할경우 

   @Test
    void testNPlusOneIssue(){
        EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("jpabook");
        EntityManager entityManager = entityManagerFactory.createEntityManager();

        entityManager.getTransaction().begin();

        List<Member> members = entityManager.createQuery("SELECT m FROM Member m", Member.class)
                .getResultList();

        for (Member member : members) {
            System.out.println("Member: " + member.getUsername() + ", Team: " + member.getTeam().getName());
        }

        entityManager.getTransaction().commit();

        entityManager.close();
        entityManagerFactory.close();


    }

아래와 같이 member를 select 한뒤 team의 쿼리가 2개 나가게 됩니다

 /* SELECT
        m 
    FROM
        Member m */ select
            member0_.member_id as member_i1_0_,
            member0_.age as age2_0_,
            member0_.TEAM_ID as TEAM_ID4_0_,
            member0_.username as username3_0_ 
        from
            Member member0_
Hibernate: 
    select
        team0_.TEAM_ID as TEAM_ID1_1_0_,
        team0_.name as name2_1_0_ 
    from
        Team team0_ 
    where
        team0_.TEAM_ID=?
Member: member0, Team: 팀1
Member: member1, Team: 팀1
Member: member2, Team: 팀1
Member: member3, Team: 팀1
Member: member4, Team: 팀1
Member: member5, Team: 팀1
Member: member6, Team: 팀1
Member: member7, Team: 팀1
Member: member8, Team: 팀1
Member: member9, Team: 팀1
Hibernate: 
    select
        team0_.TEAM_ID as TEAM_ID1_1_0_,
        team0_.name as name2_1_0_ 
    from
        Team team0_ 
    where
        team0_.TEAM_ID=?
Member: member0, Team: 팀2
Member: member1, Team: 팀2
Member: member2, Team: 팀2
Member: member3, Team: 팀2
Member: member4, Team: 팀2
Member: member5, Team: 팀2
Member: member6, Team: 팀2
Member: member7, Team: 팀2
Member: member8, Team: 팀2
Member: member9, Team: 팀2

위와 같이 N+1 문제가 생기게 돼면 성능에 부정적인 영향을 미치게 됩니다. 

  1. 성능 저하: 각각의 추가 쿼리가 데이터베이스에 대한 추가적인 요청을 생성하므로 전체적인 응답 시간이 증가합니다. 특히 데이터가 많은 경우에는 이러한 추가 쿼리들이 복잡해질 수 있고, 데이터베이스 서버에 부하를 줄 수 있습니다.
  2. 네트워크 부하: 추가 쿼리들은 데이터베이스와 애플리케이션 간의 네트워크 트래픽을 증가시킵니다. 이는 더 많은 데이터 전송으로 이어져서 대역폭 소모를 증가시킬 수 있습니다.
  3. 메모리 소비: 추가 쿼리로 인해 애플리케이션 메모리에 불필요한 데이터가 쌓일 수 있습니다. 이는 메모리 사용량을 증가시키고, GC(Garbage Collection) 작업을 더 자주 수행하게 할 수 있습니다.
  4. 데이터베이스 부하: 추가 쿼리들은 데이터베이스에 더 많은 부하를 주게 됩니다. 특히 대량의 데이터를 다룰 때는 데이터베이스 서버의 부하가 증가하고, 이로 인해 다른 쿼리들의 응답 시간이 느려질 수 있습니다.

 

N+1 문제를 해결하기 위한 방안 

1.Fetch JOIN 

 @Test
    void testFetchJoin() {
        EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("jpabook");
        EntityManager entityManager = entityManagerFactory.createEntityManager();

        entityManager.getTransaction().begin();

        List<Member> members = entityManager.createQuery(
            "SELECT m FROM Member m JOIN FETCH m.team", Member.class)
            .getResultList();

        for (Member member : members) {
            System.out.println("Member: " + member.getUsername() + ", Team: " + member.getTeam().getName());
        }

        entityManager.getTransaction().commit();

        entityManager.close();
        entityManagerFactory.close();
    }

 

결과값

SELECT
        m 
    FROM
        Member m 
    JOIN
        FETCH m.team */ select
            member0_.member_id as member_i1_0_0_,
            team1_.TEAM_ID as TEAM_ID1_1_1_,
            member0_.age as age2_0_0_,
            member0_.TEAM_ID as TEAM_ID4_0_0_,
            member0_.username as username3_0_0_,
            team1_.name as name2_1_1_ 
        from
            Member member0_ 
        inner join
            Team team1_ 
                on member0_.TEAM_ID=team1_.TEAM_ID
Member: member0, Team: 팀1
Member: member1, Team: 팀1
Member: member2, Team: 팀1
Member: member3, Team: 팀1
Member: member4, Team: 팀1
Member: member5, Team: 팀1
Member: member6, Team: 팀1
Member: member7, Team: 팀1
Member: member8, Team: 팀1
Member: member9, Team: 팀1
Member: member0, Team: 팀2
Member: member1, Team: 팀2
Member: member2, Team: 팀2
Member: member3, Team: 팀2
Member: member4, Team: 팀2
Member: member5, Team: 팀2
Member: member6, Team: 팀2
Member: member7, Team: 팀2
Member: member8, Team: 팀2
Member: member9, Team: 팀2

2. BatchSize

BatchSize를 사용하여 N+1 문제를 해결할 수 있습니다. @BatchSize 어노테이션을 사용하여 일괄 처리 크기(batch size)를 설정할 수 있습니다. 이렇게 하면 한 번에 여러 개의 엔티티를 검색하여 성능을 향상시킬 수 있습니다. 그러나 @BatchSize 애노테이션은 컬렉션에 대해 사용할 수 있기 때문에, OneToMany나 ManyToOne과 같은 연관 관계 가 있는 컬렉션에만 적용됩니다. 

 

아래는 @BatchSize 애노테이션을 사용하여 팀(Team) 엔티티의 회원(Member) 컬렉션에 대한 일괄 처리 크기를 설정하는 코드입니다.

import javax.persistence.*;

@Entity
public class Team {

    @Id @GeneratedValue
    @Column(name="TEAM_ID")
    private Long id;

    private String name;

    @OneToMany(mappedBy = "team")
    @BatchSize(size = 10) // 일괄 처리 크기 설정
    private List<Member> members;

    // 생성자, getter/setter, toString 등 생략
}

@Entity
public class Member{

    @Id
    @Column(name="member_id")
    @GeneratedValue
    private Long id;

    private String username;

    private int age;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name="TEAM_ID")
    private Team team;

    // 생성자, getter/setter, toString 등 생략
}

위의 코드에서 @BatchSize(size = 10) 애노테이션은 Team 엔티티의 members 컬렉션에 대한 일괄 처리 크기를 10으로 설정합니다. 이렇게 함으로써 한 번에 10개의 회원을 가져오므로 N+1 문제를 해결할 수 있습니다.

반응형
LIST