Skip to content

refactor: 피드 조회 성능 개선 및 N + 1 문제 해결 #368

Merged
mangsuyo merged 5 commits intomangsuyofrom
refactor/feed-query#367
Oct 22, 2025
Merged

refactor: 피드 조회 성능 개선 및 N + 1 문제 해결 #368
mangsuyo merged 5 commits intomangsuyofrom
refactor/feed-query#367

Conversation

@mangsuyo
Copy link
Copy Markdown
Member

@mangsuyo mangsuyo commented Sep 9, 2025

📌 관련 이슈

피드 조회 API 성능 개선 및 쿼리 최적화 #367

1. 개요

피드 목록 조회 API (GET /feeds)에서 발생하는 N+1 쿼리 문제를 해결하여 응답 성능을 개선했습니다.

현재 페이지 크기 (N=10) 기준으로 N+1 문제로 인해 50회의 추가 쿼리가 발생했습니다.
본 리팩토링을 통해 DTO 프로젝션, Batch Size 적용, Bulk 조회를 도입하여 N+1 관련 쿼리 수를 4회로 감소시켰습니다.

문제 해결을 위해 먼저 Feed 엔티티의 연관 관계를 분석했습니다.

피드 엔티티 연관관계 다이어그램

FeedStudent, Category, University@ManyToOne 관계를 맺고,
FeedImage, FeedLike, Bookmark 등과 @OneToMany 관계를 맺고 있었습니다.

2. 문제 원인 분석

쿼리 분석 결과, N+1 문제는 DTO 변환 과정에서 발생하는 다수의 LAZY 로딩과 반복적인 로직 호출로 인해 발생했습니다.

N+1 유발 지점

  • @ManyToOne LAZY 로딩: feed.getStudent() 접근 시 N+1 발생.
  • 연쇄 LAZY 로딩: feed.getStudent().getUniversity() 접근 시 추가 N+1 발생.
  • @OneToMany LAZY 로딩: feed.getImages() 접근 시 N+1 발생
  • 좋아요/북마크 여부 확인: feedLikeRepository.exists...bookmarkRepository.exists... 를 반복문 내에서 피드마다 호출.

3. 최적화 과정

3-1. 시도 1: Fetch Join 적용

N+1 문제를 해결하기 위한 첫 번째 시도로 가장 표준적인 방식인 Fetch Join을 선택했습니다.
@EntityGraph를 활용하여 Feed 엔티티를 조회할 때 연관된 StudentFeedImage까지 한 번에 가져오고자 했습니다.

하지만 @OneToMany 관계인 FeedImage 을 Fetch Join에 포함하자마자
페이지네이션 기능과 충돌하는 예상치 못한 문제에 부딪혔습니다.

Hibernate는 HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory!라는 경고 로그를 출력했습니다.

이는 데이터베이스 레벨에서 LIMIT 절을 통해 효율적으로 페이징하는 것이 아니라,
모든 조회 결과를 애플리케이션 메모리로 가져온 뒤 직접 페이징 처리를 한다는 의미입니다.

이러한 In-Memory 페이징 방식은 데이터가 많아질수록 심각한 성능 저하와 OOM(Out Of Memory)을 유발할 수 있으므로,
FeedImage를 가져올 때 Fetch Join 전략은 포기해야 했습니다.

3-2. 시도 2: Batch Size 적용

Fetch Join의 대안으로 @BatchSize를 적용하는 전략을 시도했습니다.
FeedImage 컬렉션에 @BatchSize를 설정하자, N개의 개별 쿼리가 1개의 IN 절을 사용하는 쿼리로 성공적으로 통합되었습니다.

하지만 Student에서는 동일한 최적화가 적용되지 않았습니다.
@BatchSize 설정에도 불구하고 여전히 Student 엔티티를 조회하기 위한 개별 쿼리가 N번 실행되는 현상이 지속되었습니다.

원인 분석 결과, Student 엔티티 내부의 다수 연관관계가 문제였습니다.

Student 엔티티는 여러 다른 엔티티와 @OneToOne 관계를 맺고 있었으며,
해당 관계들의 FetchType이 기본값인 EAGER로 설정되어 있었습니다.

Student 엔티티 연관관계 다이어그램

Hibernate가 Student 프록시를 배치로 초기화하는 과정에서 이 Eager Loading 설정들이 연쇄적으로 동작하면서 충돌이 발생했고,
결과적으로 배치 처리가 실패하여 개별 쿼리로 폴백(Fallback)되는 것으로 확인되었습니다.

3-3. 시도 3: DTO 프로젝션 적용

Student 엔티티는 @OneToOne 관계에서 연관 관계의 주인이 아니도록 설계되었습니다.

이로 인해 FetchType을 LAZY로 명시적으로 변경하더라도 Eager Loading이 강제되는 경향이 있어
엔티티 조회 자체의 복잡성이 높다고 판단했습니다.

따라서 엔티티를 직접 조회하는 대신, 필요한 데이터만 선별하여 DTO로 바로 조회하는 DTO 프로젝션 방식으로 전략을 변경했습니다.

결과적으로 이 방식을 통해 Eager Loading 연쇄 문제를 원천적으로 회피하였고,
N개의 Student 조회 쿼리를 단 1개의 DTO 프로젝션 쿼리로 대체하여 최적화를 완료했습니다.

4. N+1 쿼리 수 비교 (페이지 크기 = 10 기준)

N+1 발생 지점 Before (개별 조회) After (최적화 적용) 최적화 기법
작성자 정보 (Student + University) 20회 (10+10) 1회 DTO Projection
좋아요 상태 (Like) 10회 1회 Bulk IN Query
북마크 상태 (Bookmark) 10회 1회 Bulk IN Query
피드 이미지 (Image) 10회 1회 @batchsize
합계 50회 4회

@mangsuyo mangsuyo self-assigned this Sep 9, 2025
@mangsuyo mangsuyo added the 🔨 Refactor 코드 리팩토링 label Sep 9, 2025
@mangsuyo mangsuyo changed the title Refactor: 피드 조회 성능 개선 및 N + 1 문제 해결 refactor: 피드 조회 성능 개선 및 N + 1 문제 해결 Sep 10, 2025
@mangsuyo mangsuyo changed the base branch from dev to mangsuyo October 22, 2025 11:21
@mangsuyo mangsuyo merged commit 790827c into mangsuyo Oct 22, 2025
1 check passed
@mangsuyo mangsuyo deleted the refactor/feed-query#367 branch October 22, 2025 11:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🔨 Refactor 코드 리팩토링

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant