JPA N+1 문제 해결하기: 단계별 가이드
1. N+1 문제가 뭘까요?
N+1 문제를 이해하기 위해 먼저 실제 상황을 살펴보겠습니다.

예를 들어 병원 근무표 신청 시스템이 있다고 할게요:
- 근무자가 근무 신청을 합니다 (Request)
- 각 근무자는 특정 병동에 소속되어 있죠 (WardMember)
- 근무자의 정보도 있습니다 (Member)
@Transactional
public List<WardRequestResponseDto> readWardRequest(Member member) {
Ward myWard = member.getWardMember().getWard();
return requestRepository.findByWardMember_Ward(myWard)
.stream()
.map(WardRequestResponseDto::of)
.toList();
}
이 코드가 실행되면 어떤 일이 일어날까요?
우선 myWard 를 조회하는 과정을 살펴보면 다음과 같습니다.
member(1) : wardMemeber(1)
wardMember(N): ward(1)
ward 를 지목하는 과정에서 항상 단일 객체를 지목하므로 문제가 없습니다.
2. 문제 상황 이해하기 👀
DTO를 변환하는 과정을 보면:
public static WardRequestResponseDto of(Request request) {
return WardRequestResponseDto.builder()
.memberId(request.getWardMember().getMember().getMemberId())
.name(request.getWardMember().getMember().getName())
.date(request.getRequestDate())
.shift(String.valueOf(request.getRequestShift()))
.memo(request.getMemo())
.status(request.getStatus().getValue())
.build();
}
실제로 실행되는 쿼리를 보면:
- 첫 번째 쿼리: 모든 Request 조회
SELECT * FROM request WHERE ward_member_id IN ( SELECT ward_member_id FROM ward_member WHERE ward_id = ? )
- Request가 5개라면?
- 각 Request마다 WardMember 조회: 5번
- 각 WardMember마다 Member 조회: 5번
- 총 11번의 쿼리가 실행됨! (1 + 5 + 5)
이것이 바로 N+1 문제입니다.
실제 쿼리를 보면 다음과 같아요 .
SELECT r.*
FROM request r
INNER JOIN ward_member wm ON r.ward_member_id = wm.ward_member_id
WHERE wm.ward_id = ?
-- 1번째 Request의 WardMember 조회
SELECT wm.*
FROM ward_member wm
WHERE wm.ward_member_id = ?
-- 2번째 Request의 WardMember 조회
SELECT wm.*
FROM ward_member wm
WHERE wm.ward_member_id = ?
-- 5번 반복
-- 1번째 WardMember의 Member 조회
SELECT m.*
FROM member m
WHERE m.member_id = ?
-- 2번째 WardMember의 Member 조회
SELECT m.*
FROM member m
WHERE m.member_id = ?
---5번 반복
3. 해결 방법: @EntityGraph 사용하기
JPA에서는 이 문제를 해결하기 위한 좋은 도구를 제공합니다.
바로 @EntityGraph 또는 fetch join입니다!
이 포스트에선 fetch join 을 다뤄볼게요.
@Query("SELECT DISTINCT r FROM Request r " +
"LEFT JOIN FETCH r.wardMember wm " +
"LEFT JOIN FETCH wm.member m " +
"WHERE wm.ward = :ward")
List<Request> findByWardMember_Ward(@Param("ward") Ward ward);
left join 을 사용한 이유는 request는 유효한 상황에서 해당 member 가 병동을 탈퇴할 수 있기 때문입니다.
이렇게 하면:
- Request, WardMember, Member를 한 번의 쿼리로 가져옵니다
- 실행되는 SQL은:
SELECT r.*, wm.*, m.* FROM request r LEFT OUTER JOIN ward_member wm ON r.ward_member_id = wm.ward_member_id LEFT OUTER JOIN member m ON wm.member_id = m.member_id WHERE wm.ward_id = ?
4. 코드 개선하기 💪
서비스 코드도 조금 개선해볼까요?
@Transactional
public List<WardRequestResponseDto> readWardRequest(Member member) {
if (!String.valueOf(member.getRole()).equals("HN"))
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "관리자만 접근할 수 있는 요청입니다.");
Ward myWard = member.getWardMember().getWard();
return requestRepository.findAllWardRequests(myWard)
.stream()
.map(WardRequestResponseDto::of)
.toList();
}
5. 정리 📝
N+1 문제는:
- 연관 관계가 있는 엔티티를 조회할 때 발생
- 최초 1번의 쿼리 이후 N번의 추가 쿼리가 발생하는 현상
- 성능에 매우 안 좋은 영향을 미침
이렇게 하면 N+1 문제를 깔끔하게 해결할 수 있습니다! 🎉
실습해보기 🔥
- 여러분의 프로젝트에서 비슷한 상황을 찾아보세요
- 실제로 실행되는 쿼리를 확인해보세요 (log로 확인)
이렇게 하나씩 따라하다 보면 어려웠던 N+1 문제도 자연스럽게 해결할 수 있습니다!
💡 Tip
- 항상 실행되는 쿼리를 먼저 확인하세요
- 연관 관계가 있는 엔티티를 조회할 때는 N+1 문제를 의심해보세요
'Project' 카테고리의 다른 글
[JPA] entity의 cascade 와 db의 on delete cascade 차이 이해하기 (1) | 2025.05.06 |
---|---|
[FE] 프론트 관련 내용 정리 (0) | 2025.04.29 |
[JPA] API 서비스 흐름 알아보기. (0) | 2025.02.02 |
[JPA] Entity 설계하기. (2) | 2025.01.27 |
[프로젝트 Day 5] OAuth2 로그인 (Google) (1) | 2024.11.19 |