
컬렉션 필드가 많고 여러 연관 데이터(Count, 특정 Role 등)를 조합해야 하는 경우,
✨ Querydsl에서 DTO 직접 조회(Projection)와 메모리 맵핑(Map Binding) 전략을 사용하는 것이 성능과 코드 가독성 측면에서 가장 효율적입니다.
batch_size에만 의존하면 10만 건 이상의 데이터 처리 시 불필요한 엔티티 로딩으로 메모리 부하가 커질 수 있습니다. 다음과 같은 고도화 전략을 추천합니다.
엔티티 전체를 들고 오지 말고, 화면에 필요한 계산된 값(Count 등)을 포함한 전용 DTO를 만듭니다.
@Getter
@NoArgsConstructor
public class ProjectResponseDto {
private Long projectId;
private String projectName;
private long workerCount; // 계산된 값
private List<String> labelNames; // 리스트
private String myRole; // 현재 사용자의 역할
@QueryProjection
public ProjectResponseDto(Long projectId, String projectName, long workerCount, String myRole) {
this.projectId = projectId;
this.projectName = projectName;
this.workerCount = workerCount;
this.myRole = myRole;
}
public void setLabelNames(List<String> labelNames) {
this.labelNames = labelNames;
}
}
workerCount 같은 수치는 JPAExpressions를 사용한 서브쿼리나 GroupBy로 처리합니다.
public List<ProjectResponseDto> findProjectDetailList(Long currentUserId) {
return queryFactory
.select(new QProjectResponseDto(
project.id,
project.name,
ExpressionUtils.as(
JPAExpressions.select(userProject.count())
.from(userProject)
.where(userProject.project.eq(project)
.and(userProject.role.eq(UserRole.WORKER))), "workerCount"),
userProject.role.stringValue() // 현재 유저의 역할
))
.from(project)
.leftJoin(userProject).on(userProject.project.eq(project).and(userProject.user.id.eq(currentUserId)))
.fetch();
}
labelInfo나 tasks 처럼 리스트로 가져와야 하는 필드들은 쿼리를 딱 두 번으로 분리하여 메모리에서 합치는 것이 가장 빠릅니다. (N+1 완전 소멸)
public List<ProjectResponseDto> getProjectsWithDetails(Long userId) {
// 1. 기본 정보 먼저 조회 (1번 쿼리)
List<ProjectResponseDto> content = findProjectDetailList(userId);
List<Long> projectIds = content.stream().map(ProjectResponseDto::getProjectId).toList();
// 2. 관련 라벨 정보 한꺼번에 조회 (2번 쿼리)
Map<Long, List<String>> labelMap = queryFactory
.from(labelInfo)
.where(labelInfo.project.id.in(projectIds))
.transform(GroupBy.groupBy(labelInfo.project.id).as(GroupBy.list(labelInfo.name)));
// 3. 메모리에서 셋팅 (매우 빠름)
content.forEach(dto -> dto.setLabelNames(labelMap.getOrDefault(dto.getProjectId(), Collections.emptyList())));
return content;
}
SELECT * 대신 필요한 컬럼만 가져오므로 DB와 애플리케이션 사이의 트래픽이 줄어듭니다.List<Entity> 필드 때문에 발생하는 복잡한 로직을 DTO 한 곳에서 관리할 수 있습니다.Project 테이블에 worker_count 컬럼을 따로 두고 유저가 추가될 때마다 업데이트(Denormalization)하는 방식도 고려해 보세요.userProjectRole) 판별 로직은 Querydsl의 CaseBuilder를 사용하면 SQL 레벨에서 깔끔하게 처리됩니다.현재 프로젝트에서 가장 컬렉션이 많이 꼬여있는 엔티티가 무엇인가요? 구체적인 관계를 알려주시면 실제 맵핑 코드를 더 상세히 짜드릴 수 있습니다.