N+1 문제Lazy FetchEagerFetchLazyFetch vs EagerFetch해결방안영속성 컨텍스트로 인해 발생하는 이슈(EntityCache ≠ DB data)Dirty Checking 성능 이슈Entity 에 Collection 있을 시
N+1 문제
- 하나의 엔티티를 조회 했는데 연관관계로 묶여있는 엔티티를 가져오기 위해 쿼리가 추가적으로 발생하는 것
- 연관 관계에서 발생하는 이슈로 연관 관계가 설정된 엔티티를 조회할 경우에 조회된 데이터 갯수(n) 만큼 연관 관계의 조회 쿼리가 추가로 발생하여 데이터를 읽어오게 됨. 이를 N+1 문제라 함
- Lazy Fetch와 Eager Fetch는 데이터를 가져오는 시점의 문제이지, N+1문제를 해결할 수는 없음
// Review.java public class Review{ @OneToMany @JoinColumn(name="review_id") private List<Comment> comments; } //Comment.java public class Comment{ @ManyToOne private Review review; }
Lazy Fetch
- Lazy Fetch의 경우 Review를 조회할 때, 항상 comments가 같이 붙어와야 하는 건 아닐 수 있기에 실제로 조회를 하려고 할 때 쿼리를 날려 comment를 가져오게 됨
failed to lazily initialize a collection of role: com.example.bookmanager.domain.Review.comments, could not initialize proxy - no Session
org.hibernate.LazyInitializationException
- Lazy Fetch가 동작하기 위해서는 아래 조건을 만족해야 함
- 세션이 열려 있을 때 → @Transactional이 적용되어 영속성 컨텍스트가 해당 entity를 관리하고 있을때
- @Transactional이 붙어있지 않으면, findAll이나 save를 하고 나면 바로 persistenceContext가 닫히게 됨. SpringJpaRepository를 이용하여 메서드를 호출할 때는.
- 그래서 영속성 컨텍스트가 닫히게 되어 LazyInitialization을 하지를 못하는것. 이미 Connection을 닫아버렸으니까. 영속성 컨텍스트도 비워진거고
- 또한, 해당 relation을 ToString.Exclude를 해주지 않으면 로그를 남기거나 객체 print를 하기 위해서 결국엔 다 가져와야 하기 때문에 Lazy Fetch의 이점을 전혀 활용할 수가 없음
EagerFetch
- Eager Fetch는 Review를 조회하게 되면 그 안에 있는 relation entity들을 한번에 다 가져오는 방법임
LazyFetch vs EagerFetch
@Test @Transactional void reviewTest(){ List<Review> reviews = reviewRepository.findAll(); // System.out.println(reviews); System.out.println("전체를 가져왔습니다."); System.out.println(reviews.get(0).getComments()); System.out.println("첫번째 리뷰를 가져왔습니다."); System.out.println(reviews.get(1).getComments()); System.out.println("두번째 리뷰를 가져왔습니다."); } //생성 쿼리(Lazy Fetch) Hibernate: select review0_.id as id1_6_, review0_.created_at as created_2_6_, review0_.updated_at as updated_3_6_, review0_.book_id as book_id7_6_, review0_.content as content4_6_, review0_.score as score5_6_, review0_.title as title6_6_, review0_.user_id as user_id8_6_ from review review0_ 전체를 가져왔습니다. Hibernate: select comments0_.review_id as review_i5_4_0_, comments0_.id as id1_4_0_, comments0_.id as id1_4_1_, comments0_.created_at as created_2_4_1_, comments0_.updated_at as updated_3_4_1_, comments0_.comment as comment4_4_1_, comments0_.review_id as review_i5_4_1_ from comment comments0_ where comments0_.review_id=? [] 첫번째 리뷰를 가져왔습니다. Hibernate: select comments0_.review_id as review_i5_4_0_, comments0_.id as id1_4_0_, comments0_.id as id1_4_1_, comments0_.created_at as created_2_4_1_, comments0_.updated_at as updated_3_4_1_, comments0_.comment as comment4_4_1_, comments0_.review_id as review_i5_4_1_ from comment comments0_ where comments0_.review_id=? [] 두번째 리뷰를 가져왔습니다.
- EagerFetch의 경우에는 reviewRepository.findAll()에서 모든 쿼리가 다 발생함
- LazyFetch는 reviewRepository.findAll()에서는 review들만 가져오고, reviews.get(0).getComments()를 할 때 첫 번째 review에 대한 comments를 가져오는 쿼리가 발생함
해결방안
- @Query를 이용하여 쿼리 직접생성
- @EntityGraph(Spring jpa 2.1 버전 이후부터 제공)를 이용하여 한번에 가져오도록 쿼리메써드 변경
- 그러나 join 문을 통해서 가져올 때 더 부담이 될 수도 있기에 상황에 따라 달라질 수 있음. join문으로 한번에 가져오는 것과 나누어서 여러 번 가져오는 것 중.
@Query(value="select distinct r from Review r join fetch r.comments") List<Review> findAllByJoin(); @EntityGraph(attributePaths="comments") List<Review> findAll();
영속성 컨텍스트로 인해 발생하는 이슈(EntityCache ≠ DB data)
- 앞에서 생긴 변화가 1차 캐쉬에 저장되어 있어서 db와 persistence context 간의 값의 차이가 존재하는 경우가 있음. → 이 때 entityManger.clear()를 활용 했었음
// Comment.java public class Comment{ @Column(columnDefinition = "datetime") // millisecond 단위 없음. private LocalDateTime commentedAt; } @Test @Transactional void commentTest(){ Comment comment = new Comment(); comment.setCommentedAt(LocalDateTime.now()); comment.setComment("우와우"); commentRepository.save(comment); entityManager.clear(); // clear를 하지 않으면 캐쉬에 저장되어 있는 millisecond 단위 //까지 나오는 commentedAt이 출력되고, clear하면 db에 저장되어 있는 // millisecond가 없는 commentedAt이 출력됨 System.out.println(commentRepository.findById(4L).get()); }
@DynamicInsert public class Comment{ } @Test @Transactional void commentTest(){ Comment comment = new Comment(); comment.setComment("우와우"); commentRepository.saveAndFlush(comment); System.out.println("entity cache : " + comment); entityManager.clear(); System.out.println("from db : " + commentRepository.findById(4L).get()); } /* entity cache : Comment(super=BaseEntity(createdAt=2022-03-14T16:57:37.643672, updatedAt=2022-03-14T16:57:37.643672), id=4, comment=우와우, commentedAt=null) from db : Comment(super=BaseEntity(createdAt=2022-03-14T16:57:37.643672, updatedAt=2022-03-14T16:57:37.643672), id=4, comment=우와우, commentedAt=2022-03-14T16:57:37.720460) */
- 기본적으로 insert와 update는 setComment()를 하지 않은 모든 필드에 대해서 다 적용되게 됨. 만약 comment에 대해서만 insert 혹은 update하고 싶으면 @DynamicInsert, @DynamicUpdate를 적용해야함
- DynamicInsert 적용하지 않으면 comment 객체가 insert 될 때, commentedAt이 null로 db에 업데이트가 되어서 db에서 불러오는 값도 null이 되어버림
Dirty Checking 성능 이슈
- Transaction 내에서 데이터를 참조하기 위해 select를 한 entity에 대해서는 하나하나 dirty checking을 하는 과정이 들어가게 됨 → 대용량 데이터 조회 시 성능 저하
- 이때 사용하는 것이 @Transactional(readOnly=true) → FlushModeType.MANUAL로 변경하게 됨 flush()가 자동적으로 일어나지 않음 , dirty check가 자동으로 일어나지 않음
public enum FlushModeType { /** * Corresponds to {@link org.hibernate.FlushMode#ALWAYS}. */ ALWAYS, /** * Corresponds to {@link org.hibernate.FlushMode#AUTO}. */ AUTO, /** * Corresponds to {@link org.hibernate.FlushMode#COMMIT}. */ COMMIT, /** * @deprecated use MANUAL, will be removed in a subsequent release */ @Deprecated NEVER, /** * Corresponds to {@link org.hibernate.FlushMode#MANUAL}. */ MANUAL, /** * Current flush mode of the persistence context at the time the query is executed. */ PERSISTENCE_CONTEXT }
Entity 에 Collection 있을 시
- 해당 Collection은 unmodifiable list이면 안됨! unmodifiable list로 초기화해서 save하려고 하면
UnsupportedOperationException
이 발생함 → [UnsupportedOperationException merge-saving many-to-many relation with hibernate and JPA]
- 그래서 Entity 내에서 컬렉션을 사용할 때, Hibernate는 아래와 같이 즉시 초기화해서 사용하기를 권장함
Collection<Entity> entities = new ArrayList<Entity>();