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를 해결한다. |
'Diary > TIL' 카테고리의 다른 글
2024-05-19) CI/CD with github action에 대한 고찰 (0) | 2024.05.24 |
---|---|
2024-05-16) MYSQL-MSSQL 비교 (0) | 2024.05.24 |
2024-05-12) 동시성 이슈, 락에 대한 고찰 (0) | 2024.05.24 |
2024-05-08) MySQL DB Indexing 고찰 (0) | 2024.05.24 |
2024-04-29) DB Connection pool 검색 (0) | 2024.05.22 |