문제
- 일정을 조회하고 DTO를 만들 때마다 하나의 일정의 DTO를 생성할 때마다 댓글 개수가 조회가 됨
- select count(*) from comment group by schedule_id(group by는 없는 것 같음)
- 문제 코드 DTO를 만들 때 조회
Page<GetSchedulePageResponse> responsePage = schedulePage.map(s ->
new GetSchedulePageResponse(
s.getId(),
s.getUser().getId(),
s.getTitle(),
s.getContent(),
commentRepository.countBySchedule_Id(s.getId()),
s.getCreatedAt(),
s.getModifiedAt()));
- Console SQL
- 일정 3개만 만들고 조회했을 때
count가 3개 모두 일어남
Hibernate:
select
s1_0.id,
s1_0.content,
s1_0.created_at,
s1_0.modified_at,
s1_0.title,
s1_0.user_id
from
schedule s1_0
order by
s1_0.modified_at desc
limit
?, ?
Hibernate:
select
count(c1_0.id)
from
comment c1_0
join
schedule s1_0
on s1_0.id=c1_0.schedule_id
where
s1_0.id=?
Hibernate:
select
count(c1_0.id)
from
comment c1_0
join
schedule s1_0
on s1_0.id=c1_0.schedule_id
where
s1_0.id=?
Hibernate:
select
count(c1_0.id)
from
comment c1_0
join
schedule s1_0
on s1_0.id=c1_0.schedule_id
where
s1_0.id=?
- 이 것을 해결하기 위해 JPQL과 그 외의 방법을 찾아본 것으로
JPQL을 찾아봄
JPQL
- JPQL은 Entity 객체를 조회하는 객체 지향 쿼리
- 테이블을 대상으로 쿼리하는 것이 아니라 Entity 객체를 대상으로 쿼리
특징
- 테이블이 아닌 객체를 검색하는 객체지향 쿼리
- SQL을 추상화 했기 때문에 특정 데이터베이스 SQL에 종속적이지 않음
- JPA는 JPQL을 분석하여 SQL을 생성한 후 DB에서 조회
요약
| 항목 | 설명 |
|---|
| 대상 | 엔티티와 필드 |
| 문법 | SQL과 유사하지만 객체 지향적 |
| 장점 | DB 독립성, 코드 유지보수성 증가 |
| 주요 기능 | SELECT, JOIN, UPDATE, DELETE 쿼리 지원 |
| 주의사항 | 엔티티 이름 및 필드 사용, Lazy Loading 성능 문제 |
작성하는 방법
1. @Query 어노테이션 사용
@Query("select
c.schedule.id as scheduleId,
coalesce(count(c), 0) as count
from Comment c
group by c.schedule.id")
- 메서드 이름은 자유롭게 알아보기 쉽게 지정
- 특정 컬럼만 조회할 때 DTO를 활용하여 받음 (DTO 프로젝션 이라고함)
@Query
- 더 구체적인 쿼리 메서드를 작성하기 위해 사용하는 쿼리 메서드인 커스텀 버전
Entity의 JpaRepository를 상속받는 인터페이스에 @Query 어노테이션으로 정의
해결 코드
public class ScheduleCommentDTO {
private final Long scheduleId;
private final int count;
public ScheduleCommentDTO(Long id, int commentCount) {
this.id = id;
this.commentCount = commentCount;
}
}
public interface CommentRepository extends JpaRepository<Comment, Long> {
@Query("select c.schedule.id as scheduleId, coalesce(count(c), 0) as count from Comment c group by c.schedule.id")
List<ScheduleCommentDTO> countCommentByScheduleIdList();
}
찾아보니까 이 방법은 DTO Projection 이라는 방법이었다
그 외에도 EntityGraph, Fetch Join, BatchSize 방법이 있다.
DTO Projection
- 불필요한 Entity → DTO 변환 작업 없이 바로 DTO 파일로 필요한 정보를 받을 수 있음
요약
| 항목 | 설명 |
|---|
| 장점 | 필요한 데이터만 조회, 불필요한 연관 로딩 방지 |
| 단점 | 엔티티가 아님 → 변경 감지 불가, 복잡한 로직 포함 어려움 |
Fetch Join
- 연관된 엔티티를 한 번의 쿼리로 함께 조회하기 위한 JPQL 키워드
예시
@Query("SELECT p FROM Post p JOIN FETCH p.comments WHERE p.user.username = :username")
List<Post> findAllWithCommentsByUsername(@Param("username") String username);
BatchSize
- Hibernate 설정을 통해 컬렉션 또는 연관된 엔티티들을 배치로 로딩할 수 있게 해주는 기능
- LAZY 로딩이더라도 미리 설정된 개수만큼 한 번에 불러오게 해줌
- LAZY전략을 유지하면서도 IN절을 활용한 일괄 조회가 가능
설정 방법
글로벌 설정
spring:
jpa:
properties:
hibernate:
default_batch_fetch_size: 100
개별 설정
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
@BatchSize(size = 100)
private List<Post> posts = new ArrayList<>();
EntityGraph
- Spring Data JPA에서 지원하는 기능
- 연관관계를 JPQL없이 즉시 로딩하도록 지정
예시
@EntityGraph(attributePaths = {"user","comments"})
List<Post> findByUserUsername(String username);
정리
비교
| 방식 | 특징 | 페이징 가능 | JPQL 필요 | 실무 활용 |
|---|
| Fetch Join | 빠르고 직관적 | ✖ (컬렉션 제한) | O | O (조회 성능 빠름) |
| BatchSize | Lazy 유지 + IN 조건으로 개선 | ⭕ | ✖ | O (간편 설정) |
| EntityGraph | 선언적 설정 | ✖ (컬렉션 제한) | ✖ (Spring Data 지원) | △ (단순 관계에 유리) |