개발일지

게시물 페이징 조회시 n+1 본문

카테고리 없음

게시물 페이징 조회시 n+1

Johnnnn 2025. 8. 13. 16:56

1️⃣ 문제 상황: 게시판 페이징 조회 시 N+1 문제

엔티티 구조

  • Board (게시글) ↔ Member (작성자) : @ManyToOne(fetch = LAZY)
 
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false)
private Member member;
  • 게시글을 조회할 때 Board 객체만 가져오고 Member는 LAZY 로딩 설정
  • 따라서 게시글 목록을 조회하면서 board.getMember().getName() 같은 호출이 발생하면 Member를 DB에서 추가로 조회하는 쿼리가 반복적으로 실행

실제 로그 예시 (N+1 문제 발생)

 
select * from board limit 10; -- 1번 쿼리 (게시글 목록)
select * from member where member_id=?; -- 10번 쿼리 (각 게시글 작성자 조회)
select * from member where member_id=?;
...
  • 게시글 10개 조회 시, Member 조회 쿼리가 10번 반복 → N+1 문제

2️⃣ 문제 발생 원인

  • @ManyToOne(fetch = LAZY) → 기본적으로 연관 엔티티를 즉시 가져오지 않음
  • PostResponse DTO 생성 시 board.getMember().getName() 호출 → Member가 아직 로딩되지 않아 추가 쿼리 발생

즉, Board는 한 번 조회했지만 Member는 게시글 개수만큼 추가 조회되는 구조가 문제였습니다.


3️⃣ 해결 방법

(1) Fetch Join 사용

  • JPQL에서 Board와 Member를 한 번에 조회하도록 JOIN FETCH 사용
 
@Query("SELECT b FROM Board b JOIN FETCH b.member ORDER BY b.createdAt DESC") List<Board> findAllWithMember();
  • 이렇게 하면 Board와 연관된 Member 데이터를 한 번의 쿼리로 가져올 수 있음

(2) 페이징 처리

  • JPA에서는 fetch join과 Pageable을 함께 쓰면 count 쿼리가 필요
  • 따라서 데이터 조회 쿼리와 count 쿼리를 분리
@Query(value = "SELECT b FROM Board b JOIN FETCH b.member ORDER BY b.createdAt DESC",
countQuery = "SELECT count(b.boardId) FROM Board b")
List<Board> findAllWithMemberPaged(Pageable pageable);
  • 데이터 조회 쿼리: 게시글 + 작성자 한 번에 조회
  • count 쿼리: 페이지네이션용 게시글 수만 계산

(3) 서비스에서 DTO 변환 시 추가 쿼리 없음

  • 이제 board.getMember().getName() 호출해도 이미 JOIN으로 가져왔기 때문에 추가 쿼리 발생하지 않음

4️⃣ 결과

  • 기존: 게시글 10개 조회 → Member 10개 추가 조회 → 총 11번 쿼리 (N+1 문제)
  • 개선: 게시글 10개 조회 + Member JOIN → 1번 쿼리 + count 1번 → 총 2번 쿼리 ✅
  • 로그 예시:
 
select b1_0.board_id, b1_0.title, ..., m1_0.member_id, m1_0.name, ... from board b1_0 join member m1_0 on m1_0.member_id = b1_0.member_id order by b1_0.created_at desc; select count(*) from board b1_0;

즉, N+1 문제가 완전히 해결됨.

 

select
        b1_0.board_id,
        b1_0.category,
        b1_0.comment_count,
        b1_0.content,
        b1_0.created_at,
        b1_0.member_id,
        m1_0.member_id,
        m1_0.birthdate,
        m1_0.content,
        m1_0.created_at,
        m1_0.email,
        m1_0.experience_level,
        m1_0.is_deleted,
        m1_0.name,
        m1_0.password,
        m1_0.region,
        m1_0.updated_at,
        b1_0.title,
        b1_0.updated_at,
        b1_0.view_count 
    from
        board b1_0 
    join
        member m1_0 
            on m1_0.member_id=b1_0.member_id 
    order by
        b1_0.created_at desc

 
    select
        count(*) 
    from
        board b1_0