JPA를 사용하기전 Mybatis를 사용할 때는 xml로 객체를 SQL에 매핑하는데 시간을 썼지만 JPA는 자동으로 쿼리를 만들어줘서 편하게 사용하고 있었다. 하지만 편한 만큼이나 나도 모르게 1 + n 쿼리가 실행되고 있었다. 이것만 잘 컨트롤 해줘도 성능이 좋아지고 JPA이해도가 높아지는 것 같다. 이번 글에는 Fetch Join과 Fetch 전략을 사용해서 n+1을 개선할 것이다.
n+1 이란?
Car 라는 클래스와 그 차의 주인인 Master라는 클래스가 있습니다. 아시다시피 차 주인은 여러 대의 차를 가질 수 있으니 차와 주인의 관계는 N : 1 관계입니다.
@Entity
public class Master {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column
private String name;
}
@Entity
public class Car {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column
private String name;
@ManyToOne
private Master master;
}
아래 코드는 마스터한명과 차 2대를 생성하는 로직입니다.
@GetMapping("/test1")
public void createCarAndMaster(){
// Car 와 Master를 생성한다.
Master master = Master.builder().name("빈디젤").build();
Car car = Car.builder().name("스포츠카").build();
//Master를 저장
Master saveMaster= masterRepository.save(master);
//저장된 Master를 Car에 관계를 맺여준다.
car.settingMaster(saveMaster);
//Car 저장
carRepository.save(car);
}
H2 데이터베이스에 저장된 걸 확인 할 수 있습니다.
그러면 이제 우리는 저장된 Car를 보고싶기에 Car를 조회합니다. 하지만 아래와 같이 조회를 하면 쿼리가 2번 발생하고 car를 조회했지만 master도 같이 조회가 되신 걸 확인 할 수 있습니다. 이것을 쿼리가 n번 더 일어났다고 해서 n+1 이라고 합니다.
@GetMapping("/test2")
public List<Car> getCar() {
return carRepository.findAll();
}
Hibernate:
select
car0_.id as id1_1_,
car0_.master_id as master_i3_1_,
car0_.name as name2_1_
from
car car0_
Hibernate:
select
master0_.id as id1_3_0_,
master0_.name as name2_3_0_
from
master master0_
where
master0_.id=?
Response
[
{
"id": 1,
"name": "스포츠카",
"master": {
"id": 1,
"name": "빈디젤"
}
}
]
Fetch 전략이란?
fetch 전략에는 FetchType.EAGER(즉시 로딩), FetchType.LAZY(지연 로딩) 2가지 설정이 있습니다.
즉시 로딩(FetchType.EAGER)
연관 관계에 있는 Entity 들 모두 가져온다 @ManyToOne 과 @OneToOne는
기본값으로 즉시 로딩(FetchType.EAGER)로 설정되어 있다.
지연 로딩(FetchType.LAZY)
연관 관계에 있는 Entity 가져오지 않고, 필요할 때 가져온다. @OneToMany와 @ManyToMany는
기본값으로 지연 로딩(FetchType.LAZY)로 설정되어 있습니다.
사용법 (예제)
Car 클래스에 연관관계를 맺여준 변수에(fetch = FetchType.LAZY) 를 설정해주면 끝이다.
@Entity
public class Car {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column
private String name;
@ManyToOne(fetch = FetchType.LAZY)
private Master master;
}
조회를 해준다. 리턴값을 null을 주었는데 자세히는 나중에 설명을 하겠습니다. 아래 코드를 실행하면 로직이 한번만 진행합니다.
@GetMapping("/test2")
public List<Car> getCar() {
List<Car> carList= carRepository.findAll();
return null;
}
Hibernate:
select
car0_.id as id1_1_,
car0_.master_id as master_i3_1_,
car0_.name as name2_1_
from
car car0_
이번에는 Master를 사용하는 로직을 만들어 어떻게되는지 확인해보면 Master를 조회하는 쿼리가 발생합니다.
@GetMapping("/test2")
public List<Car> getCar() {
List<Car> carList= carRepository.findAll(); // Car를 조회하는 쿼리 발생
System.out.println(carList.get(0).getMaster().getName()); // Master를 조회하는 쿼리 발생
return null;
}
Hibernate:
select
car0_.id as id1_1_,
car0_.master_id as master_i3_1_,
car0_.name as name2_1_
from
car car0_
Hibernate:
select
master0_.id as id1_3_0_,
master0_.name as name2_3_0_
from
master master0_
where
master0_.id=?
Master의 데이터가 필요없다면 Car 객체만 리턴해주면 되겠지?! 라고 생각 하실 수도 있지만 Car을 return 해주면 Master가 조회됩니다. 그러므로 반드시 Car만 필요하다면 리턴해줄 때 Dto객체로 생성해 리턴해주세요
@GetMapping("/test2")
public List<Car> getCar() {
List<Car> carList= carRepository.findAll(); //Car 조회 쿼리 발생
return carList; // Master 조회 쿼리 발생
}
Hibernate:
select
car0_.id as id1_1_,
car0_.master_id as master_i3_1_,
car0_.name as name2_1_
from
car car0_
Hibernate:
select
master0_.id as id1_3_0_,
master0_.name as name2_3_0_
from
master master0_
where
master0_.id=?
@EntityGraph에 대해해서는 2부에서 설명이 이어집니다.
2022.08.04 - [데이터베이스/JPA] - [JPA] Fetch 전략과 @EntityGraph로 1+n 해결하기(2)
참고했던 자료 및 참고하면 좋은 자료
'데이터베이스 > JPA' 카테고리의 다른 글
[JPA] JPQL @Query에 각 DB function() 사용해보기 (0) | 2022.12.31 |
---|---|
[JPA] Pageable과 countQuery 사용한 Spring-Data FETCH JOIN (0) | 2022.11.24 |
[JPA] Fetch 전략과 @EntityGraph로 n+1 해결하기(2) (1) | 2022.08.04 |
[JPA] JPA로 Pageable사용해서 페이징 처리하기 (0) | 2022.06.29 |
[JPA] JPA 대해서 (0) | 2022.06.29 |