JPA N+1 문제와 해결 방안
N+1 문제란?
N+1 문제는 JPA에서 발생하는 대표적인 성능 이슈입니다. 연관 관계가 설정된 엔티티를 조회할 때 조회된 데이터 갯수(N)만큼 추가로 조회 쿼리가 발생하는 현상을 말합니다.
예시 상황
@Entity
public class Answer {
@ManyToOne(fetch = FetchType.LAZY)
private Question question;
@ManyToOne(fetch = FetchType.LAZY)
private User user;
}
// Repository
List<Answer> answers = answerRepository.findAll();
// 첫 번째 쿼리: SELECT * FROM answer
answers.forEach(answer -> {
String title = answer.getQuestion().getTitle(); // N번의 추가 쿼리 발생
String username = answer.getUser().getUsername(); // N번의 추가 쿼리 발생
});
위 코드는 다음과 같은 쿼리를 발생시킵니다:
- Answer 목록 조회 쿼리 1번
- Question 조회 쿼리 N번
- User 조회 쿼리 N번
해결 방안 1: Fetch Join
JPQL을 사용한 Fetch Join
@Query("SELECT a FROM Answer a " +
"JOIN FETCH a.question q " +
"JOIN FETCH a.user u")
List<Answer> findAllWithQuestionAndUser();
Spring Data JPA에서 @EntityGraph 사용
@EntityGraph(attributePaths = {"question", "user"})
List<Answer> findAll();
생성되는 SQL:
SELECT a.*, q.*, u.*
FROM answer a
LEFT JOIN question q ON q.id = a.question_id
LEFT JOIN user u ON u.id = a.user_id
해결 방안 2: QueryDSL 적용
기본 설정
// build.gradle
dependencies {
implementation 'com.querydsl:querydsl-jpa'
annotationProcessor 'com.querydsl:querydsl-apt'
}
Repository 구성
// Custom Repository 인터페이스
public interface AnswerRepositoryCustom {
Page<Answer> findAllWithQuestionAndUser(Pageable pageable);
}
// Custom Repository 구현
@RequiredArgsConstructor
public class AnswerRepositoryImpl implements AnswerRepositoryCustom {
private final JPAQueryFactory queryFactory;
@Override
public Page<Answer> findAllWithQuestionAndUser(Pageable pageable) {
List<Answer> content = queryFactory
.selectFrom(answer)
.leftJoin(answer.question).fetchJoin()
.leftJoin(answer.user).fetchJoin()
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
// count 쿼리
Long total = queryFactory
.select(answer.count())
.from(answer)
.fetchOne();
return new PageImpl<>(content, pageable, total);
}
}
// 메인 Repository
public interface AnswerRepository extends
JpaRepository<Answer, Long>,
AnswerRepositoryCustom {
}
Service에서 사용
@Service
@RequiredArgsConstructor
public class AnswerService {
private final AnswerRepository answerRepository;
public List<AnswerListResponse> getAnswerResponsePage(int page) {
Pageable pageable = PageRequest.of(page, PAGE_SIZE);
return answerRepository.findAllWithQuestionAndUser(pageable)
.map(AnswerListResponse::from)
.getContent();
}
}
장단점 비교
Fetch Join
- 장점:
- 구현이 간단
- 한 번의 쿼리로 필요한 데이터 조회
- 단점:
- 페이징 처리시 메모리 이슈 가능성
- 여러 컬렉션을 함께 조회할 수 없음
QueryDSL
- 장점:
- 타입 안전성 보장
- 동적 쿼리 생성 용이
- 복잡한 조건과 조인 처리 가능
- 단점:
- 초기 설정 필요
- 별도의 인터페이스와 구현체 작성 필요
참고사항
- 항상 즉시 로딩(EAGER)보다는 지연 로딩(LAZY)을 기본으로 설정
- 필요한 경우에만 fetch join을 사용
- 페이징이 필요한 경우 ToOne 관계만 fetch join하고, ToMany 관계는 batch size 조정 고려
이를 통해 N+1 문제를 효과적으로 해결하고 애플리케이션의 성능을 개선할 수 있습니다.
'스프링' 카테고리의 다른 글
스프링 - JPA 다대일 처리 (0) | 2024.12.22 |
---|---|
스프링 - OSIV (1) | 2024.12.22 |
스프링 - REST API (0) | 2024.12.17 |
스프링 시큐리티 - 간단한 인증절차 정리 (0) | 2024.12.16 |
스프링 - 스프링 시큐리티를 이용한 로그인 구현하기 (1) (1) | 2024.12.15 |
댓글