해당 포스트는 Meduim 의 아티클을 번역한 내용이다.
개요
이전 프로젝트에서 다음과 같은 이유로 Spring Data JPA 를 이용하여 data access layer 를 구현했습니다.
- 즉시 사용가능한 CRUD 작업 제공
- 손쉬운 사용자 정의 쿼리 생성
- 페이징과 정렬 지원
- data access layer에 대한 쉬운 단위테스트 작성
클라이언트는 관계형 데이터베이스를 사용하고 있다고 말했으며, 어플리케이션 도메인은 집계 루트를 통해서만 액세스되도록 Bounded Context 개념을 구현하고자 했습니다. 이를 위해 JPA 연관 관계인 @OneToMany, @ManyToOne 등이 사용되었습니다. 모든 것은 클라이언트가 각 관련 자식 테이블 행 수를 표시해야 하는 기능을 요청하기 전까지 문제없이 작동했습니다. (N+1 쿼리 문제)
제가 겪은 과정은 다음과 같습니다. :
Join Fetch 와 Entity Graph
Join Fetch는 연관된 엔티티를 하나의 쿼리에서 즉시 로드하는데 사용할 수 있습니다.
public interface TeamRepository extends JpaRepository<Team, Long> {
@Query("""
select distinct t
from Team t
left join fetch t.members
left join fetch t.milestones
""")
List<Team> findAllUsingJoinFetch();
마찬가지로, JPA Entity Graph를 사용하면 런타임 중에 연관된 엔티티의 fetch 유형을 동적으로 변경하여 실행 시 성능을 향상시킬 수 있습니다.
public interface TeamRepository extends JpaRepository<Team, Long> {
@EntityGraph(attributePaths = {
"members", "milestones"
})
List<Team> findAll();
}
문제점
이 두가지 접근 방식은 이전보다 훨씬 나은 성능을 제공해야 합니다. 그러나 여기에는 몇가지 어려움이 있습니다.
우선, Team 엔티티를 살펴보겠습니다. Team 엔티티에는 두 개의 자식 엔티티가 List 로 매핑되어 있습니다.
@Entity
@Data
@NoArgsConstructor
@Table(name = "teams")
public class Team {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "team", cascade = {CascadeType.PERSIST, CascadeType.MERGE}, orphanRemoval = true)
private List<Member> members;
@OneToMany(mappedBy = "team", cascade = {CascadeType.PERSIST, CascadeType.MERGE}, orphanRemoval = true)
private List<Milestone> milestones;
// constructors, helper functions, etc ..
}
여러 하위 항목에 대해 동시에 join fetch 또는 entity graph 를 사용하면 다음과 같은 오류가 발생합니다. :
org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags: [io.jay.service.repository.Team.members, io.jay.service.repository.Team.milestones]
List 대신에 Set을 사용한다면 이 문제를 해결할 수 있습니다. List 를 사용하고자 한다면 각 자식을 즉시 로드하는 두 개의 쿼리로 분리하고 어플리케이션 코드에서 병합해야 합니다. 따라서 여러 자식 엔티티를 로드해야 하는 경우 코드 일부를 변경해야 할 수 있으며 이 결정은 기능 수용 기준에 따라 달라질 것입니다.
더 큰 문제는 두가지 솔루션 모두 페이지네이션에 어려움을 겪는다는 것입니다. 문제를 설명하기 위해 두 자식 엔티티를 List 대신 Set으로 변경해 보겠습니다.
public interface TeamRepository extends JpaRepository<Team, Long> {
@EntityGraph(attributePaths = {
"members", "milestones"
})
Page<Team> findAll(Pageable pageable);
}
이 메소드를 호출하면 페이지네이션 응답이 성공적으로 반환됩니다.
{
"content": [
{
"id": 1,
"name": "First Team",
"memberCount": 4,
"milestoneCount": 0
},
{
"id": 2,
"name": "Second Team",
"memberCount": 3,
"milestoneCount": 0
}
],
"pageable": {
"pageNumber": 0,
"pageSize": 20,
"sort": {
"empty": true,
"unsorted": true,
"sorted": false
},
"offset": 0,
"paged": true,
"unpaged": false
},
"totalElements": 2,
"totalPages": 1,
"last": true,
"size": 20,
"number": 0,
"sort": {
"empty": true,
"unsorted": true,
"sorted": false
},
"numberOfElements": 2,
"first": true,
"empty": false
}
응답을 살펴보면, entity graph 를 사용하고 쿼리에 Pageable을 전달함으로써 페이지네이션이 자동으로 처리된다고 생각할 수 있습니다. 그러나 이것은 메모리에서 페이지네이션이 수행되고 있음을 나타내는 경고 메시지가 표시됩니다.
WARN 88609 --- [nio-8080-exec-3] org.hibernate.orm.query : HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory
두 가지 접근 방식 모두 N+1 문제를 효과적으로 해결하지만, 데이터를 모두 로드한 ㄷ음 페이지네이션을 데이터베이스가 아닌 어플리케이션 내에서 관리해야 하므로 페이지네이션을 사용할때 문제가 됩니다.
Batch Processing
하이버네이트는 batch 작업을 위해 fetch_size 와 batch_size 속성을 제공하며, 이를 모든 엔티티 또는 특정 엔티티에 구성할 수 있습니다. fetch size 속성을 사용하면 select 구문에서 검색된 데이터의 수를 조정할 수 있습니다. (데이터베이스 드라이버에서 지원하는 경우)
Batch processing 은 데이터베이스의 처리량을 줄일 수 있습니다. 이는 코드 변경을 필요로하지 않기때문에 가장 간단한 해결책으로 보일 수 있습니다. 하지만 운영환경 고려없이 최적의 batch size 를 결정하기는 어렵습니다.
QueryDSL
팀에서는 프로덕션 환경에서 구성이나 인프라를 조정하기 전에 어플리케이션 코드에서 성능을 최대한 향상시키기로 결정했습니다. QueryDSL 은 몇가지 이점을 제공했습니다.
- 복잡한 쿼리의 단순화 : 서브 쿼리 및 동적쿼리를 사용하여 더 복잡한 쿼리를 쉽게 작성할 수 있습니다.
- Type Safety
- 페이징 지원
- 사용자정의 프로젝션 : 쿼리 결과를 도메인 모델이나 DTO에 쉽게 매핑할 수 있는 사용자 정의 프로젝션을 제공하여 모든 불필요한 필드를 매핑하는 것보다 빠른 수행을 할 수 있습니다.
다음 글에서는 QueryDSL 을 적용하는 과정에 대하여 자세히 살펴볼 것입니다.
'Medium' 카테고리의 다른 글
| 7가지 아키텍처 디자인 패턴 - 면접전에 알았으면 좋았을 것들 (1) | 2023.11.06 |
|---|---|
| Spring Boot3 with QueryDSL - Part2 (0) | 2023.11.06 |
| Retry 와 Fallback 메카니즘을 활용한 스프링 마이크로서비스 회복탄력성 (0) | 2023.10.26 |
| 소프트웨어 엔지니어가 알아야 할 12가지 소프트웨어 아키텍처 스타일 (0) | 2023.10.23 |
| 스프링 마이크로서비스와 사이드카 패턴 (0) | 2023.10.19 |