Project & Issu

게시판 개인 프로젝트 JPA 이슈 해결 - N+1, LazyInitializationException

gu9gu 2023. 5. 2. 15:37

 

서론

 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 연관관계로 뒀을 때

  1. 성능상 이점을 위해 필요할 때만 조회하는 LAZY, LAZY로 변경
  2. 게시글 목록 조회시, 게시글 조회시 LazyInitializationException 을 해결하기 위해 @Transactional 내부에서 연관관계를 미리 조회
  3. 게시글 목록 조회시 user에 대한 N+1을 해결하기 위해 fetch join을 사용해서 목록을 조회
  4. 게시글 상세 조회시
    1. user에 대해서는 같이 조회하기 위해 fetch join을 사용하고
    2. 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 페이지 처리 방법