12.09 TIL - N + 1 해결

이서준·2025년 12월 9일

TIL

목록 보기
16/24

문제

  • 일정을 조회하고 DTO를 만들 때마다 하나의 일정의 DTO를 생성할 때마다 댓글 개수가 조회가 됨
  • select count(*) from comment group by schedule_id(group by는 없는 것 같음)
  1. 문제 코드 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()));
  1. 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")
  1. 메서드 이름은 자유롭게 알아보기 쉽게 지정
  2. 특정 컬럼만 조회할 때 DTO를 활용하여 받음 (DTO 프로젝션 이라고함)

@Query

  • 더 구체적인 쿼리 메서드를 작성하기 위해 사용하는 쿼리 메서드인 커스텀 버전
  • EntityJpaRepository를 상속받는 인터페이스에 @Query 어노테이션으로 정의

해결 코드


//DTO 생성
public class ScheduleCommentDTO {

    private final Long scheduleId;
    private final int count;

    public ScheduleCommentDTO(Long id, int commentCount) {
        this.id = id;
        this.commentCount = commentCount;
    }
}

//JPQL 생성
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빠르고 직관적✖ (컬렉션 제한)OO (조회 성능 빠름)
BatchSizeLazy 유지 + IN 조건으로 개선O (간편 설정)
EntityGraph선언적 설정✖ (컬렉션 제한)✖ (Spring Data 지원)△ (단순 관계에 유리)
profile
Allons-y

0개의 댓글