프로젝트의 전체 소스 코드는 이곳에서 확인하실 수 있습니다.
Shoe-auction 프로젝트의 비즈니스 로직 구현이 7~80프로 정도 완료된 것 같다. 새로운 feature를 구현하기 전에 성능적인 부분을 최적화하는 작업을 진행하기로 했다.
이번 포스팅에서는 JPA를 사용하면서 발생할 수 있는 불필요한 쿼리를 줄이며 성능을 최적하는 과정을 일부분 소개하려고 한다.
1. Fetch 전략을 EAGER로 설정했을 때 발생하는 문제점 해결하기
첫 번째로 해결할 문제는 User와 @OneToOne
관계를 맺고 있는 AddressBook(주소록)의 Fetch전략이 EAGER로 설정되어 있어서 발생하는 문제다.
참고로 XToOne
처럼 One으로 끝나는 연관관계는 모두 Fetch전략의 default 타입이 EAGER라는 점을 알아두자.
✏️ EAGER로 설정했을 때 문제점은 ?
아래 코드는 해당 사용자(USER)의 계좌 정보(ACCOUNT)를 수정하는 코드이다.
해당 코드를 실행하면 email로 User를 조회하는 Select 쿼리
, Account 정보를 수정하는 update 쿼리
가 발생하면서 총 2개의 쿼리가 실행될 것으로 예상된다. 아래 실행 결과를 보자.
----------------------USER 조회 select 쿼리 ---------------------------
select
user0_.user_id as user_id2_9_,
user0_1_.created_date as created_3_9_,
user0_1_.modified_date as modified4_9_,
user0_1_.email as email5_9_,
user0_1_.password as password6_9_,
user0_1_.user_level as user_lev7_9_,
user0_.account_number as account_1_8_,
user0_.bank_name as bank_nam2_8_,
user0_.depositor as deposito3_8_,
user0_.addressbook_id as addressb9_8_,
user0_.cart_id as cart_id10_8_,
user0_.nickname as nickname4_8_,
user0_.nickname_modified_date as nickname5_8_,
user0_.phone as phone6_8_,
user0_.user_status as user_sta7_8_
from
user user0_
inner join
user_base user0_1_
on user0_.user_id=user0_1_.user_id
where
user0_1_.email=?
---------------------AddressBook 조회 select 쿼리----------------------
select
addressboo0_.id as id1_1_0_
from
address_book addressboo0_
where
addressboo0_.id=?
---------------------Account 수정 update 쿼리---------------------------
update
user
set
account_number=?,
bank_name=?,
depositor=?,
addressbook_id=?,
cart_id=?,
nickname=?,
nickname_modified_date=?,
phone=?,
user_status=?
where
user_id=?
예상과 다르게 계좌 정보(Account) 업데이트와는 전혀 관련 없는 AddressBook을 조회하는 쿼리가 실행되었다.
위 예제 코드처럼 Fetch 전략을 EAGER로 설정하게 되면 USER를 조회할 때 연관관계를 맺고 있는 AddressBook까지 함께 조회하게 된다. 우리는 단순히 USER 정보만 필요한데, AddressBook까지 함께 조회하는 쿼리를 실행하는 것은 분명한 낭비이다.
🎁아래와 같이 개선해보자
fetch 전략을 LAZY로 수정하면 User가 조회되는 시점에 AdressBook을 프록시 객체로 가져와서 실제로 AddressBook이 사용되는 시점에 실행된다.
추가적으로 orphanRemoval = true 옵션은 고아 객체를 삭제해 주는 옵션인데, 해당 USER가 회원 탈퇴를 했을 경우 AddressBook도 함께 삭제되도록 설정해 주자.
이제 다시 Account 수정 메서드를 실행하면 정상적으로 email로 User를 조회하는 Select쿼리
, Account 정보를 수정하는 update 쿼리
만 발생할 것이다.
2. N+1 문제 해결하기
N+1이란 ?
@Entity
public class User {
@Id
@GeneratedValue
@Column(name = "USER_ID")
private Long id;
@OneToOne(fetch = FetchType.LAZY, orphanRemoval = true)
@JoinColumn(name = "ADDRESSBOOK_ID")
private AddressBook addressBook;
}
N+1 문제란 findAll()
메서드를 통해 USER 전체를 조회하게 되면 USER 전체를 조회하는 쿼리가 한번 나가고, USER의 수만큼 USER와 연관관계를 맺고있는 AddressBook을 조회하는 쿼리가 추가로 실행되는 것이다.
예를들어 USER가 10명이 존재한다고 가정했을 때 findAll()
메서드를 한번 실행하게 되면 전체 USER를 조회하는 쿼리 1번, AddressBook을 조회하는 쿼리 10번이 실행되는 문제를 의미한다.
✏️ N+1 문제 해결하기
위에서 Fetch 전략을 Lazy로 변경한 후 현재 Shoe-auction 프로젝트의 USER 테이블은 다음과 같다.
아래 코드는 회원의 주소록을 조회하는 메서드다.
실행되는 쿼리는 다음과 같다.
-------------------------------USER 조회 쿼리 -------------------------
select
user0_.user_id as user_id2_9_,
user0_1_.created_date as created_3_9_,
user0_1_.modified_date as modified4_9_,
user0_1_.email as email5_9_,
user0_1_.password as password6_9_,
user0_1_.user_level as user_lev7_9_,
user0_.account_number as account_1_8_,
user0_.bank_name as bank_nam2_8_,
user0_.depositor as deposito3_8_,
user0_.addressbook_id as addressb9_8_,
user0_.cart_id as cart_id10_8_,
user0_.nickname as nickname4_8_,
user0_.nickname_modified_date as nickname5_8_,
user0_.phone as phone6_8_,
user0_.user_status as user_sta7_8_
from
user user0_
inner join
user_base user0_1_
on user0_.user_id=user0_1_.user_id
where
user0_1_.email=?
----------------addressBook 조회 쿼리 : N+1 문제 해결을 통해 제거 가능-------------
select
addressboo0_.id as id1_1_0_
from
address_book addressboo0_
where
addressboo0_.id=?
---------------주소록에 저장되어있는 주소 조회 쿼리----------------------
select
addresslis0_.addressbook_id as addressb6_0_0_,
addresslis0_.id as id1_0_0_,
addresslis0_.id as id1_0_1_,
addresslis0_.address_name as address_2_0_1_,
addresslis0_.detailed_address as detailed3_0_1_,
addresslis0_.postal_code as postal_c4_0_1_,
addresslis0_.road_name_address as road_nam5_0_1_
from
address addresslis0_
where
addresslis0_.addressbook_id=?
N+1 문제는 위에서 설명한 것과 같이 USER의 개수만큼 추가로 실행된다고 했다. 위 예제 코드는 단일 유저에 대한 조회이기 때문에 1개의 추가 쿼리가 발생할 것이다.
select 쿼리 하나 더 추가된다고 당장 애플리케이션 성능에 무리를 주는 것은 아니지만, 이런 이슈들이 쌓이고 쌓이면 결국 성능적인 부분에서 문제가 발생하게 될 것이다.
해결 방법은 매우 간단하다. USER를 조회할 때 해당 USER와 연관관계를 맺고 있는 AddressBook까지 한 번에 조회해버리면 간단하게 해결할 수 있다.
🎁아래와 같이 개선해보자
Spring Data JPA에서는 @EntityGraph
으로 간단하게 N+1 문제를 해결할 수 있다.
위와 같이 수정하고 주소록을 조회하는 메서드를 실행하면 다음과 같은 쿼리가 실행된다.
-------------------------------USER 조회 쿼리 -------------------------
select
user0_.user_id as user_id2_9_,
user0_1_.created_date as created_3_9_,
user0_1_.modified_date as modified4_9_,
user0_1_.email as email5_9_,
user0_1_.password as password6_9_,
user0_1_.user_level as user_lev7_9_,
user0_.account_number as account_1_8_,
user0_.bank_name as bank_nam2_8_,
user0_.depositor as deposito3_8_,
user0_.addressbook_id as addressb9_8_,
user0_.cart_id as cart_id10_8_,
user0_.nickname as nickname4_8_,
user0_.nickname_modified_date as nickname5_8_,
user0_.phone as phone6_8_,
user0_.user_status as user_sta7_8_
from
user user0_
inner join
user_base user0_1_
on user0_.user_id=user0_1_.user_id
where
user0_1_.email=?
---------------주소록에 저장되어있는 주소 조회 쿼리----------------------
select
addresslis0_.addressbook_id as addressb6_0_0_,
addresslis0_.id as id1_0_0_,
addresslis0_.id as id1_0_1_,
addresslis0_.address_name as address_2_0_1_,
addresslis0_.detailed_address as detailed3_0_1_,
addresslis0_.postal_code as postal_c4_0_1_,
addresslis0_.road_name_address as road_nam5_0_1_
from
address addresslis0_
where
addresslis0_.addressbook_id=?
후기
간단하게 Fetch 전략을 Lazy로 수정하고, N+1 문제를 해결해서 JPA 성능 최적화를 해보았다.
처음 USER 클래스를 설계했을 때 USER와 AddressBook은 항상 함께 사용된다고 생각해서 EAGER 타입으로 지정했었다. 하지만 EAGER 타입으로 설정하게 되면 예상하지 못한 쿼리가 실행된다. 따라서 모든 Fetch 전략은 Lazy로 설정하고, N+1 쿼리가 발생하는 경우 @EntityGraph 또는 Fetch Join으로 해결하자.
'프로젝트 : shoe-auction' 카테고리의 다른 글
[CI/CD] Jenkins를 이용해 CI/CD 자동화하기 - CI편 (3) | 2021.04.26 |
---|---|
[AWS] AWS S3에 이미지 업로드하고 AWS Lambda로 이미지 리사이징 적용하기 (3) | 2021.04.15 |
[Spring] Redis 캐시 저장소와 세션 저장소 분리하기 (2) | 2021.03.28 |
[Spring] 회원가입시 필요한 인증번호 관리 (0) | 2021.03.15 |
[JPA] 카트(위시리스트) 구현시 연관관계 매핑에 대한 고찰 (0) | 2021.03.11 |