💡발단
HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory

WARN이 반복적으로 나오는 걸 보게 됐다. 이때까지만 해도 ERROR은 아니니까 적당히 나중에 리팩토링해야겠다고 생각했다.
그러나 쿼리문을 자세히 보니 큰 문제가 있다는걸 깨달았다.
실제 쿼리를 날릴 때 limit 을 걸지 않는다.
알고보니 DB에서 모든 row를 가져온 뒤에 메모리상에 올려두고 처리하는 방식이었다.
아래는 Querydsl 코드. location_bookmark는 location에 대한 일대다 엔티티이다.
location_bookmark를 레프트 조인하고 fetch join하면서 발생하는 부분이 핵심이다.
public JPAQuery<Location> selectFromSimpleLocationPrefix() {// 로케이션 페이지네이션 조회
return queryFactory.selectFrom(location)
.leftJoin(location.locationCategory, locationCategory).fetchJoin()
.leftJoin(location.locationBookmarkList, locationBookmark).fetchJoin();
}
아래가 실제 수행된 쿼리문인데, limit이 안 걸리는걸 볼 수 있다.

이것의 가장 큰 문제는 limit이 걸리지 않아 원래 찾으려던 테이블을 FULL scan하게 되는 것이다. 이는 큰 성능 이슈가 될 수 있기 때문에 바로잡아야 한다.
실사용 서비스에서 생기는 레코드의 개수는 10만개가 될 수도 있고 몇억개가 될 수도 있다.
정작 클라이언트가 요청한건 10개인데, N+1 문제를 최적화한다고 페치 조인을 했다가 DB상으로는 10만개를 찾는건 앞뒤가 맞지 않는다.
어떻게 해결하지?
사실 김영한님의 JPA 강의에서 fetch join 한계 돌파라는 이름으로 배운 적이 있다. (왜 새하얗게 잊었을까…?)
결론부터 말하면, 페이지네이션에서는 fetch join을 포기해야 한다. 즉 N+1 문제를 허용하는 것.
대신에 Batch Size를 이용하면 N+1 성능 저하를 획기적으로 줄일 수 있다. 쉽게 말하면 IN절로 연관관계 엔티티들에 대한 쿼리를 추가적으로 날리는 것이다.

yml에서 default_batch_size
를 설정하면 된다. 그러면 설정한 값만큼 IN절 안에 들어가게 된다. 나는 이 값을 100으로 설정해뒀다.
spring:
jpa:
properties:
hibernate:
default_batch_fetch_size: 100
엔티티마다 다른 batch size를 주고 싶다면 @Batch(size = )
를 쓸 수 있다.
정리 : fetch join을 해도 되는 경우는?
- 다대일 혹은 일대일 엔티티를 조인하는 경우
- 단일 row를 조회하는 경우 (id와 비교하는 등)
- limit을 걸지 않는 경우 (페이지네이션이 아닌 경우)
로케이션 1개를 상세 조회하는 API가 있었다.
여기선 limit을 걸 일이 없고 단순히 WHERE 절에서 id를 비교하여 조회하므로 , fetch join을 그대로 유지했다.
@Override
public Optional<Location> getLocation(Long locationId) {
JPAQuery<Location> query = queryFactory.selectFrom(location)
.leftJoin(location.locationCategory, locationCategory).fetchJoin()
.leftJoin(location.locationBookmarkList, locationBookmark).fetchJoin()
.where(location.id.eq(locationId));
return Optional.ofNullable(query.fetchOne());
}
'Back-end > Spring Boot' 카테고리의 다른 글
@ConfigurationProperites 이용한 프로퍼티 객체 관리 (0) | 2024.01.13 |
---|---|
[Spring Boot] logback으로 CloudWatch에 서버 요청 로깅하기 (1) | 2023.10.06 |
[Spring JPA] 엔티티 컬렉션 필드를 초기화해야 하는 이유 (0) | 2023.09.18 |
[Spring Boot] 커서 페이징(no offset)에서 Page 대신 Slice 사용하기 (1) | 2023.09.11 |
[Spring JPA] Hibernate에서 지원하지 않는 MySQL 랜덤 함수 직접 만들기 + Expressions 파헤쳐보기 (0) | 2023.09.06 |
💡발단
HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory

WARN이 반복적으로 나오는 걸 보게 됐다. 이때까지만 해도 ERROR은 아니니까 적당히 나중에 리팩토링해야겠다고 생각했다.
그러나 쿼리문을 자세히 보니 큰 문제가 있다는걸 깨달았다.
실제 쿼리를 날릴 때 limit 을 걸지 않는다.
알고보니 DB에서 모든 row를 가져온 뒤에 메모리상에 올려두고 처리하는 방식이었다.
아래는 Querydsl 코드. location_bookmark는 location에 대한 일대다 엔티티이다.
location_bookmark를 레프트 조인하고 fetch join하면서 발생하는 부분이 핵심이다.
public JPAQuery<Location> selectFromSimpleLocationPrefix() {// 로케이션 페이지네이션 조회
return queryFactory.selectFrom(location)
.leftJoin(location.locationCategory, locationCategory).fetchJoin()
.leftJoin(location.locationBookmarkList, locationBookmark).fetchJoin();
}
아래가 실제 수행된 쿼리문인데, limit이 안 걸리는걸 볼 수 있다.

이것의 가장 큰 문제는 limit이 걸리지 않아 원래 찾으려던 테이블을 FULL scan하게 되는 것이다. 이는 큰 성능 이슈가 될 수 있기 때문에 바로잡아야 한다.
실사용 서비스에서 생기는 레코드의 개수는 10만개가 될 수도 있고 몇억개가 될 수도 있다.
정작 클라이언트가 요청한건 10개인데, N+1 문제를 최적화한다고 페치 조인을 했다가 DB상으로는 10만개를 찾는건 앞뒤가 맞지 않는다.
어떻게 해결하지?
사실 김영한님의 JPA 강의에서 fetch join 한계 돌파라는 이름으로 배운 적이 있다. (왜 새하얗게 잊었을까…?)
결론부터 말하면, 페이지네이션에서는 fetch join을 포기해야 한다. 즉 N+1 문제를 허용하는 것.
대신에 Batch Size를 이용하면 N+1 성능 저하를 획기적으로 줄일 수 있다. 쉽게 말하면 IN절로 연관관계 엔티티들에 대한 쿼리를 추가적으로 날리는 것이다.

yml에서 default_batch_size
를 설정하면 된다. 그러면 설정한 값만큼 IN절 안에 들어가게 된다. 나는 이 값을 100으로 설정해뒀다.
spring:
jpa:
properties:
hibernate:
default_batch_fetch_size: 100
엔티티마다 다른 batch size를 주고 싶다면 @Batch(size = )
를 쓸 수 있다.
정리 : fetch join을 해도 되는 경우는?
- 다대일 혹은 일대일 엔티티를 조인하는 경우
- 단일 row를 조회하는 경우 (id와 비교하는 등)
- limit을 걸지 않는 경우 (페이지네이션이 아닌 경우)
로케이션 1개를 상세 조회하는 API가 있었다.
여기선 limit을 걸 일이 없고 단순히 WHERE 절에서 id를 비교하여 조회하므로 , fetch join을 그대로 유지했다.
@Override
public Optional<Location> getLocation(Long locationId) {
JPAQuery<Location> query = queryFactory.selectFrom(location)
.leftJoin(location.locationCategory, locationCategory).fetchJoin()
.leftJoin(location.locationBookmarkList, locationBookmark).fetchJoin()
.where(location.id.eq(locationId));
return Optional.ofNullable(query.fetchOne());
}
'Back-end > Spring Boot' 카테고리의 다른 글
@ConfigurationProperites 이용한 프로퍼티 객체 관리 (0) | 2024.01.13 |
---|---|
[Spring Boot] logback으로 CloudWatch에 서버 요청 로깅하기 (1) | 2023.10.06 |
[Spring JPA] 엔티티 컬렉션 필드를 초기화해야 하는 이유 (0) | 2023.09.18 |
[Spring Boot] 커서 페이징(no offset)에서 Page 대신 Slice 사용하기 (1) | 2023.09.11 |
[Spring JPA] Hibernate에서 지원하지 않는 MySQL 랜덤 함수 직접 만들기 + Expressions 파헤쳐보기 (0) | 2023.09.06 |