블로그 서비스에서 블로그가 ManyToOne
관계이고, 계층형(계단식) 쿼리 구조로 설계된 카테고리를 객체 형식으로 만들려고 하면서 겪은 이슈 해결 과정이다.
발단
List<BlogCategoryEntity> findAllByParentIsNull();
처음 구현하려고 하였을 때, JPA의 배치사이즈를 설정해 두었으니, 부모가 없는 가장 상위 행들을 불러와서 DTO로 매핑하면, 자식들을 한 번에 불러올 것으로 생각했다.
위기
findAllByParentIsNull:
Hibernate: select b1_0 from blog_category b1_0 where b1_0.parent_id is null
Hibernate: select c1_0 from blog_category c1_0 where c1_0.parent_id in (?)
Hibernate: select c1_0 from blog_category c1_0 where c1_0.parent_id in (?)
Hibernate: select c1_0 from blog_category c1_0 where c1_0.parent_id=?
findAll:
Hibernate: select b1_0 from blog_category b1_0
Hibernate: select c1_0 from blog_category c1_0 where c1_0.parent_id in (?)
생각과 다르게 쿼리가 많이나갔다. n+1
문제가 발생한 것이다. 그래서 일단 다 조회해 오기로 마음먹었다. 근데 잉? 생각해 보니 OneToMany
의 상황에 fetch=Lazy
로 설정해 두어서 한 번에 불러오지도 못 하고, 매핑하니 모든 객체의 자식이 있는지 확인하려고 이번엔 객체 개수에 비례해서 쿼리를 날리고 있었다. 내 눈에는 이미 모든 객체가 있으니 영속성 컨텍스트가 알아서 매핑해주려나 쉽게 생각했다가 봉변당하고 만 것이다.
해결
나머지는 로직에서
@Transactional(readOnly = true)
public List<BlogCategory> getBlogCategoryList() throws Exception {
List<BlogCategoryEntity> blogCategoryEntities = blogCategoryRepository.findAllByOrderByName();
List<BlogCategory> blogCategories = new ArrayList<>();
Map<UUID, BlogCategory> blogCategoryMap = new HashMap<>();
blogCategoryEntities.forEach(entity -> convertEntityToDto(entity, blogCategoryMap, blogCategories));
return blogCategories;
}
private void convertEntityToDto(BlogCategoryEntity entity, Map<UUID, BlogCategory> blogCategoryMap,
List<BlogCategory> blogCategories) {
BlogCategory blogCategory = BlogCategoryMapper.INSTANCE.toBlogCategory(entity);
blogCategoryMap.put(blogCategory.getId(), blogCategory);
if (entity.getParent() != null)
blogCategoryMap.get(entity.getParent().getId()).getChild().add(blogCategory);
else
blogCategories.add(blogCategory);
}
하는 수 없지 다 불러와서 로직으로 처리하기로 했다. HashMap
을 옆에 두고 forEach
로 한번씩만 순회해서 부모가 있으면 자식을 추가하고, 부모가 없으면 리스트에 추가하도록 구현하였다.