차례
- 일반조인 이해하기
- 일반조인에서 LazyInitializationException가 발생하는 상황 이해하기(feat. LAZY/EAGER, OSIV ON/OFF)
- 준영속 상태란?
- fetch조인 이해하기
일반조인
public interface BookRepository extends JpaRepository<Book, Long> {
@Query("SELECT b from Book b join b.bookCategory where b.bookPublisher = :bookPublisher")
List<Book> findBooks(@Param("bookPublisher") String bookPublisher);
}
@Entity
@Table(name = "book_info")
@Getter
public class Book {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "b_id")
private Long bookId;
...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "c_id")
private Category bookCategory;
}
- 조인 조건을 제외하고 실제 질의하는 대상 entity에 대한 컬럼만 조회한다.
- 즉, 여기서는 Category와 조인을 하지만 Book에 대해서만 조회한다는 의미이다.
- 일반 조인은 실제 쿼리에 조인을 걸어주기는 하지만, 조인 대상의 영속성까지는 관여하지 않는다.
- 대상 entity(Book) 값만 영속성 컨텍스트에 가져오고, 조인한 Category는 영속성 컨텍스트에 가져오지 않는다.
- 따라서 나중에 category 정보가 필요하다면 따로 select 문이 나가게 된다.
- OSIV 설정에 따라 LazyInitializationException이 발생한다.
- 영속성 컨텍스트에서 벗어난 detach(준영속) 상태가 되었을 때 지연 로딩을 할 때 자주 발생하는 예외이다.
일반조인 - 테스트
@Slf4j
@Service
@RequiredArgsConstructor
public class BookService {
private final BookRepository bookRepository;
@Transactional
public List<Book> getBooks(String publisher){
List<Book> books = bookRepository.findBooks(publisher);
return books;
}
}
@GetMapping("/test")
public void getBookTest(){
try{
List<Book> books = bookService.getBooks("민음사");
log.info("====NEXT====");
for(Book book: books){
log.info("{}",book.getBookCategory().getCategoryName());
}
}catch (Exception e){
e.printStackTrace();
}
}
select
b1_0.b_id,
b1_0.b_author,
b1_0.c_id,
b1_0.b_isbn13,
b1_0.b_picture,
b1_0.b_year,
b1_0.b_publisher,
b1_0.b_name
from
book_info b1_0
join
book_category b2_0
on b2_0.c_id=b1_0.c_id
where
b1_0.b_publisher=?
c.b.b.d.books.controller.BookController : ====NEXT====
select
c1_0.c_id,
c1_0.c_name
from
book_category c1_0
where
c1_0.c_id=?
c.b.b.d.books.controller.BookController : 소설
c.b.b.d.books.controller.BookController : 소설
c.b.b.d.books.controller.BookController : 소설
지금 나는 "민음사"로 조회한 데이터가 모두 카테고리가 같기 때문에 Category 쿼리문이 하나만 나갔다.
만약 조회된 책 3권이 모두 카테고리가 달랐다면 3개의 쿼리가 날라갔을 것이다.
이를 N+1 문제라고 한다.
이렇게 book.getBookCategory().getCategoryName()을 호출할 때 두 번째 select 문이 나가는 것을 확인할 수 있다.
그런데 여기서 LazyInitializationException이 발생하지 않는다.
왜 LazyInitializationException이 발생하지 않았을까?
OSIV가 default로 켜져있기 때문이다.
spring.jpa.open-in-view=false
application.properties에 위와 같이 명시하지 않으면 true로 설정되어 영속성 컨텍스트가 Service, Repository를 넘어 Controller, View까지 살아있다.
그렇기 때문에 해당 예외가 발생하지 않았다.
만약 다음과 같이 false로 설정하고 같은 테스트를 실행해보면 LazyInitializationException을 확인할 수 있다.
spring.jpa.open-in-view=false
만약 EAGER라면 어떨까?
TLDR; OSIV가 false여도 LazyInitializationException은 발생하지 않는다.
EAGER도 "조인 조건을 제외하고 실제 질의하는 대상 entity에 대한 컬럼만 조회한다." 라는 사실에는 변함이 없다.
하지만 LAZY와 다른 점은 나눠진 쿼리가 한 번에 질의된다는 것이다.
EAGER는 OSIV가 꺼져서 Controller 단에서 준영속 상태가 되어도 이미 필요한 데이터는 다 메모리에 있기 때문에 Exception이 발생하지 않는 것이다.
select
b1_0.b_id,
b1_0.b_author,
b1_0.c_id,
b1_0.b_isbn13,
b1_0.b_picture,
b1_0.b_year,
b1_0.b_publisher,
b1_0.b_name
from
book_info b1_0
join
book_category b2_0
on b2_0.c_id=b1_0.c_id
where
b1_0.b_publisher=?
select
c1_0.c_id,
c1_0.c_name
from
book_category c1_0
where
c1_0.c_id=?
c.b.b.d.books.controller.BookController : ====NEXT====
c.b.b.d.books.controller.BookController : 소설
c.b.b.d.books.controller.BookController : 소설
c.b.b.d.books.controller.BookController : 소설
준영속 상태란?
- 영속성 컨텍스트가 관리하는 영속 상태의 엔티티가 영속성 컨텍스트에서 분리된 상태
- 준영속 상태의 엔티티는 영속성 컨텍스트가 제공하는 기능을 사용할 수 없다.
Fetch 조인
- 조회의 주체가 되는 엔티티 외에 fetch join이 걸린 연관 entity도 함께 select하여 함께 영속화한다.
- fetch join이 걸린 entity 모두 영속화하기 때문에 FetchType이 Lazy인 Entity를 참조하더라도 이미 영속성 컨텍스트에 들어있기 때문에 따로 쿼리가 실행되지 않을 채로 N+1 문제가 해결된다.
Fetch 조인 - 테스트
다음 네가지 경우 모두에서 로그가 찍힌다.
- LAZY + OSIV ON
- LAZY + OSIV OFF
- EAGER + OSIV ON
- EAGER + OSIV OFF
원래 우리가 알고 있던 조인으로 조회하여 가져오는 것이다.
이렇게 하면 추가 쿼리가 발생하지 않기 때문에 N+1 문제도 발생하지 않는다.
select
b1_0.b_id,
b1_0.b_author,
b2_0.c_id,
b2_0.c_name,
b1_0.b_isbn13,
b1_0.b_picture,
b1_0.b_year,
b1_0.b_publisher,
b1_0.b_name
from
book_info b1_0
join
book_category b2_0
on b2_0.c_id=b1_0.c_id
where
b1_0.b_publisher=?
c.b.b.d.books.controller.BookController : ====NEXT====
c.b.b.d.books.controller.BookController : 소설
c.b.b.d.books.controller.BookController : 소설
c.b.b.d.books.controller.BookController : 소설
참고
https://cobbybb.tistory.com/18
'개발 > Spring' 카테고리의 다른 글
MySQL 외래키와 데드락 (1) | 2023.08.17 |
---|---|
MySQL 비관적 락을 이용한 인원 제한 구현하기 (0) | 2023.08.17 |
JPA 프록시 개념/존재이유와 find(), getReferenceById() (0) | 2023.08.08 |
[N+1 문제] 게시글 댓글 조회 기능 querydsl로 해결하기 (0) | 2023.08.06 |
N+1 문제란? feat. 해결방법 - 개념적 이야기 (0) | 2023.08.06 |
댓글