SOPT-34th-Spring-Study / spring-jpa-study

🍀 [NOW SOPT] JPA 스터디 🍀
0 stars 4 forks source link

[recap] JPA N+1 문제 #8

Open kgy1008 opened 4 months ago

kgy1008 commented 4 months ago

무엇을 알게 되었나요?

프록시 객체 (Proxy Object)

먼저 프록시 객체에 대한 개념부터 간단히 살펴봅시다.

프록시 객체는 엔티티의 실제 데이터를 데이터베이스에서 가져오는 시점을 지연시키기 위해 원본(타겟) 객체를 대신해서 호출될 가짜 객체이다.

프록시 객체는 클라이언트 코드와 실제 데이터베이스에서 로드된 엔티티 객체(타겟 객체) 사이에 위치하기 때문에 클라이언트는 실제 엔티티 객체에 직접 접근하지 않고, 프록시 객체를 통해 간접적으로 접근하게 됩니다.

쉽게 비유하자면 타겟 객체를 집 주인이라고 생각했을 때, 프록시 객체는 집 주인을 대신해서 계약을 요청받는 중개인으로 생각하면 되겠네요!

지연 로딩 (Lazy Loading)

지연 로딩은 엔티티가 로드될 때, 연관된 엔티티를 즉시 로드하지 않고 필요한 시점에 연관된 객체의 데이터를 로드하는 방식입니다. @OneToMany@ManyToMany 는 기본 설정이 지연로딩이라고 합니다:D

@ManyToOne(fetch = FetchType.LAZY)  // 지연 로딩 설정 방법

연관관계를 맺은 엔티티 이때 사용되는 것이 바로 프록시 객체인데요, 지연 로딩 방식에서는 연관된 엔티티 데이터는 실제로 접근할 때까지 로드되지 않습니다. 즉, 클라이언트 코드가 객체의 메서드를 호출해야 비로소 프록시 객체는 그 순간 데이터베이스에 접근하여 실제 데이터를 로드하게 됩니다.

코드로 한번 살펴봅시다!

public Blog getBlogByPostId(Long postId) {
    Post post = postRepository.findById(postId).orElseThrow(() -> new NotFoundException("Post not found"));
    Blog blog = post.getBlog();  
    return blog;
}

Post 엔티티 내에서 Blog에 대한 접근은 FetchType.LAZY 즉, 지연 로딩으로 설정되어 있기 때문에 Post 객체만 먼저 로드되고 Blog에 대한 프록시 객체가 생성되게 됩니다. 그 후, getter 함수를 통해 post.getBlog() 를 호출하면, 프록시 객체는 실제 데이터가 필요한 시점이기 때문에 비로소 데이터베이스에 접근하여 Blog 데이터를 로드하게 되는 것이죠!

즉시 로딩 (Eager Loading)

즉시 로딩이란 말 그대로 데이터를 조회할 때, 연관된 모든 객체의 데이터까지 한 번에 불러오는 방식입니다. @ManyToOne@OneToOne 는 기본 설정이 즉시 로딩이라고 하네요:D

@OneToMany(fetch = FetchType.EAGER)  // 즉시 로딩 설정 방법

역시 코드로 한번 살펴봅시다.

아까의 코드에서 만약 Post 엔티티와 Blog 엔티티가 FetchType.EAGER 즉, 즉시 로딩으로 설정되어 있다면 어떻게 될까요? postRepository.findById(postId) 메서드를 통해 Post 엔티티를 조회할 때 즉시 Post 데이터와 연관된 Blog 데이터가 함께 로드됩니다. 프록시 객체의 생성 없이 별도의 쿼리를 통해 바로 Blog 엔티티에 접근하는 것이죠!

이처럼 즉시 로딩 방식은 지연 로딩 방식에 비해 연관된 데이터가 필요한 작업에서 빠르게 처리를 할 수 있다는 장점이 있겠네요! 하지만!!! 특히 실무에서는 즉시 로딩을 가급적 사용하지 않는다고 합니다! 이 이유로는 앞으로 설명할 JPA N+1 문제와도 연관이 있습니다.

JPA N+1 문제

JPA N+1 문제란?

JPA N+1 문제란 데이터를 조회할 때, 1개의 쿼리로 요청이 처리할 것으로 기대했으나 의도하지 않은 N개의 쿼리가 추가적으로 더 발생하는 현상을 말합니다.

말로만 들으니 너무 모호한 것 같네요. 이 역시 코드를 한번 살펴봅시다

public void getAllBlogTitleByPostId(Long postId) {
    List<Post> posts = postRepository.findByPostId(postId);
    for (Post post : posts) {
    System.out.println(post.getBlog().getTitle());
    }
}

해당 메소드를 실행시키면 다음 순서에 따라 쿼리문이 발생하겠죠!

  1. 주어진 postId에 대응하는 모든 Post 객체들을 데이터베이스로부터 로드하는 쿼리를 발생시킨다.
  2. 리스트에서 각 Post 객체에 대해 post.getBlog().getTitle() 메서드를 호출할 때마다, 각각의 Post 객체에 대해 개별적으로 Blog를 로드하는 추가 쿼리가 실행된다. (N번의 쿼리 발생)

결과적으로 첫번째 Post 객체를 로드하는 쿼리 1개와 각 Post 객체의 Blog를 로드하는 추가적인 쿼리 N개(각 Post 마다 1개)가 발생하게 되어 총 N+1회의 쿼리가 발생하게 됩니다.

이런 JPA N+1 문제는 fetch 타입이 원인인 것은 아니지만(즉시 로딩, 지연 로딩 모두 발생할 수 있음) 즉시 로딩일 경우, 특히 개발자가 제어할 수 없는 쿼리가 실행되고 더 자주 N+1문제에 마주하기 때문에 사용을 지양한다고 하네요

어려운 내용이 있었다면 이를 어떻게 해결하였나요?

어떤 자료를 참고하였나요?

https://incheol-jung.gitbook.io/docs/q-and-a/spring/n+1

hoonyworld commented 4 months ago

추가 공부하느라 고생하셨습니다! 해당 내용 스터디에서 발표해주시면 감사하겠습니다 ㅎㅎ 지연 로딩(LAZY LOADING) 으로 인하여 발생하는 LazyInitializationException은 어떻게 해결하는게 좋은 지도 알아보시면 좋을 것 같아요!