N+1 문제에 대해 알아봅시다.
가정합니다.
우리의 토끼는 인싸라서 10마리의 호랑이들과 친구입니다.
DB 테이블로 보면 N:1 이지만
객체에서는 N:1 양방향 관계로 설정해주었습니다.
직접 만들어봅시다.
프로젝트명: nPlusOne
빌드환경: gradle
자바버전: 11
라이브러리: spring web, spring-data-jpa(hibernate), lombok, h2
하나씩 채워봅시다.
plugins {
id 'org.springframework.boot' version '2.6.2'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
runtimeOnly 'com.h2database:h2'
}
test {
useJUnitPlatform()
}
lombok 쓰니까 Enable annotaion processing 체크까지 해줍니다.
spring:
# DB 연결
datasource:
# 설치된 H2 DB와 연결 URL
url: jdbc:h2:tcp://localhost/~/test
# 접속을 위한 드라이버
driver-class-name: org.h2.Driver
# springboot 2.4 부터는 username이 꼭 있어야합니다. 없으면 에러가 발생합니다.
username: sa
jpa:
# JPA가 수행하는 SQL을 볼 수 있다.
show-sql: true
# 객체를 보고 자동으로 테이블 생성 여부. 생성 - create, 비생성 - none
# 테스트이기 때문에 create로 설정하며
# 실제로는 none 으로 합니다. create이면 기존의 테이블을 전부 밀어버립니다.
hibernate:
ddl-auto: create
# 콘솔 확인을 위한 always
output:
ansi:
enabled: always
# 파라미터 확인을 위한 trace
logging:
level:
org.hibernate.type: trace
// Tiger.java
package com.example.nplusone;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
@Entity
@Getter
@Setter
public class Tiger {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// 의존관계의 주인 rabbit
@ManyToOne
private Rabbit rabbit;
// 연관관계 편의 메소드
// 양쪽으로 넣어주느것보다 한쪽에서 넣어준다.
// setter랑 혼동될 수 있어서 change prefix를 붙인다.
public void changeTiger(Rabbit rabbit) {
this.rabbit = rabbit;
rabbit.getTigerList().add(this);
}
}
// Rabbit.java
package com.example.nplusone;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter
@Setter
public class Rabbit {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// mappedBy를 통한 양방향 바인딩, 즉시 로딩
@OneToMany(mappedBy = "rabbit", fetch = FetchType.EAGER)
private List<Tiger> tigerList = new ArrayList<>();
}
JPA 기본 쿼리만 사용할 것입니다.
//TigerRepository.java
package com.example.nplusone;
import org.springframework.data.jpa.repository.JpaRepository;
public interface TigerRepository extends JpaRepository<Tiger, Long> {
}
//RabbitRepository.java
package com.example.nplusone;
import org.springframework.data.jpa.repository.JpaRepository;
public interface RabbitRepository extends JpaRepository<Rabbit, Long>{
}
package com.example.nplusone;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import javax.persistence.EntityManager;
import java.util.ArrayList;
import java.util.List;
@SpringBootTest
class NPlusOneApplicationTests {
private final EntityManager entityManager;
private final RabbitRepository rabbitRepository;
private final TigerRepository tigerRepository;
@Autowired
// 의존성 주입
public NPlusOneApplicationTests(RabbitRepository rabbitRepository, TigerRepository tigerRepository, EntityManager entityManager) {
this.rabbitRepository = rabbitRepository;
this.tigerRepository = tigerRepository;
this.entityManager = entityManager;
}
@Test
@Transactional//반드시 하나의 트랜잭션에 있어야합니다.
@DisplayName("1. 토끼 1마리 생성, 호랑이 10마리 생성 후 토기 1마리 매핑, 토끼 4마리 추가 생성, 토끼 모두 조회")
void create() {
// 1. 토끼 1마리 생성
// 1) 비영속 상태
Rabbit rabbit = new Rabbit();
// 2) 영속 상태
rabbitRepository.save(rabbit);
// 2. 호랑이 10마리 생성
// 1) 비영속 상태
List<Tiger> tigerList = new ArrayList<>();
for(int i=0; i<10; i++) {
Tiger tiger = new Tiger();
// 토끼 등록
tiger.changeTiger(rabbit);
tigerList.add(tiger);
}
// 2) 영속성 컨텍스트에 등록
tigerRepository.saveAll(tigerList);
System.out.println("========================");
// 3. 토끼 4마리 추가 생성
for(int i=0; i<4; i++) {
Rabbit rabbit1 = new Rabbit();
rabbitRepository.save(rabbit1);
}
// 4. 영속성 컨텍스트 모두 비움
entityManager.clear();
System.out.println("========================");
// 5. 토끼 모두 조회
rabbitRepository.findAll();
}
}
토끼 모두(5마리) 조회하는 쿼리는 수행되었는데
관련해서 호랑이 모두(10마리)도 조회하고 있다.
조회된 토끼가 5마리 이기 때문에 N번의 호랑이 조회 쿼리가 수행됩니다.
SELECT * FROM RABBIT;
SELECT * FROM TIGER WHERE RABBIT_ID = 1;
SELECT * FROM TIGER WHERE RABBIT_ID = 2;
SELECT * FROM TIGER WHERE RABBIT_ID = 3;
SELECT * FROM TIGER WHERE RABBIT_ID = 4;
SELECT * FROM TIGER WHERE RABBIT_ID = 5;
N:1 양방향 관계의 OneToMany측 엔티티를 조회할때 연관관계의 엔티티를 모두 조회하게 될때 발생됩니다.
즉시로딩(Eager), 지연로딩(Lazy)에 관계없이 모두 발생합니다.
다만, N+1 문제가 발생하는 시점이 다릅니다.
즉시로딩 - 조회 시점에 발생
지연로딩 - 연관 엔티티를 꺼내는 시점에 발생
Fetch Type을 Lazy로 변경하고 수행합니다.
package com.example.nplusone;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter
@Setter
public class Rabbit {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// mappedBy를 통한 양방향 바인딩
@OneToMany(mappedBy = "rabbit", fetch = FetchType.LAZY)
private List<Tiger> tigerList = new ArrayList<>();
}
package com.example.nplusone;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import javax.persistence.EntityManager;
import javax.transaction.Transactional;
import java.util.ArrayList;
import java.util.List;
@SpringBootTest
class NPlusOneApplicationTests {
private final EntityManager entityManager;
private final RabbitRepository rabbitRepository;
private final TigerRepository tigerRepository;
@Autowired
// 의존성 주입
public NPlusOneApplicationTests(RabbitRepository rabbitRepository, TigerRepository tigerRepository, EntityManager entityManager) {
this.rabbitRepository = rabbitRepository;
this.tigerRepository = tigerRepository;
this.entityManager = entityManager;
}
@Test
@Transactional// 반드시 하나의 트랜잭션에 있어야한다.
@DisplayName("1. 토끼 1마리 생성, 호랑이 10마리 생성 후 토기 1마리 매핑, 토끼 4마리 추가 생성, 토끼 모두 조회")
void create() {
// 1. 토끼 1마리 생성
// 1) 비영속 상태
Rabbit rabbit = new Rabbit();
// 2) 영속 상태
rabbitRepository.save(rabbit);
// 2. 호랑이 10마리 생성
// 1) 비영속 상태
List<Tiger> tigerList = new ArrayList<>();
for(int i=0; i<10; i++) {
Tiger tiger = new Tiger();
// 토끼 등록
tiger.changeTiger(rabbit);
tigerList.add(tiger);
}
// 2) 영속성 컨텍스트에 등록
tigerRepository.saveAll(tigerList);
System.out.println("========================");
// 3. 토끼 4마리 추가 생성
for(int i=0; i<4; i++) {
Rabbit rabbit1 = new Rabbit();
rabbitRepository.save(rabbit1);
}
// 4. 영속성 컨텍스트 모두 비움
entityManager.clear();
System.out.println("findAll ========================");
List<Rabbit> rabbitList = rabbitRepository.findAll();
System.out.println("rabbitList loop ========================");
for(Rabbit rabbit2: rabbitList) {
System.out.println(rabbit2.getTigerList().size());
}
}
}
fetch join은 조회하는 Entity 이외에 연관 Entity를 조회 후 영속성 컨텍스트에 등록합니다.
이미 영속성 컨텍스트에 등록되어 있기 대문에 지연 로딩 시점에서도 재조회하지 않습니다.
// 일대다 join을 수행해서 중복이 발생할 수 있습니다. 반드시 distinct를 사용합니다.
List<Rabbit> rabbitList = entityManager.createQuery("select distinct r from Rabbit r join fetch r.tigerList", Rabbit.class).getResultList();
package com.example.nplusone;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import javax.persistence.EntityManager;
import java.util.ArrayList;
import java.util.List;
@SpringBootTest
class NPlusOneApplicationTests {
private final EntityManager entityManager;
private final RabbitRepository rabbitRepository;
private final TigerRepository tigerRepository;
@Autowired
// 의존성 주입
public NPlusOneApplicationTests(RabbitRepository rabbitRepository, TigerRepository tigerRepository, EntityManager entityManager) {
this.rabbitRepository = rabbitRepository;
this.tigerRepository = tigerRepository;
this.entityManager = entityManager;
}
@Test
@DisplayName("호랑이 10마리 생성, 토끼 5마리 생성, 토끼 모두 조회")
void create() {
// 1. 호랑이 10마리 생성
// 1) 비영속 상태
List<Tiger> tigerList = new ArrayList<>();
for(int i=0; i<10; i++) {
Tiger tiger = new Tiger();
tigerList.add(tiger);
}
// 2) 영속성 컨텍스트에 등록
tigerRepository.saveAll(tigerList);
System.out.println("========================");
// 2. 토키 5마리 생성
// 1) 비영속 상태
List<Rabbit> rabbitList = new ArrayList<>();
for(int i=0; i<5; i++) {
// 1) 비영속 상태
Rabbit rabbit = new Rabbit();
rabbit.setTigerList(tigerList);
rabbitList.add(rabbit);
}
// 2) 영속성 컨텍스트에 토끼 등록
rabbitRepository.saveAll(rabbitList);
System.out.println("========================");
// 3. 영속성 컨텍스트 모두 비움
entityManager.clear();
System.out.println("========================");
// fetch join 사용
entityManager.createQuery("select r from Rabbit r join fetch r.tigerList", Rabbit.class).getResultList();
}
}
1번만 조회되었습니다.
참고로 DB 상태