[Spring] fetch join (1)

이연우·2025년 8월 20일

TIL

목록 보기
98/100

🧵 N + 1 문제란?

  • 연관 엔티티를 조회할 때 추가 쿼리가 반복적으로 실행되어
    N+1번의 쿼리가 나가는 문제

🐢 지연 로딩에서의 N+1 (예시: Tutor N : 1 Company)

  • select t from Tutor t튜터 N개 로드
  • 각 튜터의 t.getCompany().getName() 접근 시, 회사 조회 쿼리가 또 실행
  • 결과적으로 1(Tutor 목록) + N(Company 조회)
@Entity
@Table(name = "tutor")
public class Tutor {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "company_id")
    private Company company;

    public Tutor() {
    }

    public Tutor(String name) {
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public void setCompany(Company company) {
        this.company = company;
    }
}
@Entity
@Table(name = "company")
public class Company {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(mappedBy = "company")
    private List<Tutor> tutors = new ArrayList<>();

    public Company() {
    }

    public Company(String name) {
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public List<Tutor> getTutors() {
        return tutors;
    }

}
public class LazyMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("entity");

        EntityManager em = emf.createEntityManager();

        EntityTransaction transaction = em.getTransaction();

        transaction.begin();

        try {

            Company sparta = new Company("sparta");
            Company etc = new Company("etc");

            em.persist(sparta);
            em.persist(etc);

            Tutor tutor1 = new Tutor("tutor1" );
            Tutor tutor2 = new Tutor("tutor2" );
            Tutor tutor3 = new Tutor("tutor3" );

            tutor1.setCompany(sparta);
            tutor2.setCompany(etc);
            tutor3.setCompany(sparta);

            em.persist(tutor1);
            em.persist(tutor2);
            em.persist(tutor3);

            em.flush();
            em.clear();

            String query = "select t from Tutor t";
            List<Tutor> tutorList = em.createQuery(query, Tutor.class).getResultList();

            for (Tutor tutor : tutorList) {
                System.out.println("tutor.getName() = " + tutor.getName());
                System.out.println("tutor.getCompany().getName() = " + tutor.getCompany().getName());
            }


            transaction.commit();
        } catch (Exception e) {
            transaction.rollback();
        } finally {
            em.close();
        }

        emf.close();
    }
}

🔍 실행 흐름 요약

  • 반복 1: Tutor 1 로드 → Company(sparta) 쿼리 → 회사명 출력
  • 반복 2: Tutor 2 로드 → Company(etc) 쿼리 → 회사명 출력
  • 반복 3: Tutor 3 로드 → Company(sparta는 1차 캐시에서) 출력

👉 Tutor · Company 수가 많아질수록 N+1 폭증

💡 메모: N+1은 지연 로딩/즉시 로딩 모두에서 발생 가능


Entity Fetch Join (N:1)이란?

  • JPQL에서 한 번의 SQL로 연관 엔티티까지 함께 로드하는 최적화
    (SQL의 JOIN과 목적이 다름: “객체 그래프”를 한 번에 로드)

🔗 일반 JOIN

🛒 fetch join

✅ N:1 fetch join (TutorCompany)

String query = "select t from Tutor t join fetch t.company";
  • Tutor를 조회하면서 연관 Company까지 즉시 로딩
  • 지연 로딩 설정보다 fetch join이 우선
  • 조회된 모든 엔티티는 영속성 컨텍스트에서 관리 (프록시 X, 실제 엔티티 O)

📚 Collection Fetch Join (1:N)이란?

  • @OneToMany 기본 fetch는 LAZY

✅ 1:N fetch join (Company → Tutors)

String query = "select c from Company c join fetch c.tutorList";
List<Company> companyList = em.createQuery(query, Company.class).getResultList();

for (Company c : companyList) {
    System.out.println(c.getName());
    System.out.println(c.getTutorList().size());
}

🔎 중복과 DISTINCT

  • SQL 결과JOIN 특성상 행 중복이 발생할 수 있음
  • Hibernate 6.0+: DISTINCT 자동 적용
  • 이전 버전: select distinct c from Company c join fetch c.tutorList 사용 권장
  • JPQL의 DISTINCT는 PK 기준으로 엔티티 중복 제거 (DB DISTINCT와 의미 차이)

🚨 페이징 주의점 (Collection Fetch Join)

String query = "select c from Company c join fetch c.tutorList";
List<Company> list = em.createQuery(query, Company.class)
        .setFirstResult(0)
        .setMaxResults(1)
        .getResultList();
  • 실제 실행 SQL은 전체를 조회
  • setFirstResult / setMaxResultsSQL에 반영되지 않음
  • 모든 데이터를 읽은 뒤 메모리에서 페이징 처리

🧠 요약 정리

구분핵심 내용포인트
N+1 문제연관 엔티티 접근 시 쿼리 반복1 + N 쿼리 폭증
Entity Fetch JoinN:1 관계를 한 번에 로드join fetch t.company
효과지연 로딩보다 우선, 실제 엔티티 로드영속성 컨텍스트 관리됨
Collection Fetch Join1\:N 컬렉션 동시 로드join fetch c.tutorList
중복 처리Hibernate 6.0+ 자동 DISTINCTJPQL DISTINCTPK 기준
페이징 주의컬렉션 fetch join 시 DB 페이징 불가메모리 페이징으로 동작

0개의 댓글