[4] 스프링 부트와 JPA 활용 (9) - API 개발 고급 2 (컬렉션 조회 최적화)
Intro
컬렉션 조회 최적화 ?
일대다(OneToMany)관계
에 있으면 필드
로 컬렉션(Collection)
을 가지게 되고 이러한 상황에서의 조회
를 단계적
으로 최적화
를 다루는 것
일대일(OneToOne)
, 다대일(ManyToOne)
연관관계
에서 데이터 조회
와 최적화
는 앞 글에서 다룸
- 순서
V1
: (엔티티 조회)
/ 엔티티 직접 반환
V2
: (엔티티 조회)
/ DTO를 사용한 반환
V3
: (엔티티 조회)
/ DTO 반환
+ fetch join
/ 페이징 X
V4
: (DTO 조회)
/ DTO 반환
V5
: (DTO 조회)
/ DTO 반환
+ N+1 문제
해결
V6
: (DTO 조회)
/ DTO 반환
+ 1방쿼리
엔티티 조회
V1
(API 로직)
- 로직
: 모든 Order들을 조회
한 후 Entity를 직접 반환
- 문제점
N+1 문제
: order를 순회
하면서 프록시 객체
를 강제로 초기화
json 무한루프 문제
: json 무한루프 문제
를 해결하기 위해 Entity
에 직접 @JsonIgnore
를 써야함
V2
(API 로직)
- 해결
DTO를 반환
하여 json 무한루프 문제
를 해결
- 문제점
N+1 문제
: new OrderDto(o)
과정에서 order를 순회
하며 내부 생성자
에서 각 요소를 강제 초기화
하기 때문에 여전히 N+1 문제
가 발생
(DTO)
- 주의
Order
에 있는 컬렉션
인 orderItems
도 DTO
를 사용해서 반환
해야 한다
--> 실무에서 많이하는 실수!
(Entity를 직접 반환하지 말자
)
V3
(API 로직)
- 해결
fetch join
을 사용해서 N+1 문제
를 해결
함
- 문제점
페이징
불가능
: 1:N 관계
에서 join
을 하면 데이터가 뻥튀기
되어서 우리가 원하는 페이징이 불가능함
(내부적으로 hibernate가 메모리에 올려서
해주려고 노력은 하지만,
메모리에 모든 데이터를 올린다는 것
은 부하가 생길 위험이 매우 크다!
--> 1:N관계
에서 페이징
을 하려면 V3.1 방법
을 사용하자)
(fetch join 코드 추가 - repository)
컬렉션 패치조인
은 distinct 필수
: 컬렉션을 가진다는 것
은 1:N 관계
라는 것이고 join
시 데이터가 뻥튀기
됨
즉, 반드시 distinct
옵션으로 중복 객체를 제거
해야 한다
컬렉션 패치조인
은 1개만 사용
: 컬렉션을 패치조인
하면 1:N으로 데이터가 증가
되는데 컬렉션을 또 패치조인
하면
1:N:M 관계
로 데이터가 많아져서
부정합하게 조회
될 수 있음
V3.1
(API 로직)
- 로직
컬렉션이 아닌 Entity
는 fetch join
을 사용해서 조회 쿼리
를 감소
시킨다
OrderDto(o)
과정에서 지연로딩
으로 인해 쿼리가 나갈 때
Batch size 옵션
에 지정한 만큼 크기를 미리 가져온다
(SQL의 IN 키워드
를 통해서 효율적으로 동작
함)
- 결과적으로
N번의 쿼리가 나가지 않고
1번의 쿼리
만 수행
된다
(Batch size에 따라 상이
)
- 해결
N+1 문제 부분 해결
: Batch size옵션
을 통해서 쿼리의 횟수
를 많이 줄일 수 있음
(1+N+N
이 1+1+1
이 된다)
Trade Off
fetch join
을 사용하는 것 보다 많은 쿼리
가 발생
--> 컬렉션 관계
에서 페이징
을 하려면 어쩔 수 없음
--> 하지만, 정규화된 데이터만 가져오기 때문
에 때에 따라서 더 효율적이기도 함
(fetch join 코드 추가 - repository)
Batch size 옵션
을 켰기 때문
에 컬렉션이 아닌 Entity 조회
시에도 SQL IN절이 사용
하지만, 이것보다는 fetch join
을 사용하는 것이 더 효율적
(컬렉션이 아닌 Entity 관계
는 join을 해도 데이터가 뻥튀기 되지 않기 때문
에 안전
함)
(Batch size 글로벌 옵션 추가 - application.yml)
Batch size를 지정
하는 방법
- 글로벌 옵션
: jpa.properties.hibernate.default_batch_fetch_size
옵션 지정
(보통 글로벌 옵션
으로 지정해서 많이 사용
함)
- 개별 옵션
: @BatchSize
어노테이션으로 개별 지정
Batch size
의 적당한 크기 ?
100 ~ 1000 사이
가 적당
(클수록 성능은 좋으나, 순간 부하
가 높아서 DB성능에 따라서 조절
해야 함)
- 왜냐하면,
SQL IN 절을 사용
할 때 DB에 따라 1000건 이상시 오류를 발생
할 수 있음
DTO 조회
V4
(API 로직)
- 로직
JPA를 DTO로 조회
하는 findOrderQueryDtos()
메서드 호출
OrderQueryDto
DTO를 통해 반환
(OrderQueryRepository - 쿼리 추가)
DTO 조회
를 위한 별도의 repository(OrderQueryRepository)
를 생성
표현 계층
에 데이터를 만들기 위한 특수한 repository
관심사를 분리
해서 확실하게 유지보수
할 수 있다
repository를 분리
할 경우에 DTO
도 controller에서 정적으로 생성하지 말고
따로 파일로 분리
해야 한다
--> 그렇지 않으면 repository
가 controller
를 참조하는 역행이 발생
- 로직
컬렉션이 아닌 Entity
와 데이터
는 findOrders()
라는 쿼리로 값을 미리 채움
결과를 순회
하며 orderId
에 해당하는 정보를 찾는 findOrderItems()
호출
(순회
하면 쿼리
가 나가기 때문에 N+1 문제 발생
)
- 해결
DTO
를 통해서 원하는 데이터
만 DB
에서 가져올 수 있음
- 문제점
순회하며 쿼리를 수행
하기 때문에 N+1 문제가 발생
(JPA를 직접 조회하기 위한 DTO)
컬렉션 값
인 orderItems
는 따로 쿼리를 호출
하기 때문에 생성자에 주입 X
V5
(API 로직)
- 로직
JPA를 DTO로 조회
하는 findAllByDto_optimization()
메서드 호출
OrderQueryDto
DTO를 통해 반환
(OrderQueryRepository - 쿼리 추가)
- 로직
컬렉션이 아닌 Entity
와 데이터
는 findOrders()
라는 쿼리
로 값을 미리 채움
결과
에서 orderId를 모두 추출
해서 쿼리의 파라미터로 전송
(SQL IN 절 사용
)
쿼리의 결과
를 orderId
에 따르는 List<OrderItemQueryDto>
로 정리하기 위해 Map<Long, List<OrderItemQueryDto>>
형태로 변환
순회
하면서 setOrderItems()
를 통해 정리한 값 지정
- 해결
N+1 문제 해결
: orderId를 모두 추출
해서 SQL IN절을 사용
하여 1번의 쿼리
로 해결
V6
(API 로직)
- 로직
JPA를 DTO로 조회
하는 findAllByDto_flat()
호출
(컬렉션
을 따로처리하지 않고
모든 데이터를 가져오게
동작)
모든 데이터
를 가진 OrderFlatDto
로 반환
(V1 ~ V5와 결과 스펙이 다르다!
이것을 맞춰주려면 application에서 값을 일일히 매칭하고 형식을 바꿔주어야 함
--> 난 귀찮으니 패스)
(OrderQueryRepository - 쿼리 추가)
컬렉션을 따로처리
하지 않고 모~두 한꺼번에 join해서 가져옴
--> 데이터 중복
이 발생할 수 밖에 없으며, 이것은 application에서 처리
해야함
(모든 데이터를 가진 DTO 추가 생성)
정리
- 엔티티 조회
V1
: 엔티티를 직접 반환
V2
: 엔티티 조회
후 DTO로 반환
V3
: fetch join으로 쿼리 수 최적화
/ 페이징 X
V3.1
컬렉션을 제외한 나머지
는 fetch join으로 조회
컬렉션
은 지연 로딩을 유지
하며, Batch size 옵션 지정
--> 페이징 가능!
- DTO 직접 조회
V4
: JPA에서 DTO로 직접 조회
V5
: 일대다 관계인 컬렉션
을 SQL IN 절
을 통해 메모리에 미리 조회
해서 최적화
(페이징 O)
V6
: 모든 테이블 join결과
를 application에서 정리
해서 API스펙에 맞춰 반환
(페이징 X)
엔티티 조회
vs DTO 직접 조회
엔티티 조회
는 JPA에서 제공
하는 fetch join
이나 batch size옵션
으로 N+1 문제 쉽게 해결
하지만, DTO 직접 조회
에서는 추가적으로 해야할 것
이 많음
--> 이것은 변경사항 발생
시 그만큼 수정해야 할 것이 많음
을 의미
--> 기본적으로
는 엔티티 조회
를 우선적으로 사용
하는 것을 권장
- 권장 순서
엔티티 조회
- 페이징 없으면 -->
V3
- 페이징 있으면 -->
V3.1
DTO 직접 조회