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 문제가 생기게 돼면 성능에 부정적인 영향을 미치게 됩니다.
- 성능 저하: 각각의 추가 쿼리가 데이터베이스에 대한 추가적인 요청을 생성하므로 전체적인 응답 시간이 증가합니다. 특히 데이터가 많은 경우에는 이러한 추가 쿼리들이 복잡해질 수 있고, 데이터베이스 서버에 부하를 줄 수 있습니다.
- 네트워크 부하: 추가 쿼리들은 데이터베이스와 애플리케이션 간의 네트워크 트래픽을 증가시킵니다. 이는 더 많은 데이터 전송으로 이어져서 대역폭 소모를 증가시킬 수 있습니다.
- 메모리 소비: 추가 쿼리로 인해 애플리케이션 메모리에 불필요한 데이터가 쌓일 수 있습니다. 이는 메모리 사용량을 증가시키고, GC(Garbage Collection) 작업을 더 자주 수행하게 할 수 있습니다.
- 데이터베이스 부하: 추가 쿼리들은 데이터베이스에 더 많은 부하를 주게 됩니다. 특히 대량의 데이터를 다룰 때는 데이터베이스 서버의 부하가 증가하고, 이로 인해 다른 쿼리들의 응답 시간이 느려질 수 있습니다.
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 문제를 해결할 수 있습니다.
'개인 공부 > JPA' 카테고리의 다른 글
Fetch Join시 생기는 문제 (1) | 2024.04.28 |
---|---|
05 연관관계 매핑 시작 실전예제 (0) | 2023.10.04 |
05 연관관계 매핑 기초 (0) | 2023.10.04 |
04 엔티티 매핑 (0) | 2023.09.20 |
03 영속성 관리 (0) | 2023.09.19 |