본문 바로가기

Diary/TIL

2024-05-14) 프로젝트 복기

N+1 문제는 데이터베이스 쿼리 성능을 저하시킬 수 있는 일반적인 문제로, 이를 해결하기 위해 여러 가지 방법이 존재한다.

Fetch Join

Fetch Join은 연관된 엔티티를 한 번의 쿼리로 가져와서 N+1 쿼리를 한 번의 쿼리로 줄여주는 방법이다.

@Entity
public class Author {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @OneToMany(mappedBy = "author", fetch = FetchType.LAZY)
    private List<Book> books;
}

@Entity
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String title;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "author_id")
    private Author author;
}

@Query("SELECT a FROM Author a JOIN FETCH a.books")
List<Author> findAllAuthorsWithBooks();

Batch Size

Batch Size는 한 번에 로드할 엔티티의 수를 조절함으로써 데이터베이스 쿼리 횟수를 줄이는 방법이다.

#application.yml
spring:
  jpa:
    properties:
      hibernate:
        default_batch_fetch_size: 10
@Entity
public class Author {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @OneToMany(mappedBy = "author", fetch = FetchType.LAZY)
    @BatchSize(size = 10)
    private List<Book> books;
}

EntityGraph

EntityGraph는 특정 엔티티와 그 연관된 엔티티를 미리 정의하여 한 번의 쿼리로 가져오는 방법이다.

@Entity
@NamedEntityGraph(
    name = "Author.books",
    attributeNodes = @NamedAttributeNode("books")
)
public class Author {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @OneToMany(mappedBy = "author", fetch = FetchType.LAZY)
    private List<Book> books;
}
#Repository
@EntityGraph(value = "Author.books", type = EntityGraph.EntityGraphType.LOAD)
@Query("SELECT a FROM Author a")
List<Author> findAllAuthorsWithBooks();

JPA 디자이너 플러그인을 사용하면 EntityGraph를 시각적으로 설계하고 관리할 수 있어 매우 유용했다고 느꼈다.

다음과 같이 편리하게 시각화해주어 편리함을 느꼈던 것 같다.

OSIV = false로 발생하는 오류와 해결책

Open Session In View (OSIV)를 false로 설정하면 발생할 수 있는 오류에 대해서도 공부해보았다. OSIV를 false로 설정하면 뷰 레이어에서 지연 로딩(Lazy Loading)을 사용할 수 없게 되어 LazyInitializationError가 발생할 수 있다. 이를 해결하기 위해서는 필요한 데이터를 미리 로드하거나, DTO(Data Transfer Object)를 사용하여 필요한 데이터를 명시적으로 가져오는 방법이 있다고 한다.

DTO(Data Transfer Object) 사용: 필요한 데이터를 미리 로드하고 DTO에 담아 뷰 레이어로 전달합니다.

public class AuthorDTO {
    private String name;
    private List<String> bookTitles;

    // Constructor and getters
}

public List<AuthorDTO> findAllAuthorsWithBooks() {
    List<Author> authors = authorRepository.findAllAuthorsWithBooks();
    return authors.stream()
        .map(author -> new AuthorDTO(author.getName(), author.getBooks().stream().map(Book::getTitle).collect(Collectors.toList())))
        .collect(Collectors.toList());
}

Fetch Join 사용: JPQL 또는 Criteria API를 사용하여 필요한 데이터를 한 번의 쿼리로 로드합니다.

@Query("SELECT a FROM Author a JOIN FETCH a.books")
List<Author> findAllAuthorsWithBooks();

Spring Data JPA의 @EntityGraph 사용: 엔티티 그래프를 사용하여 지연 로딩된 엔티티를 미리 로드합니다.

@EntityGraph(attributePaths = {"books"})
@Query("SELECT a FROM Author a")
List<Author> findAllAuthorsWithBooks();

강제 초기화: Hibernate.initialize()를 사용하여 지연 로딩된 엔티티를 명시적으로 초기화합니다.

public List<Author> findAllAuthorsWithBooks() {
    List<Author> authors = authorRepository.findAll();
    authors.forEach(author -> Hibernate.initialize(author.getBooks()));
    return authors;
}

지연 로딩된 엔티티를 미리 로딩해야하기 때문에 EntityGraph와 fetch join도 해결 방법으로 포함된다고 한다. 다만 n+1 문제와 lazyloading은 계통이 살짝 다른 것임을 이해하면 좋을것 같다.

N+1 문제와 LazyInitializationError의 차이점

N+1 문제

  • 개요: N+1 문제는 하나의 엔티티를 조회할 때, 그와 연관된 엔티티를 개별적으로 조회하기 위해 추가적인 쿼리가 발생하는 문제다.
  • 발생 상황: 예를 들어, Author 엔티티를 조회할 때, 각 Author에 대한 Book 엔티티를 각각의 쿼리로 조회하는 경우 발생한다.
  • 영향: 불필요하게 많은 데이터베이스 쿼리가 실행되어 성능 저하가 발생한다.

LazyInitializationError

  • 개요: LazyInitializationError는 지연 로딩된 엔티티에 접근하려고 할 때, 세션이 이미 닫혀 있는 경우 발생하는 오류다.
  • 발생 상황: OSIV 설정이 false인 상태에서 뷰 레이어나 서비스 레이어에서 지연 로딩된 엔티티를 접근하려고 할 때 발생한다.
  • 영향: 데이터베이스 세션이 열려 있지 않기 때문에 데이터를 로드할 수 없어서 오류가 발생한다.

대충 해결 방법으로 표로 표기하면 다음과 같다.

해결 방법 N+1 문제 LazyInitializationError  
EntityGraph Y Y 한 번의 쿼리로 연관된 엔티티를 미리 로드하여 성능을 최적화하고 지연 로딩 문제를 해결
Fetch Join Y Y JPQL에서 Fetch Join을 사용하여 연관된 엔티티를 한 번의 쿼리로 로드하여 성능을 최적화하고 지연 로딩 문제를 해결
Batch Size Y N 한 번에 로드할 엔티티의 수를 조절하여 N+1 문제를 완화하지만, LazyInitializationError를 해결하지는 않는다.
DTO 사용 N Y 필요한 데이터를 미리 로드하고 DTO에 담아 뷰 레이어로 전달하여 지연 로딩 문제를 해결합니다.
강제 초기화 N Y Hibernate.initialize()를 사용하여 지연 로딩된 엔티티를 명시적으로 초기화하여 LazyInitializationError를 해결한다.