전날에는 환경 문제로 이 강의를 건너뛰고 스프링 시큐리티를 잠깐 봤습니다. 오늘은 원래대로 돌아와서 JPA 강의를 마저 봅니다.
실습 가능한 환경이 있는 곳으로 돌아왔건만 오늘은 별로 실습할 내용이 없습니다. 근데 강의 길이는 좀 깁니다... 한 동영상에 40분 뭐야...
N+1 문제를 향해
이번 강의의 주제는 N+1 문제가 무엇인지, 그리고 어떻게 해결하는지 알아보는 것입니다. 많이 들어봤을 유명한 문제라고는 하셨는데 이게 무엇인지는 명확하게 짚어주지 않으신 채로 앞으로 나아갔습니다. 심지어 저도 찾아봤었는데 너무 오래전에 찾아봐서 뭔지 까먹었었거든요. 패캠 제발 강의 검수좀...
정리를 계속 하기 전에 이게 뭔지부터 다시 알아보고 갑시다.
N+1 문제?
- https://stackoverflow.com/a/97253
- 그 외에는 적절히 언급할 출처는 없는 거 같네요. 검색 결과가 전부 블로그입니다.
(이 글도 그 중 하나가 되겠죠 ㅎㅎ)
N+1 문제는, ORM 에서 분명 테이블은 한 번 조회했는데 쿼리가 N번 추가로 일어나는 현상입니다. 원인은, 나는 객체 "Blog"(1에 해당) 를 조회했는데 필드인 "comments"(blog의 횟수만큼 == N) 까지 딸려오기 때문입니다.
JPA에 국한된 문제는 아니고 ORM이라면 일어나는 일입니다. 데이터베이스 테이블을 클래스에 매핑하고, 그걸 자동으로 조회하기 때문에 일어나는 일이거든요. 그래서 ORM이라면 해결방법이 꼭 제공된다고 강의에서 들었던 거 같기도 하고?
fetch 전략: EAGER 와 LAZY 의 차이점
N+1 문제를 자세히 알아보기 전에 걸리적거리는 EAGER fetch 에 대해서 짚고 넘어가봐야 합니다.
N+1 문제를 확인하기 위한 환경을 세팅하는 과정에서 자연스럽게 의도하신건지 아니면 정말로 이참에 알아보자인지는 강사님 본인만 아시겠지만 이전에 슬쩍 설정만 하고 넘어갔던... 어라 한 번 설명하신 적 있는 거 같은데... EAGER / LAZY fetch 설정에 대해서 알아봅니다.
fetch 전략: 세팅
이걸 위해서 다음과 같이 설정합니다. 목적은 @OneToOne, @OneToMany, @ManyToOne, @ManyToMany 넷 중에 뒤에가 One인거랑 Many 인거랑 하나씩을 써보는 것입니다. 기본값을 설명하는데 필요하거든요.
설정은 리뷰(Review)에 댓글(Comment)이 달렸다는 기묘한 설정입니다. YES YES YES
일단 이번에 테스트할 클래스인 Comment 를 추가하고...
@Data
@NoArgsConstructor
@Entity
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String commentText;
@ManyToOne(fetch = FetchType.LAZY)
@ToString.Exclude
private Review review;
}
Review 엔티티를 좀 손봐줍니다.
@Data
생략
@Entity
public class Review extends 생략 {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
생략
@ManyToOne(fetch = FetchType.LAZY)
@ToString.Exclude // <- 밑에선 생략
private Account account;
@ManyToOne(fetch = FetchType.LAZY)
private Book book;
// @OneToMany(fetch = FetchType.LAZY) // << ---- 이 부분을 가지고
@OneToMany(fetch = FetchType.EAGER) // << ---- 테스트합니다.
@JoinColumn(name = "review_id")
private List<Comment> comments = new ArrayList<>();
}
눈치채셨나요? Review에 새로 추가한 comments 외에는 모두 LAZY 로 처리되어있습니다.
fetch 전략이란
fetch 전략이란 객체의 필드를 어느 시점에 불러올건지를 결정하는 내용입니다.
https://docs.jboss.org/hibernate/jpa/2.2/api/javax/persistence/FetchType.html
- EAGER: 데이터를 열성적으로, 적극적으로 가져옵니다.
적극적으로 가져온다는 말은 그 필드는 가능한한 빠른 시점에 데이터베이스에서 조회해와야 한다는 의미입니다.
> Defines that data must be eagerly fetched. - LAZY: 데이터를 가능한한 늦게 가져옵니다.
해당 필드에 직접 액세스하는 등, 그 필드가 필요해지는 시점까지 데이터의 조회를 가능한한 늦춥니다.
> Defines that data can be lazily fetched.
LAZY의 오류 - Persistence Context가 필요하다
아무 생각없이 fetch 전략을 LAZY 로 설정할 경우 오류가 발생했었습니다. LAZY가 동작하려면 해당 객체가 영속성 컨텍스트(Persistence Context) 안에서 관리되고 있는 상태여야만 가능하기 때문입니다. 그게 세션이구요. 그래서 오류 내용이 세션이 없다고 나오는 겁니다.
오류 내용:
failed to lazily initialize a collection of role: kr.co.fastcampus.jpasandbox.review.Review.comments, could not initialize proxy - no Session
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: kr.co.fastcampus.jpasandbox.review.Review.comments, could not initialize proxy - no Session
해결 방법은
- @Transactional 을 넣어서 세션을 만들거나
- EAGER로 바꾸거나
둘 중 하나네요. 이전 강의들 중 일부에서 오류를 해결하기 위해 일단 EAGER를 박고 시작한 강의가 몇몇 있었는데 이런 이유였죠.
EAGER vs LAZY, 쿼리가 언제 실행되는지의 차이
암튼 원래대로 돌아와서, Review.comments 가 EAGER 이면 findAll() 만 해도 바로 Comment를 조회하는 쿼리가 실행되고, 그렇지 않으면 해당 필드를 실제로 사용할 때에 쿼리가 실행됩니다.
@Test
@Transactional
open fun lazyEagerFetchTest() {
// 추풍낙엽과도 같은 들여쓰기 변덕부림
// (협업 코드에선 이런 짓은 절대 하지 마세요)
// 강의에서는 이런 몹쓸 짓은 안 했습니다.
val review1 = putReview("리뷰1")
val comment1 = putComment("댓글1")
val comment2 = putComment("댓글2")
reviewWithComment(review1, comment1)
reviewWithComment(review1, comment2)
val review3 = putReview("리뷰3")
val comment3 = putComment("댓글3")
reviewWithComment(review3, comment3)
println("---------------START: find all-------------------")
val reviews= reviewRepo.findAll()
// ^^^^^^^^^
// EAGER 이면 commnent 의 조회가 여기에서 일어납니다.
println("---------------END: find all------------------")
// LAZY 이면 commnent 의 조회가 여기에서 일어납니다. 아마 프록시 객체를 쓰겠죠?
// vvvvvvvv
println(reviews.get(0).comments)
}
쿼리 실행 결과는 생략 (주석 확인). 살짝 신기하긴 했어요 ㅋㅋ 왜 이러는지 알면서도 예상외인 것처럼 돌아가니까...
EAGER vs LAZY: 기본값!
필드 타입이 List가 되는 조회할 게 많은 건 기본값이 lazy, 그렇지 않고 하나만 넣으면 되는 건 기본값이 EAGER 입니다. 알고가야 뒤통수를 안 맞겠죠?
강의에서 이렇게 설명하신 건 아니고 대체 왜 그럴까를 생각해보니까 눈치를 까겠더라구요. 맞아요, 짐작입니다. 킹리적 갓심이라고 부르는 그거죠.
N+1 문제 해결하기
EAGER이던, LAZY이던 이러나 저러나 JVM <- ORM -> DB 의 왕복이 1~N번 더 일어나는 문제에서는 자유롭지 않습니다. 한 번의 쿼리문으로 다 조회해와야 여러번 왕복하는... N번의 쿼리를 실행하는 일이 없겠죠. ORM이 최적화를 해주면 제일 좋은데...
- 방법 1: 커스텀 쿼리로 직접 조회
- 방법 2: 엔티티 그래프 어노테이션 사용 (Spring Data JPA 2.1 이후)
요약코드
interface ReviewRepo: JpaRepository<Review, Long> {
@Query("select distinct r from Review r join fetch r.comments")
fun findAllbyFetchJoin(): List<Review>
@EntityGraph(attributePaths = "comments")
@Query("select r from Review r")
fun findAllByEntityGraph(): List<Review>
@EntityGraph(attributePaths = "comments")
override fun findAll(): List<Review>
}
커스텀 쿼리로 조회해서 연관 테이블까지 한 번에 조회하기
JPA가 자동으로 한 번에 조회해주지 못한다면 직접 쿼리를 넣어주면 됩니다. 네이티브 쿼리가 아닌 JPQL도 괜찮습니다.
@Query("select distinct r from Review r join fetch r.comments")
fun findAllbyFetchJoin(): List<Review>
이번 구문에는 JPQL 로 "join fetch" 를 하고, distinct 를 사용해서 중복된 객체가 만들어지지 않도록 바로잡아줍니다. JPQL 멀?라요... 필요할 때 찾아보면 되겠쥬. 테이블이 아니라 필드 기준인 건 당연한 거구요.
distinct 가 없으면 (review 1 x comment 1), (review 1 x comment 2) 처럼 Review 객체가 두 개 생성됩니다.
@EntityGraph 로 같이 조회하기
이름으로 봐서 엔티티의 그래프를 만들어서 한꺼번에 조회하는 데 사용되는 거 같은데, 쓰는 입장에서야 그런 건 아무래도 좋고 한꺼번에 조회할 필드 이름을 적어주면 됩니다. 솔직히 궁금하긴 한데 시간이 없어... 그..그래도 문서라도...
- 오피셜 - https://docs.spring.io/spring-data/jpa/docs/current/api/org/springframework/data/jpa/repository/EntityGraph.html
- 블로그 - https://www.baeldung.com/spring-data-jpa-named-entity-graphs
@EntityGraph(attributePaths = "comments")
@Query("select r from Review r")
fun findAllByEntityGraph(): List<Review>
@EntityGraph(attributePaths = "comments")
override fun findAll(): List<Review>
적용 가능한 메소드의 종류가 한정적이지 않고, 커스텀 쿼리도 되고 JpaRepo 의 기본 메소드들도 되고 뭐 그런 점만 확인하시면 될듯
아 저거 attributePaths 배열이네요 강의에서 배열표시 좀 넣어주시지.
이거 실행해볼 시간은 없을듯; 오늘 제출일...
본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성되었습니다.
패스트캠퍼스: https://bit.ly/37BpXiC
#패스트캠퍼스 #패캠챌린지 #직장인인강 #직장인자기계발 #패스트캠퍼스후기 #한번에끝내는JavaSpring웹개발마스터초격차패키지Online
JPQL은 언제 시간을 내서 배워둘 가치가 있는 것 같습니다. Spring을 쓰겠다면 말이죠...
'FastCampus - 한번에 끝내는 Java|Spring 웹 개발 > 04 JPA' 카테고리의 다른 글
JPA Ch 11 - Dirty Check에 의한 batch 성능 이슈 - 패스트캠퍼스 챌린지 45일차 (0) | 2022.03.09 |
---|---|
JPA Ch 11 - 캐시와 DB의 불일치 - 패스트캠퍼스 챌린지 44일차 (0) | 2022.03.08 |
JPA Ch 10 @Embeddable - 패스트캠퍼스 챌린지 41일차 (0) | 2022.03.05 |
JPA Ch 9 @Converter - 패스트캠퍼스 챌린지 40일차 (0) | 2022.03.04 |
JPA Ch 9 @Query (3), NativeQuery - 패스트캠퍼스 챌린지 39일차 (0) | 2022.03.03 |