서론
JPA를 사용하면 연관관계를 갖는 엔티티에 의해 N+1 문제가 생긴다고 하던데,
게시판 개인 프로젝트에서도 게시물 목록을 조회 시 N+1 문제가 발생했다.
추가적으로 연관관계에서 성능을 위해 FetchType을 EAGER 보단 LAZY로 변경하는 걸 권장하길래
글 엔티티와 댓글 엔티티 연관관계에서 FetchType을 LAZY로 변경했더니
게시글 글 조회 시 LazyInitializationException 으로 인한 500Error가 발생했다.
위 2가지 문제 상황에 대해서 해결해보자.
"배운점 선 요약"에서 주요 학습 내용 요약을 보고 "버그 내용", "처리 내용 및 결과"의 상단 부분을 보면 된다.
자세한 내용은 "<상세 내용>"에 작성하였다.
게시판 개인 프로젝트에서 소스 참조 : https://github.com/jeoningu/Springboot-JPA-Blog
문제 해결을 위한 커밋 소스인데 보기 불편하므로 아래 설명의 "처리 내용 및 결과" 항목 보면서 소스 보면 좋다.
: https://github.com/jeoningu/Springboot-JPA-Blog/commit/b3cdeef432648be68dd08be3e732c896f0ec62a6
배운점 선 요약
- EAGER 전략 대신 LAZY를 사용하는 이유
- 직접 사용할 때만 연관관계를 조회하는 LAZY 전략이 성능상 좋기 때문
- LazyInitializationException 이 발생하는 이유, 해결하기 위한 방법
- 이유
영속상태가 아닌 엔티티 객체 내에 DB로 부터 읽어들이지 않은 연관관계 정보를 읽으려고 시도할 때 발생되는 에러 - 해결법
영속 상태 내에서 미리 조회를 해주면 됨- fetch join이나 EntityGraph를 사용해서 join으로 한번에 조회하던지
- service단의 @Transactional 내부에서 연관관계 데이터를 미리 조회
- 이유
- N+1이 발생하는 이유, @ManyToOne 일 때 해결하기 위한 방법
- 이유
JPQL은 첫 entity 기준으로 쿼리를 만들고 추 후에 연관관계 엔티티에 대해서 N개의 추가 쿼리로 조회하기 때문에 N+1 문제 발생 - 해결법
@ManyToOne 연관관계 엔티티에 대해서 left outer join으로 하고 싶으면 @EntityGraph를 사용하면 되고 inner join으로 조회하고 싶으면 fetch joinn을 사용하면 됨. 참고로 fetch join도 left를 명시하면 left outer joinn으로 쿼리를 생성할 수 있음 - 참고
쿼리 메소드(@Query("")) 에 처음부터 join으로 하면 되는 것이 아니냐 할 수 있지만 처음 부터 join으로 조회해도 N+1문제는 발생함 그렇기 떄문에 fetch join이나 @EntityGraph를 사용해서 조회해줘야 함 - 추가 공부 필요
@OneToMany인 연관관계 엔티티에 대한 N+1 문제를 해결할 때는 페이징 처리에서 문제가 생기기 때문에 별도 처리가 필요함. 아래 사이트 참고하고 내 프로젝트에서 발생하게 되면 직접 적용해보자
- 이유
버그 내용
- 화면 : 게시판 목록 조회, 게시판 글 상세 조회
- Entity : board에 user @ManToOne, reply @OneToMany 연관관계로 뒀을 때
- 문제
- 각각 EAGER, EAGER 전략을 설정하면 게시판 목록을 조회 시 user 와 reply에 대한 N개의 쿼리가 추가로 발생하여 N+1 문제가 발생한다.
- LAZY, LAZY로 변경하면 게시글 목록 조회 시, 게시글 조회 시 모두 user에 대해서 조회하여 사용하기 때문에
- 우선 user 엔티티에 대해서 LazyInitializationException 발생한다.
- user에 대해서 해결하고 나면 reply에 대해서도 LazyInitializationException 발생한다.
- LazyInitializationException 을 해결한 후에도 게시판 목록 조회시 N+1문제 발생하고 게시글 조회시에도 board와 user를 조회할 때 각각 쿼리가 발생한다.
<상세 내용>
1. EAGER, EAGER일 때
1.1 게시판 목록 조회 시
조회는 되는데
N+1 문제가 발생한다.
Board에 대해서만 JOIN으로 조회하지 않고
연관관계 엔티티인 User, Reply에 대해서 N개의 쿼리로 추가 조회된다.
Hibernate: select board0_.id as id1_0_, board0_.content as content2_0_, board0_.count as count3_0_, board0_.createdDate as createdd4_0_, board0_.modifiedDate as modified5_0_, board0_.title as title6_0_, board0_.userId as userid7_0_ from Board board0_ order by board0_.id desc limit ?
Hibernate: select user0_.id as id1_3_0_, user0_.createDate as createda2_3_0_, user0_.email as email3_3_0_, user0_.name as name4_3_0_, user0_.password as password5_3_0_, user0_.provider as provider6_3_0_, user0_.providerId as provider7_3_0_, user0_.role as role8_3_0_, user0_.username as username9_3_0_ from User user0_ where user0_.id=?
Hibernate: select user0_.id as id1_3_0_, user0_.createDate as createda2_3_0_, user0_.email as email3_3_0_, user0_.name as name4_3_0_, user0_.password as password5_3_0_, user0_.provider as provider6_3_0_, user0_.providerId as provider7_3_0_, user0_.role as role8_3_0_, user0_.username as username9_3_0_ from User user0_ where user0_.id=?
Hibernate: select user0_.id as id1_3_0_, user0_.createDate as createda2_3_0_, user0_.email as email3_3_0_, user0_.name as name4_3_0_, user0_.password as password5_3_0_, user0_.provider as provider6_3_0_, user0_.providerId as provider7_3_0_, user0_.role as role8_3_0_, user0_.username as username9_3_0_ from User user0_ where user0_.id=?
Hibernate: select replys0_.boardId as boardid5_2_0_, replys0_.id as id1_2_0_, replys0_.id as id1_2_1_, replys0_.boardId as boardid5_2_1_, replys0_.content as content2_2_1_, replys0_.createDate as createda3_2_1_, replys0_.modifiedDate as modified4_2_1_, replys0_.userId as userid6_2_1_, user1_.id as id1_3_2_, user1_.createDate as createda2_3_2_, user1_.email as email3_3_2_, user1_.name as name4_3_2_, user1_.password as password5_3_2_, user1_.provider as provider6_3_2_, user1_.providerId as provider7_3_2_, user1_.role as role8_3_2_, user1_.username as username9_3_2_ from Reply replys0_ left outer join User user1_ on replys0_.userId=user1_.id where replys0_.boardId=? order by replys0_.id desc
Hibernate: select replys0_.boardId as boardid5_2_0_, replys0_.id as id1_2_0_, replys0_.id as id1_2_1_, replys0_.boardId as boardid5_2_1_, replys0_.content as content2_2_1_, replys0_.createDate as createda3_2_1_, replys0_.modifiedDate as modified4_2_1_, replys0_.userId as userid6_2_1_, user1_.id as id1_3_2_, user1_.createDate as createda2_3_2_, user1_.email as email3_3_2_, user1_.name as name4_3_2_, user1_.password as password5_3_2_, user1_.provider as provider6_3_2_, user1_.providerId as provider7_3_2_, user1_.role as role8_3_2_, user1_.username as username9_3_2_ from Reply replys0_ left outer join User user1_ on replys0_.userId=user1_.id where replys0_.boardId=? order by replys0_.id desc
Hibernate: select replys0_.boardId as boardid5_2_0_, replys0_.id as id1_2_0_, replys0_.id as id1_2_1_, replys0_.boardId as boardid5_2_1_, replys0_.content as content2_2_1_, replys0_.createDate as createda3_2_1_, replys0_.modifiedDate as modified4_2_1_, replys0_.userId as userid6_2_1_, user1_.id as id1_3_2_, user1_.createDate as createda2_3_2_, user1_.email as email3_3_2_, user1_.name as name4_3_2_, user1_.password as password5_3_2_, user1_.provider as provider6_3_2_, user1_.providerId as provider7_3_2_, user1_.role as role8_3_2_, user1_.username as username9_3_2_ from Reply replys0_ left outer join User user1_ on replys0_.userId=user1_.id where replys0_.boardId=? order by replys0_.id desc
Hibernate: select replys0_.boardId as boardid5_2_0_, replys0_.id as id1_2_0_, replys0_.id as id1_2_1_, replys0_.boardId as boardid5_2_1_, replys0_.content as content2_2_1_, replys0_.createDate as createda3_2_1_, replys0_.modifiedDate as modified4_2_1_, replys0_.userId as userid6_2_1_, user1_.id as id1_3_2_, user1_.createDate as createda2_3_2_, user1_.email as email3_3_2_, user1_.name as name4_3_2_, user1_.password as password5_3_2_, user1_.provider as provider6_3_2_, user1_.providerId as provider7_3_2_, user1_.role as role8_3_2_, user1_.username as username9_3_2_ from Reply replys0_ left outer join User user1_ on replys0_.userId=user1_.id where replys0_.boardId=? order by replys0_.id desc
Hibernate: select replys0_.boardId as boardid5_2_0_, replys0_.id as id1_2_0_, replys0_.id as id1_2_1_, replys0_.boardId as boardid5_2_1_, replys0_.content as content2_2_1_, replys0_.createDate as createda3_2_1_, replys0_.modifiedDate as modified4_2_1_, replys0_.userId as userid6_2_1_, user1_.id as id1_3_2_, user1_.createDate as createda2_3_2_, user1_.email as email3_3_2_, user1_.name as name4_3_2_, user1_.password as password5_3_2_, user1_.provider as provider6_3_2_, user1_.providerId as provider7_3_2_, user1_.role as role8_3_2_, user1_.username as username9_3_2_ from Reply replys0_ left outer join User user1_ on replys0_.userId=user1_.id where replys0_.boardId=? order by replys0_.id desc
1.2 게시판 글 조회 시
게시판 글 조회도 되는데,
게시글과 게시글유저의 left outer join
게시글의 댓글과 댓글유저의 left outer join
이 발생한다. 이렇게 해도 문제가 없는건가?..
from Board board0_
left outer join Reply replys1_ on board0_.id=replys1_.boardId
left outer join User user2_ on replys1_.userId=user2_.id
left outer join User user3_ on board0_.userId=user3_.id
where board0_.id=?
order by replys1_.id desc
Hibernate: select board0_.id as id1_0_0_, board0_.content as content2_0_0_, board0_.count as count3_0_0_, board0_.createdDate as createdd4_0_0_, board0_.modifiedDate as modified5_0_0_, board0_.title as title6_0_0_, board0_.userId as userid7_0_0_, replys1_.boardId as boardid5_2_1_, replys1_.id as id1_2_1_, replys1_.id as id1_2_2_, replys1_.boardId as boardid5_2_2_, replys1_.content as content2_2_2_, replys1_.createDate as createda3_2_2_, replys1_.modifiedDate as modified4_2_2_, replys1_.userId as userid6_2_2_, user2_.id as id1_3_3_, user2_.createDate as createda2_3_3_, user2_.email as email3_3_3_, user2_.name as name4_3_3_, user2_.password as password5_3_3_, user2_.provider as provider6_3_3_, user2_.providerId as provider7_3_3_, user2_.role as role8_3_3_, user2_.username as username9_3_3_, user3_.id as id1_3_4_, user3_.createDate as createda2_3_4_, user3_.email as email3_3_4_, user3_.name as name4_3_4_, user3_.password as password5_3_4_, user3_.provider as provider6_3_4_, user3_.providerId as provider7_3_4_, user3_.role as role8_3_4_, user3_.username as username9_3_4_ from Board board0_ left outer join Reply replys1_ on board0_.id=replys1_.boardId left outer join User user2_ on replys1_.userId=user2_.id left outer join User user3_ on board0_.userId=user3_.id where board0_.id=? order by replys1_.id desc
2. LAZY, LAZY일 때
2.1 게시판 목록 조회 시
게시판 목록 화면단에서 게시글 저자를 보여주고 있기 때문에 User 엔티티 조회가 필요한데
LAZY 전략으로 설정한 User 엔티티에 대해서 LazyInitializationException 이 발생하여 500Error다.
Hibernate: select board0_.id as id1_0_, board0_.content as content2_0_, board0_.count as count3_0_, board0_.createdDate as createdd4_0_, board0_.modifiedDate as modified5_0_, board0_.title as title6_0_, board0_.userId as userid7_0_ from Board board0_ order by board0_.id desc limit ?
2023-03-29 14:06:23.977 ERROR 22420 --- [nio-8080-exec-3] o.a.c.c.C.[.[localhost].[/].[jsp] : Servlet.service() for servlet [jsp] threw exception
org.hibernate.LazyInitializationException: could not initialize proxy [com.jig.blog.model.User#3] - no Session
at org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:176) ~[hibernate-core-5.6.14.Final.jar:5.6.14.Final]
at org.hibernate.proxy.AbstractLazyInitializer.getImplementation(AbstractLazyInitializer.java:322) ~[hibernate-core-5.6.14.Final.jar:5.6.14.Final]
at org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor.intercept(ByteBuddyInterceptor.java:45) ~[hibernate-core-5.6.14.Final.jar:5.6.14.Final]
at org.hibernate.proxy.ProxyConfiguration$InterceptorDispatcher.intercept(ProxyConfiguration.java:95) ~[hibernate-core-5.6.14.Final.jar:5.6.14.Final]
at com.jig.blog.model.User$HibernateProxy$CrTCon3k.getName(Unknown Source) ~[classes/:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:566) ~[na:na]
2.2 게시판 글 상세 조회 시
게시판 목록에서 버그가 발생하여 게시판 글을 못 보는 상황이라서 아래 방법으로 화면 접속
- User에 대한 문제를 해결해서 게시물 목록을 화면에서 보이게 한 후 게시글에 접속
- 강제로 URL 입력해서 접속 해도 된다. localhost:8080/board/1
게시글 화면단에서도 게시글 저자를 보여주고 있기 때문에 User 엔티티 조회가 필요한데
LAZY 전략이라 지연로딩이므로 User를 조회하지 않는다.
그리고 User 엔티티에 대해서 LazyInitializationException 이 발생하여 500Error다.
Hibernate: select board0_.id as id1_0_0_, board0_.content as content2_0_0_, board0_.count as count3_0_0_, board0_.createdDate as createdd4_0_0_, board0_.modifiedDate as modified5_0_0_, board0_.title as title6_0_0_, board0_.userId as userid7_0_0_ from Board board0_ where board0_.id=?
2023-03-29 14:37:55.442 ERROR 22420 --- [io-8080-exec-10] o.a.c.c.C.[.[localhost].[/].[jsp] : Servlet.service() for servlet [jsp] threw exception
org.hibernate.LazyInitializationException: could not initialize proxy [com.jig.blog.model.User#4] - no Session
at org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:176) ~[hibernate-core-5.6.14.Final.jar:5.6.14.Final]
at org.hibernate.proxy.AbstractLazyInitializer.getImplementation(AbstractLazyInitializer.java:322) ~[hibernate-core-5.6.14.Final.jar:5.6.14.Final]
at org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor.intercept(ByteBuddyInterceptor.java:45) ~[hibernate-core-5.6.14.Final.jar:5.6.14.Final]
at org.hibernate.proxy.ProxyConfiguration$InterceptorDispatcher.intercept(ProxyConfiguration.java:95) ~[hibernate-core-5.6.14.Final.jar:5.6.14.Final]
at com.jig.blog.model.User$HibernateProxy$CrTCon3k.getName(Unknown Source) ~[classes/:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:566) ~[na:na]
at javax.el.BeanELResolver.getValue(BeanELResolver.java:97) ~[tomcat-embed-el-9.0.69.jar:3.0.FR]
처리 내용 및 결과
board에 user와 @ManToOne, reply와 @OneToMany 연관관계로 뒀을 때
- 성능상 이점을 위해 필요할 때만 조회하는 LAZY, LAZY로 변경
- 게시글 목록 조회시, 게시글 조회시 LazyInitializationException 을 해결하기 위해 @Transactional 내부에서 연관관계를 미리 조회
- 게시글 목록 조회시 user에 대한 N+1을 해결하기 위해 fetch join을 사용해서 목록을 조회
- 게시글 상세 조회시
- user에 대해서는 같이 조회하기 위해 fetch join을 사용하고
- reply는 @Transactional 내부에서 연관관계를 미리 조회해서 reply조회 쿼리를 생성한다.
<상세 내용>
1. LazyInitializationException 해결하기 위해 @Transactional 내부에서 연관관계를 미리 조회
1.1 게시글 목록 조회 시
실제로 사용할 때만 로딩하는 LAZY 전략을 사용하게 되면 LazyInitializationException 발생합니다.
LazyInitializationException 는 영속상태가 아닌 엔티티 객체 내에서 DB로부터 읽어들이지 않은 연관관계 정보를 읽으려 할 때 발생하는 것이므로 Transactional 내부에서 연관관계를 미리 조회해서 해결할 수 있습니다.
board 엔티티의 연관관계인 user의 변수하나를 조회할 경우 user를 조회하는 쿼리가 실행됩니다.
/**
* 게시글 목록 조회
*/
@Transactional(readOnly = true)
public Page<Board> getBoardList(Pageable pageable) {
Page<Board> boards = boardRepository.findAll(pageable);
// @Transactional 설정된 Service 내의 메서드에서, 정보를 미리 LOAD합니다.
// 영속상태에서 미리 연관관계 정보를 로드해두면, LAZY 관련 에러가 발생되지 않습니다.
boards.stream().forEach(board -> board.getUser().getEmail());
return boards;
}
Hibernate: select board0_.id as id1_0_, board0_.content as content2_0_, board0_.count as count3_0_, board0_.createdDate as createdd4_0_, board0_.modifiedDate as modified5_0_, board0_.title as title6_0_, board0_.userId as userid7_0_ from Board board0_ order by board0_.id desc limit ?
Hibernate: select user0_.id as id1_3_0_, user0_.createDate as createda2_3_0_, user0_.email as email3_3_0_, user0_.name as name4_3_0_, user0_.password as password5_3_0_, user0_.provider as provider6_3_0_, user0_.providerId as provider7_3_0_, user0_.role as role8_3_0_, user0_.username as username9_3_0_ from User user0_ where user0_.id=?
Hibernate: select user0_.id as id1_3_0_, user0_.createDate as createda2_3_0_, user0_.email as email3_3_0_, user0_.name as name4_3_0_, user0_.password as password5_3_0_, user0_.provider as provider6_3_0_, user0_.providerId as provider7_3_0_, user0_.role as role8_3_0_, user0_.username as username9_3_0_ from User user0_ where user0_.id=?
Hibernate: select user0_.id as id1_3_0_, user0_.createDate as createda2_3_0_, user0_.email as email3_3_0_, user0_.name as name4_3_0_, user0_.password as password5_3_0_, user0_.provider as provider6_3_0_, user0_.providerId as provider7_3_0_, user0_.role as role8_3_0_, user0_.username as username9_3_0_ from User user0_ where user0_.id=?
1.2 게시판 글 상세 조회 시
reply에서 발생하는 lazyInitializationException을 해결하기 위해 @Transactional 내부에서 reply의 변수 하나를 조회하여 reply 조회 쿼리를 발생시킨다.
/**
* 글 상세 조회
*/
@Transactional(readOnly = true)
public Board getBoard(int id) {
Board board = boardRepository.findById(id).orElseThrow(() -> {
return new IllegalArgumentException("글 상세 보기 실패 - 찾을 수 없는 board id 입니다. : " + id);
});
// @Transactional 설정된 Service 내의 메서드에서, 정보를 미리 LOAD합니다.
// 영속상태에서 미리 연관관계 정보를 로드해두면, LAZY 관련 에러가 발생되지 않습니다.
board.getUser().getEmail();
board.getReplys().stream().forEach(reply -> reply.getContent());
return board;
}
Hibernate: select board0_.id as id1_0_0_, board0_.content as content2_0_0_, board0_.count as count3_0_0_, board0_.createdDate as createdd4_0_0_, board0_.modifiedDate as modified5_0_0_, board0_.title as title6_0_0_, board0_.userId as userid7_0_0_ from Board board0_ where board0_.id=?
Hibernate: select user0_.id as id1_3_0_, user0_.createDate as createda2_3_0_, user0_.email as email3_3_0_, user0_.name as name4_3_0_, user0_.password as password5_3_0_, user0_.provider as provider6_3_0_, user0_.providerId as provider7_3_0_, user0_.role as role8_3_0_, user0_.username as username9_3_0_ from User user0_ where user0_.id=?
Hibernate: select replys0_.boardId as boardid5_2_0_, replys0_.id as id1_2_0_, replys0_.id as id1_2_1_, replys0_.boardId as boardid5_2_1_, replys0_.content as content2_2_1_, replys0_.createDate as createda3_2_1_, replys0_.modifiedDate as modified4_2_1_, replys0_.userId as userid6_2_1_, user1_.id as id1_3_2_, user1_.createDate as createda2_3_2_, user1_.email as email3_3_2_, user1_.name as name4_3_2_, user1_.password as password5_3_2_, user1_.provider as provider6_3_2_, user1_.providerId as provider7_3_2_, user1_.role as role8_3_2_, user1_.username as username9_3_2_ from Reply replys0_ left outer join User user1_ on replys0_.userId=user1_.id where replys0_.boardId=? order by replys0_.id desc
2. LazyInitializationException 를 해결하기 위해 @Transactional 내부에서 연관관계를 미리 조회하게 되면
- 게시글 목록 조회 시 @ManyToOne 연관관계인 엔티티에 대해서 N개 추가 조회를 하여 N+1 문제가 발생한다.
- 게시판 글 상세 조회 시 @ManyToOne, @OneToMany 연관관계인 엔티티에 대해서는 따로따로 조회하여 3개의 쿼리가 발생한다.
2.1 게시글 목록 조회 시
N+1 문제가 발생한다.
@EntityGraph 를 사용하면 기본적으로 left outer join으로 조인하고
fetch join을 사용하면 기본적으로 inner join으로 조인한다. ( left outer join으로 생성할 수도 있긴 하다.)
게시물은 항상 저자가 있으니 inner join으로 하는 게 맞고
fetch join으로 해결하려고 한다.
만약 OneToMany 관계라면 여기서 추가적인 문제가 발생할 수 있는데, 그건 다음에 알아보자.
OneToMany에서 page 처리 문제
: JPA 모든 N+1 발생 케이스과 해결책 (velog.io)
: [Spring Data JPA Tutorial] 10. LazyInitializationException 해결하기 2. @OneToMany (jiniworld.me)
2.1.2 EntityGraph 해결
- EntityGraph 는 자동으로 count 쿼리 생성
- left outer join
//from Board board0_ left outer join User user1_ on board0_.userId=user1_.id
@EntityGraph(attributePaths = "user")
Page<Board> findAllDistinctWithUserBy(Pageable pageable);
Hibernate: select distinct board0_.id as id1_0_0_, user1_.id as id1_3_1_, board0_.content as content2_0_0_, board0_.count as count3_0_0_, board0_.createdDate as createdd4_0_0_, board0_.modifiedDate as modified5_0_0_, board0_.title as title6_0_0_, board0_.userId as userid7_0_0_, user1_.createDate as createda2_3_1_, user1_.email as email3_3_1_, user1_.name as name4_3_1_, user1_.password as password5_3_1_, user1_.provider as provider6_3_1_, user1_.providerId as provider7_3_1_, user1_.role as role8_3_1_, user1_.username as username9_3_1_ from Board board0_ left outer join User user1_ on board0_.userId=user1_.id order by board0_.id desc limit ?
Hibernate: select distinct count(distinct board0_.id) as col_0_0_ from Board board0_
2.1.2 fetch join으로 해결
- fetch join은 페이징 처리를 위해 별도 count 쿼리 필요
- inner join
// N+1 문제 해결을 위해 fetch join 사용
// 만들어지는 쿼리 : from Board board0_ inner join User user1_ on board0_.userId=user1_.id
@Query(
value = "select s from Board s join fetch s.user",
countQuery = "select count(s) from Board s"
)
Page<Board> findAllWithUserBy(Pageable pageable);
Hibernate: select board0_.id as id1_0_0_, user1_.id as id1_3_1_, board0_.content as content2_0_0_, board0_.count as count3_0_0_, board0_.createdDate as createdd4_0_0_, board0_.modifiedDate as modified5_0_0_, board0_.title as title6_0_0_, board0_.userId as userid7_0_0_, user1_.createDate as createda2_3_1_, user1_.email as email3_3_1_, user1_.name as name4_3_1_, user1_.password as password5_3_1_, user1_.provider as provider6_3_1_, user1_.providerId as provider7_3_1_, user1_.role as role8_3_1_, user1_.username as username9_3_1_ from Board board0_ inner join User user1_ on board0_.userId=user1_.id order by board0_.id desc limit ?
Hibernate: select count(board0_.id) as col_0_0_ from Board board0_
/**
* 게시글 목록 조회
*/
@Transactional(readOnly = true)
public Page<Board> getBoardList(Pageable pageable) {
// #1. board에 @ManyToOne인 user 엔티티에 의해 일반 전체 조회 쿼리를 사용하면 N+1 문제가 발생한다.
// 1-1 or 1-2 방법을 사용해서 해결할 수 있다.
// 여기서는 board에는 항상 user가 있을 테니까 inner join이 되는 fetch join이 적절해 보인다.
// Page<Board> boards = boardRepository.findAll(pageable);
// #1-1. N+1 문제를 해결하기 위해 EntityGraph를 사용 -> from Board board0_ left outer join User
//Page<Board> boards = boardRepository.findDistinctWithUserBy(pageable);
// #1-2. N+1 문제를 해결하기 위해 fetch join 사용 -> from Board board0_ inner join User
Page<Board> boards = boardRepository.findAllWithUserBy(pageable);
// @Transactional 설정된 Service 내의 메서드에서, 정보를 미리 LOAD합니다.
// 영속상태에서 미리 연관관계 정보를 로드해두면, LAZY 관련 에러가 발생되지 않습니다.
boards.stream().forEach(board -> Optional.ofNullable( board.getUser()).map(User::getName));
return boards;
}
2.2 게시판 글 상세 조회 시 @ManyToOne, @OneToMany 연관관계인 엔티티에 대해서는 따로따로 조회하여 3개의 쿼리가 발생한다.
- boar와 user엔티티에 대해서 한번에 조회하기 위해 fetch 조인으로 해결
@Query("select s from Board s join fetch s.user where s.id = :id")
Optional<Board> findWithUserById(int id);
- reply는 @Transactional 내부에서 연관관계를 미리 조회하게 되면 reply를 쿼리를 생성한다.
/**
* 글 상세 조회
*/
@Transactional(readOnly = true)
public Board getBoard(int id) {
Board board = boardRepository.findWithUserById(id).orElseThrow(() -> {
return new IllegalArgumentException("글 상세 보기 실패 - 찾을 수 없는 board id 입니다. : " + id);
});
// @Transactional 설정된 Service 내의 메서드에서, 정보를 미리 LOAD합니다.
// 영속상태에서 미리 연관관계 정보를 로드해두면, LAZY 관련 에러가 발생되지 않습니다.
board.getUser().getEmail();
board.getReplys().stream().forEach(reply -> reply.getContent());
return board;
}
참고
lazyInitializationException 해결 방법
- JPA 모든 N+1 발생 케이스과 해결책 (velog.io)
- [Spring Data JPA Tutorial] 10. LazyInitializationException 해결하기 2. @OneToMany (jiniworld.me)
JPA 페이지 처리 방법
'Project & Issu' 카테고리의 다른 글
feign client에서 @RequestParameter에 "&" 를 넘겨주지 못 하는 버그 (0) | 2023.12.14 |
---|---|
좋아요 기능 동시성 처리 (0) | 2023.05.04 |
토이 프로젝트에 적용할 기능, 기술에 대한 기록 - 알림 기능, 요청에 대한 이력 적재 기능, maven -> gradle (1) | 2023.03.27 |
JWT 연습 (0) | 2023.02.10 |
security (0) | 2023.01.08 |