[JPA] N+1 문제, 원인과 결과로 완벽하게 이해하기

2026. 1. 20. 10:08·개발 공부/Java-Spring

JPA 에서  성능 이슈 로 발생하는 문제 n+1 문제입니다 조인이 안해서 따로 조회 해서 발생하는 문제입니다.

오늘은 로딩 전략(Eager/Lazy)별로 N+1이 발생하는 원인과 그 결과, 그리고 해결책까지 순서대로 정리해 보겠습니다

1. 상황 가정

  • **Member(회원)**와 **Team(팀)**은 N:1 관계입니다.
  • 회원 10명을 조회하려는데, 각 회원은 서로 다른 팀에 소속되어 있습니다.

2. CASE 1: 즉시 로딩 (Eager)일 때

JPA에게 "멤버 조회할 때 팀도 무조건 같이 가져와!"라고 설정한 경우입니다.

// Member 엔티티
@ManyToOne(fetch = FetchType.EAGER) // 즉시 로딩
@JoinColumn(name = "team_id")
private Team team;
// 단순히 전체 멤버만 조회함
List<Member> members = memberRepository.findAll();

🧐 원인 분석

  1. JPQL(select m from Member m)은 글로벌 Fetch 전략(EAGER)을 신경 쓰지 않고, SQL을 생성합니다. -> 일단 Member만 조회
  2. 데이터를 다 가져온 뒤, JPA가 엔티티를 확인해보니 Team이 EAGER입니다.
  3. "어? 팀도 당장 필요하네?"라고 판단하고, 조회된 멤버 수(N)만큼 팀 조회 쿼리를 추가로 날립니다.

💥 결과 (SQL 로그)

-- 1. Member 조회 (1번)
select * from member;

-- 2. 각 Member의 Team을 채우기 위해 급하게 추가 쿼리 발송 (N번)
select * from team where id = 1;
select * from team where id = 2;
...
select * from team where id = 10;

3. CASE 2: 지연 로딩 (Lazy)일 때

"팀은 실제로 사용할 때 가져올게"라고 미루는 설정입니다. 실무에서 권장하는 방식이지만, N+1을 근본적으로 막지는 못합니다.

// Member 엔티티
@ManyToOne(fetch = FetchType.LAZY) // 지연 로딩
@JoinColumn(name = "team_id")
private Team team;

💻 실행 코드

List<Member> members = memberRepository.findAll(); // 1. 여기까지는 괜찮음

for (Member member : members) {
    // 2. 루프를 돌며 팀의 이름을 확인(사용)하는 순간!
    System.out.println(member.getTeam().getName());
}

🧐 원인 분석

  1. findAll() 시점에는 Member만 가져오고, Team 자리에는 **프록시(가짜 객체)**를 채워둡니다.
  2. 하지만 루프 안에서 member.getTeam().getName()으로 실제 데이터에 접근하려고 하면,
  3. 프록시는 "나는 데이터가 없는데?" 하고 DB에 쿼리를 날려 데이터를 가져옵니다(초기화). 이게 루프 횟수만큼 반복됩니다.
-- 1. Member 조회 시점 (1번)
select * from member;

-- ... (잠시 후) ...

-- 2. 루프 돌며 getTeam().getName() 호출 시점 (N번)
select * from team where id = 1;
select * from team where id = 2;
...

4. 해결책 (Solution)

결국 Eager든 Lazy든 **"따로 조회한다"**는 점이 문제입니다. 이를 해결하려면 **"처음부터 조인(Join)해서 가져와라"**라고 명시해야 합니다.

방법 1: Fetch Join (가장 일반적)

직접 JPQL을 작성하여 조인을 명시합니다.

@Query("select m from Member m join fetch m.team")
List<Member> findAllJoinFetch();

결과: INNER JOIN을 사용하여 쿼리 1방에 Member와 Team을 모두 가져옵니다.

 

방법 2: @EntityGraph (가장 깔끔함)

JPQL 작성 없이 어노테이션으로 해결합니다.

@EntityGraph(attributePaths = {"team"})
List<Member> findAll();

결과: LEFT OUTER JOIN을 사용하여 쿼리 1방에 모두 가져옵니다.

'개발 공부 > Java-Spring' 카테고리의 다른 글

Spring의 핵심, IoC와 DI: 제어의 역전과 의존성 주입 완벽 가이드  (0) 2026.01.23
자바 접근 제어자및 스프링부트에서 사용예시  (0) 2026.01.20
불변의 객체 의 정의  (0) 2025.12.10
spring ai 해보기  (0) 2025.12.06
객체 지향 프로그래밍(OOP)의 특징  (0) 2025.11.22
'개발 공부/Java-Spring' 카테고리의 다른 글
  • Spring의 핵심, IoC와 DI: 제어의 역전과 의존성 주입 완벽 가이드
  • 자바 접근 제어자및 스프링부트에서 사용예시
  • 불변의 객체 의 정의
  • spring ai 해보기
cookiboii
cookiboii
초보 개발자입니다.
  • cookiboii
    Just Do it
    cookiboii
  • 전체
    오늘
    어제
    • 분류 전체보기 (79)
      • 잡소리 (2)
      • vscode꾸미기 (1)
      • 회사공부 (0)
      • 면접복기 (5)
      • 개발취직관련 좋은 글귀 (12)
      • 개발 공부 (58)
        • 백준 (0)
        • web (3)
        • js&ts&react (4)
        • Java-Spring (18)
        • 개발책 리뷰 (0)
        • c++ (0)
        • git (0)
        • code up 문제 (5)
        • 네트워크 (5)
        • 자료구조&알고리즘 (2)
        • ci,cd (1)
        • 프로젝트 (9)
        • sql (4)
        • 컴퓨터구조 (1)
        • 운영체제 (5)
        • python (0)
  • 블로그 메뉴

    • 홈
    • 태그
  • 링크

    • 네이버블로그
    • 깃허브
  • 공지사항

  • 인기 글

  • 태그

    개발
    springai #spring #java
    JVM
    포켓몬 #vscode #vscode꾸미기 #개발
    라이엇 #api #롤api
    Java
    ci #개발 #git
    js #제이쿼리 #비동기통신 #코딩 #프로그래밍
    js #web
    롤 #리그오브레전드
    코드업 #코딩문제
    사이드프로젝트 #토이프로젝트 #개발 #개발자
    자바 #Java #JPA #개발자 #개발 #초보
    js #예약어 #자바스크랩트
    프로그래밍
    자바
    코딩
    깃허브 #깃 #커밋
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
cookiboii
[JPA] N+1 문제, 원인과 결과로 완벽하게 이해하기
상단으로

티스토리툴바