@ElementCollection 관련하여 트러블 슈팅한 경험을 공유합니다. 틀리거나 지적할 부분이 있으면 환영합니다!
📌 게시물 페이징 조회할 때 조회 조건을 동적으로 변경해야 하는(개수의 변화) 요구 사항이 있었습니다. 처음에는 Spring data JPA로 해결하려고 했지만 동적쿼리가 필요함을 느꼈습니다. Spring에서 동적쿼리는 JPQL, querydsl로 해결할 수 있습니다. querydsl은 method를 이용하여 JPQL문을 쉽게 만들어주는 도구입니다. 동적쿼리를 만들어나가며 겪었던 트러블슈팅
을 공유하겠습니다!
간략하게 엔티티를 코드로 공유해드리겠습니다.
@Slf4j
@Getter
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Post extends BaseTimeEntity {
@GeneratedValue(strategy = IDENTITY)
@Id @Column(name = "post_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
@Column(nullable = false)
@Enumerated(EnumType.STRING)
private PostState state;
@Column(nullable = false)
@Enumerated(EnumType.STRING)
private Category category;
@Column(nullable = false)
@ElementCollection(fetch = FetchType.EAGER)
private List<Tech> techs;
@ElementCollection(fetch = FetchType.EAGER)
private List<String> images;
@OneToMany(mappedBy = "post")
private Collection<Comment> comment;
@NotBlank
private String title;
@NotBlank
private String body;
private Long readCount;
private Long heartCount;
}
Tech는 enum 클래스입니다.
@Getter
@AllArgsConstructor
public enum Tech {
Java("Java"),
Spring("Spring");
private String stack;
}
ERD 관계입니다. Post는 1개 이상의 Tech를 가지고, 0개 이상의 Image를 갖습니다.
JPA는 수정하는 메소드가 없어도 데이터를 수정할 수 있습니다.
Entity를 조회하여 조회된 Entity의 데이터를 변경하면 JPA가 DB에 자동으로 변경된 데이터를 반영하게 하는 기능이 Dirty Checking 즉, 변경 감지입니다.
아래 북마크는 제가 이전에 Dirty Checking에 대해 정리한 내용이니 참고해서 보시면 더욱 도움되실 겁니다.
여기서 Dirty Checking이 안되면 무슨 일이 벌어지나?
라는 질문이 생길 겁니다. 바로 수정하지 못한다는 겁니다.
저는 Post를 수정하는 기능이 필요한데, 기술 스택과 이미지를 수정할 수 없다면 사용자 입장에서는 굉장히 불편하고 게시물을 다시 등록해야 하는 수고스러운 일이 될 것입니다.
서버 입장에서는 데이터를 삭제하고 생성하는 네트워크나 어플리케이션 오버헤드가 발생할 것이고, DB의 정합성이 안맞는 문제까지 생길 수 있습니다.
@ElementCollection 필드는 기본적으로 엔티티가 아니라 고정된 value 값을 List로 특정 엔티티에 종속시켜 직관적으로 DB를 설계할 수 있다는 장점이 있습니다.
하지만 이는 동시에 단점인 것이 간단한 예시를 들겠습니다.
List<Post> tmp_posts = queryFactory.selectFrom(post)
.join(post.techs)
.where(categoryEq(category)
.and(containsTechs(stacks)))
.orderBy(post.createdDate.desc())
.fetch();
해당 예시처럼 join(post.techs)를 사용할 수 없습니다. 애초에 Tech가 Entity가 아니기 때문입니다. 물론 자동으로 @ElementCollection 필드에 대해서 querydsl이 join을 해주게 됩니다. 하지만 left join이나 fetch join 등 제가 걸어야 하는 옵션에 대해 관리할 수 없다는 것이 유지보수성
이 떨어지고 직관적이지 않다고 생각했습니다.
여기서 이제 조회를 하게 됩니다. Paging에서 count 쿼리를 할 때 각 post마다의 tech를 조회하고 있습니다. (N + 1 문제)
2023-10-03T12:59:38.280+09:00 DEBUG 17363 --- [nio-8080-exec-9] o.s.security.web.FilterChainProxy : Securing GET /api/v1/posts?page=2&category=Study
2023-10-03T12:59:38.288+09:00 DEBUG 17363 --- [nio-8080-exec-9] o.s.security.web.FilterChainProxy : Secured GET /api/v1/posts?page=2&category=Study
Hibernate:
select
p1_0.post_id,
p1_0.body,
p1_0.category,
p1_0.created_date,
p1_0.heart_count,
p1_0.modified_date,
p1_0.read_count,
p1_0.state,
p1_0.title,
p1_0.user_id
from
post p1_0
where
p1_0.category=?
2023-10-03T12:59:38.298+09:00 TRACE 17363 --- [nio-8080-exec-9] org.hibernate.orm.jdbc.bind : binding parameter [1] as [VARCHAR] - [Study]
Hibernate:
select
p1_0.post_id,
p1_0.body,
p1_0.category,
p1_0.created_date,
p1_0.heart_count,
p1_0.modified_date,
p1_0.read_count,
p1_0.state,
p1_0.title,
p1_0.user_id
from
post p1_0
where
p1_0.category=?
2023-10-03T12:59:38.302+09:00 TRACE 17363 --- [nio-8080-exec-9] org.hibernate.orm.jdbc.bind : binding parameter [1] as [VARCHAR] - [Study]
2023-10-03T12:59:38.303+09:00 INFO 17363 --- [nio-8080-exec-9] p.como.global.config.LoggerConfig : {"createdAt":"2023-10-03 12:59:38.297","item":"Post","action":"get","result":"success","uri":"/api/v1/posts","method":"getPostsByCategory"}
Hibernate:
select
t1_0.post_post_id,
t1_0.techs
from
post_techs t1_0
where
t1_0.post_post_id=?
2023-10-03T12:59:38.305+09:00 TRACE 17363 --- [nio-8080-exec-9] org.hibernate.orm.jdbc.bind : binding parameter [1] as [BIGINT] - [1]
Hibernate:
select
i1_0.post_post_id,
i1_0.images
from
post_images i1_0
where
i1_0.post_post_id=?
2023-10-03T12:59:38.305+09:00 TRACE 17363 --- [nio-8080-exec-9] org.hibernate.orm.jdbc.bind : binding parameter [1] as [BIGINT] - [1]
Hibernate:
select
t1_0.post_post_id,
t1_0.techs
from
post_techs t1_0
where
t1_0.post_post_id=?
2023-10-03T12:59:38.305+09:00 TRACE 17363 --- [nio-8080-exec-9] org.hibernate.orm.jdbc.bind : binding parameter [1] as [BIGINT] - [2]
Hibernate:
select
i1_0.post_post_id,
i1_0.images
from
post_images i1_0
where
i1_0.post_post_id=?
2023-10-03T12:59:38.305+09:00 TRACE 17363 --- [nio-8080-exec-9] org.hibernate.orm.jdbc.bind : binding parameter [1] as [BIGINT] - [2]
Hibernate:
select
t1_0.post_post_id,
t1_0.techs
from
post_techs t1_0
where
t1_0.post_post_id=?
2023-10-03T12:59:38.306+09:00 TRACE 17363 --- [nio-8080-exec-9] org.hibernate.orm.jdbc.bind : binding parameter [1] as [BIGINT] - [3]
Hibernate:
select
i1_0.post_post_id,
i1_0.images
from
post_images i1_0
where
i1_0.post_post_id=?
2023-10-03T12:59:38.306+09:00 TRACE 17363 --- [nio-8080-exec-9] org.hibernate.orm.jdbc.bind : binding parameter [1] as [BIGINT] - [3]
Hibernate:
select
t1_0.post_post_id,
t1_0.techs
from
post_techs t1_0
where
t1_0.post_post_id=?
2023-10-03T12:59:38.306+09:00 TRACE 17363 --- [nio-8080-exec-9] org.hibernate.orm.jdbc.bind : binding parameter [1] as [BIGINT] - [4]
Hibernate:
select
i1_0.post_post_id,
i1_0.images
from
post_images i1_0
where
i1_0.post_post_id=?
2023-10-03T12:59:38.306+09:00 TRACE 17363 --- [nio-8080-exec-9] org.hibernate.orm.jdbc.bind : binding parameter [1] as [BIGINT] - [4]
Hibernate:
select
t1_0.post_post_id,
t1_0.techs
from
post_techs t1_0
where
t1_0.post_post_id=?
2023-10-03T12:59:38.307+09:00 TRACE 17363 --- [nio-8080-exec-9] org.hibernate.orm.jdbc.bind : binding parameter [1] as [BIGINT] - [5]
Hibernate:
select
i1_0.post_post_id,
i1_0.images
from
post_images i1_0
where
i1_0.post_post_id=?
2023-10-03T12:59:38.307+09:00 TRACE 17363 --- [nio-8080-exec-9] org.hibernate.orm.jdbc.bind : binding parameter [1] as [BIGINT] - [5]
Hibernate:
select
t1_0.post_post_id,
t1_0.techs
from
post_techs t1_0
where
t1_0.post_post_id=?
2023-10-03T12:59:38.307+09:00 TRACE 17363 --- [nio-8080-exec-9] org.hibernate.orm.jdbc.bind : binding parameter [1] as [BIGINT] - [6]
Hibernate:
select
i1_0.post_post_id,
i1_0.images
from
post_images i1_0
where
i1_0.post_post_id=?
2023-10-03T12:59:38.308+09:00 TRACE 17363 --- [nio-8080-exec-9] org.hibernate.orm.jdbc.bind : binding parameter [1] as [BIGINT] - [6]
Hibernate:
select
t1_0.post_post_id,
t1_0.techs
from
post_techs t1_0
where
t1_0.post_post_id=?
2023-10-03T12:59:38.308+09:00 TRACE 17363 --- [nio-8080-exec-9] org.hibernate.orm.jdbc.bind : binding parameter [1] as [BIGINT] - [7]
Hibernate:
select
i1_0.post_post_id,
i1_0.images
from
post_images i1_0
where
i1_0.post_post_id=?
2023-10-03T12:59:38.308+09:00 TRACE 17363 --- [nio-8080-exec-9] org.hibernate.orm.jdbc.bind : binding parameter [1] as [BIGINT] - [7]
Hibernate:
select
t1_0.post_post_id,
t1_0.techs
from
post_techs t1_0
where
t1_0.post_post_id=?
2023-10-03T12:59:38.308+09:00 TRACE 17363 --- [nio-8080-exec-9] org.hibernate.orm.jdbc.bind : binding parameter [1] as [BIGINT] - [8]
Hibernate:
select
i1_0.post_post_id,
i1_0.images
from
post_images i1_0
where
i1_0.post_post_id=?
2023-10-03T12:59:38.309+09:00 TRACE 17363 --- [nio-8080-exec-9] org.hibernate.orm.jdbc.bind : binding parameter [1] as [BIGINT] - [8]
Hibernate:
select
t1_0.post_post_id,
t1_0.techs
from
post_techs t1_0
where
t1_0.post_post_id=?
2023-10-03T12:59:38.313+09:00 TRACE 17363 --- [nio-8080-exec-9] org.hibernate.orm.jdbc.bind : binding parameter [1] as [BIGINT] - [9]
Hibernate:
select
i1_0.post_post_id,
i1_0.images
from
post_images i1_0
where
i1_0.post_post_id=?
2023-10-03T12:59:38.313+09:00 TRACE 17363 --- [nio-8080-exec-9] org.hibernate.orm.jdbc.bind : binding parameter [1] as [BIGINT] - [9]
Hibernate:
select
t1_0.post_post_id,
t1_0.techs
from
post_techs t1_0
where
t1_0.post_post_id=?
2023-10-03T12:59:38.313+09:00 TRACE 17363 --- [nio-8080-exec-9] org.hibernate.orm.jdbc.bind : binding parameter [1] as [BIGINT] - [10]
Hibernate:
select
i1_0.post_post_id,
i1_0.images
from
post_images i1_0
where
i1_0.post_post_id=?
2023-10-03T12:59:38.313+09:00 TRACE 17363 --- [nio-8080-exec-9] org.hibernate.orm.jdbc.bind : binding parameter [1] as [BIGINT] - [10]
Hibernate:
select
t1_0.post_post_id,
t1_0.techs
from
post_techs t1_0
where
t1_0.post_post_id=?
2023-10-03T12:59:38.314+09:00 TRACE 17363 --- [nio-8080-exec-9] org.hibernate.orm.jdbc.bind : binding parameter [1] as [BIGINT] - [11]
Hibernate:
select
i1_0.post_post_id,
i1_0.images
from
post_images i1_0
where
i1_0.post_post_id=?
2023-10-03T12:59:38.314+09:00 TRACE 17363 --- [nio-8080-exec-9] org.hibernate.orm.jdbc.bind : binding parameter [1] as [BIGINT] - [11]
Hibernate:
select
t1_0.post_post_id,
t1_0.techs
from
post_techs t1_0
where
t1_0.post_post_id=?
2023-10-03T12:59:38.314+09:00 TRACE 17363 --- [nio-8080-exec-9] org.hibernate.orm.jdbc.bind : binding parameter [1] as [BIGINT] - [12]
Hibernate:
select
i1_0.post_post_id,
i1_0.images
from
post_images i1_0
where
i1_0.post_post_id=?
2023-10-03T12:59:38.315+09:00 TRACE 17363 --- [nio-8080-exec-9] org.hibernate.orm.jdbc.bind : binding parameter [1] as [BIGINT] - [12]
Hibernate:
select
t1_0.post_post_id,
t1_0.techs
from
post_techs t1_0
where
t1_0.post_post_id=?
2023-10-03T12:59:38.315+09:00 TRACE 17363 --- [nio-8080-exec-9] org.hibernate.orm.jdbc.bind : binding parameter [1] as [BIGINT] - [13]
Hibernate:
select
i1_0.post_post_id,
i1_0.images
from
post_images i1_0
where
i1_0.post_post_id=?
2023-10-03T12:59:38.315+09:00 TRACE 17363 --- [nio-8080-exec-9] org.hibernate.orm.jdbc.bind : binding parameter [1] as [BIGINT] - [13]
Hibernate:
select
t1_0.post_post_id,
t1_0.techs
from
post_techs t1_0
where
t1_0.post_post_id=?
2023-10-03T12:59:38.315+09:00 TRACE 17363 --- [nio-8080-exec-9] org.hibernate.orm.jdbc.bind : binding parameter [1] as [BIGINT] - [14]
Hibernate:
select
i1_0.post_post_id,
i1_0.images
from
post_images i1_0
where
i1_0.post_post_id=?
2023-10-03T12:59:38.316+09:00 TRACE 17363 --- [nio-8080-exec-9] org.hibernate.orm.jdbc.bind : binding parameter [1] as [BIGINT] - [14]
Hibernate:
select
t1_0.post_post_id,
t1_0.techs
from
post_techs t1_0
where
t1_0.post_post_id=?
2023-10-03T12:59:38.316+09:00 TRACE 17363 --- [nio-8080-exec-9] org.hibernate.orm.jdbc.bind : binding parameter [1] as [BIGINT] - [15]
Hibernate:
select
i1_0.post_post_id,
i1_0.images
from
post_images i1_0
where
i1_0.post_post_id=?
2023-10-03T12:59:38.316+09:00 TRACE 17363 --- [nio-8080-exec-9] org.hibernate.orm.jdbc.bind : binding parameter [1] as [BIGINT] - [15]
Hibernate:
select
t1_0.post_post_id,
t1_0.techs
from
post_techs t1_0
where
t1_0.post_post_id=?
2023-10-03T12:59:38.317+09:00 TRACE 17363 --- [nio-8080-exec-9] org.hibernate.orm.jdbc.bind : binding parameter [1] as [BIGINT] - [16]
Hibernate:
select
i1_0.post_post_id,
i1_0.images
from
post_images i1_0
where
i1_0.post_post_id=?
2023-10-03T12:59:38.317+09:00 TRACE 17363 --- [nio-8080-exec-9] org.hibernate.orm.jdbc.bind : binding parameter [1] as [BIGINT] - [16]
Hibernate:
select
t1_0.post_post_id,
t1_0.techs
from
post_techs t1_0
where
t1_0.post_post_id=?
2023-10-03T12:59:38.317+09:00 TRACE 17363 --- [nio-8080-exec-9] org.hibernate.orm.jdbc.bind : binding parameter [1] as [BIGINT] - [17]
Hibernate:
select
i1_0.post_post_id,
i1_0.images
from
post_images i1_0
where
i1_0.post_post_id=?
2023-10-03T12:59:38.317+09:00 TRACE 17363 --- [nio-8080-exec-9] org.hibernate.orm.jdbc.bind : binding parameter [1] as [BIGINT] - [17]
Hibernate:
select
t1_0.post_post_id,
t1_0.techs
from
post_techs t1_0
where
t1_0.post_post_id=?
2023-10-03T12:59:38.318+09:00 TRACE 17363 --- [nio-8080-exec-9] org.hibernate.orm.jdbc.bind : binding parameter [1] as [BIGINT] - [18]
Hibernate:
select
i1_0.post_post_id,
i1_0.images
from
post_images i1_0
where
i1_0.post_post_id=?
2023-10-03T12:59:38.318+09:00 TRACE 17363 --- [nio-8080-exec-9] org.hibernate.orm.jdbc.bind : binding parameter [1] as [BIGINT] - [18]
Hibernate:
select
t1_0.post_post_id,
t1_0.techs
from
post_techs t1_0
where
t1_0.post_post_id=?
2023-10-03T12:59:38.318+09:00 TRACE 17363 --- [nio-8080-exec-9] org.hibernate.orm.jdbc.bind : binding parameter [1] as [BIGINT] - [19]
Hibernate:
select
i1_0.post_post_id,
i1_0.images
from
post_images i1_0
where
i1_0.post_post_id=?
2023-10-03T12:59:38.319+09:00 TRACE 17363 --- [nio-8080-exec-9] org.hibernate.orm.jdbc.bind : binding parameter [1] as [BIGINT] - [19]
Hibernate:
select
t1_0.post_post_id,
t1_0.techs
from
post_techs t1_0
where
t1_0.post_post_id=?
2023-10-03T12:59:38.319+09:00 TRACE 17363 --- [nio-8080-exec-9] org.hibernate.orm.jdbc.bind : binding parameter [1] as [BIGINT] - [20]
Hibernate:
select
i1_0.post_post_id,
i1_0.images
from
post_images i1_0
where
i1_0.post_post_id=?
2023-10-03T12:59:38.319+09:00 TRACE 17363 --- [nio-8080-exec-9] org.hibernate.orm.jdbc.bind : binding parameter [1] as [BIGINT] - [20]
Hibernate:
select
t1_0.post_post_id,
t1_0.techs
from
post_techs t1_0
where
t1_0.post_post_id=?
2023-10-03T12:59:38.319+09:00 TRACE 17363 --- [nio-8080-exec-9] org.hibernate.orm.jdbc.bind : binding parameter [1] as [BIGINT] - [21]
Hibernate:
select
i1_0.post_post_id,
i1_0.images
from
post_images i1_0
where
i1_0.post_post_id=?
2023-10-03T12:59:38.320+09:00 TRACE 17363 --- [nio-8080-exec-9] org.hibernate.orm.jdbc.bind : binding parameter [1] as [BIGINT] - [21]
해당 문제가 발생하는 이유가 fetch join을 하지 않아서인데 @ElementCollection을 사용하게 되면 아까 말했듯이 join을 컨트롤할 수 없기에 저는 @ElementCollection으로 이루어져 있는 필드들을 모두 Entity로 변경하기로 결정하였습니다.
@Getter
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Tech {
@GeneratedValue(strategy = IDENTITY)
@Id @Column(name = "tech_id")
private Long id;
@JsonIgnore
@ManyToOne(fetch = FetchType.LAZY)
private Post post;
private String stack;
}
@Getter
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Image {
@GeneratedValue(strategy = IDENTITY)
@Id @Column(name = "image_id")
private Long id;
@JsonIgnore
@ManyToOne(fetch = FetchType.LAZY)
private Post post;
private String url;
}
이렇게 분리한 후에 join을 할 수 있게 되었고 엔티티 수정과 관련한 문제를 해결할 수 있었습니다.
이렇게 분리하면서 @JsonIgnore라는 어노테이션을 볼 수가 있습니다.
@JsonIgnore는 JPA 양방향 참조 문제를 해결하기 위해 사용하였습니다.
📌 여기서 막간으로 양방향 참조 문제란?
여기의 예시로 Post-Tech가 있습니다. 각각 1:N, N:1의 연관관계를 가지고 있고 Lazy Loading을 사용하고 있습니다. 만약 Post를 조회할 때는 Tech가 필요하기에 Tech를 조회하여 Json의 값에 넣어주려고 할 것입니다. 하지만 Tech는 또 Post를 참조하기 때문에 다시 순환하게 됩니다.
이렇게 순환적으로 참조하여 똑같은 데이터가 계~~속 쌓이는 문제가 발생합니다.
@JsonIgnore를 통하여 json 데이터에서 해당 프로퍼티를 null로 하게 되어(데이터를 포함시키지 않는다) 양방향 조회를 멈출 수 있게 됩니다! 정확히는 직렬화 대상에서 제외시키게 됩니다.
해당 문제에 대한 솔루션은 아래 참조 링크로 달아두었으니 참고하시길 바랍니다!
Hibernate:
select
p1_0.post_id,
p1_0.body,
p1_0.category,
p1_0.created_date,
p1_0.heart_count,
p1_0.modified_date,
p1_0.read_count,
p1_0.state,
t1_0.post_post_id,
t1_0.tech_id,
t1_0.stack,
p1_0.title,
p1_0.user_id
from
post p1_0
join
tech t1_0
on p1_0.post_id=t1_0.post_post_id
where
p1_0.category=?
and p1_0.post_id in(select
t2_0.post_post_id
from
tech t2_0
where
t2_0.stack=?)
order by
p1_0.created_date desc
Hibernate:
select
count(p1_0.post_id)
from
post p1_0
where
p1_0.category=?
and p1_0.post_id in(select
t1_0.post_post_id
from
tech t1_0
where
t1_0.stack=?) limit ?,?
쿼리 개수를 2개로 줄일 수 있게 되었습니다. (PageableExecutionUtils를 사용하여 count쿼리가 줄어들수도 있음!)
조회 관련하여 기술적인 부분은 이정도로 마치고 다음으로는 querydsl을 사용하며 겪었던 문제들을 기술해보겠습니다!
List<Post> tmp_posts = queryFactory.selectFrom(post)
.join(post.techs)
.where(categoryEq(category)
.and(containsTechs(stacks)))
.orderBy(post.createdDate.desc())
.fetch();
List<Post> posts = tmp_posts.subList((int)pageable.getOffset(), Math.min((int)pageable.getOffset() + pageable.getPageSize(), tmp_posts.size()));
List<PostPagingResponseDto> content = posts.stream().map(post -> {
PostPagingResponseDto dto = new PostPagingResponseDto();
dto.setId(post.getId());
dto.setTitle(post.getTitle());
dto.setCategory(post.getCategory());
dto.setState(post.getState());
dto.setTechs(post.getTechs().stream().map(Tech::getStack).collect(Collectors.toList()));
dto.setHeartCount(post.getHeartCount());
return dto;
}).toList();
여기서 querydsl을 해보신 분은 아시겠지만 Projection (bean, constructor, fields)를 사용하여 바로 dto를 뽑아낼 수 있습니다.
하지만 이를 사용하면 List techs, List images (@ElementCollection 필드들) 의 타입을 인지하지 못하여 다음과 같은 Exception을 발생시킵니다.
java.lang.IllegalArgumentException: argument type mismatch
1번은 비효율적이라고 생각했고 2번은 잘 이해가 안갔습니다. 제가 한 방식으로 하게 되면 1번보다는 쿼리가 하나 줄게 되지만 이를 stream을 통해 DTO로 변환하는 작업이 필요하게 됩니다. 하지만 이렇게 하는게 시간적으로 적게 걸릴거 같아서 해당 방식으로 처리하게 되었습니다!
HHH000104: firstResult/maxResults specified with collection fetch; applying in memory
저 같은 경우에는 Post에서 Tech와 Image의 관계들이 모두 1:N이기에 fetch join과 pagination을 사용해야 하는 상황이었습니다.
이 에러 메시지는 쿼리 결과를 모두 메모리에 적재 후에 Pagination 작업을 어플리케이션 레벨에서 수행한다는 아주 위험한 메시지입니다.
쿼리 메소드에 limit()을 걸었는데 왜 안됐을까라는 질문이 생겼는데 querydsl 내부적으로 fetch join을 사용하게 되면 selection 객체가 limit 정보를 null로 가진다고 합니다.
그래서 이를 해결하기 위해 hibernate.default_batch_fetch_size
를 지정하여 연관된 엔티티를 size만큼 한번에 IN 쿼리로 조회할 수 있게 됩니다.
이 방법 말고도 아래 블로그 참조 하시면 더 좋은 인사이트를 얻으실 수 있을 겁니다!
1. 페이징 성능 개선하기 - No Offset 사용하기
[JPA] 값타입 컬렉션 ( @ElementCollection, @CollectionTable )