0. 관련 포스팅
1. 개요
이전 포스팅에서
~ToOne에 Fetch Join,
~ToMany의 경우 한 개의 연관 관계에만 사용하는 것이 좋다고 언급하였다.
그렇다면 '어떻게 연관 관계에 있는 Entity를 함께 조회하는게 좋을까?' 에 대한 것을 정리해보고
연관관계에 있는 Entity를
모두 Fetch Join 하는 경우와 ~ToOne 만 Fetch Join 하는 경우를 비교해보는 포스팅이다.
2. 구현
a. Comment
이전 포스팅에서의 도메인 관계에 Comment(댓글) 라는 Entity를 추가하였다.
연관 관계의 형태는
- Member : Comment (1:N)
- Board : Comment (1:N)
- Member : Board (1:N)
관계를 띠고있고, Comment 클래스는 아래 코드와 같이 구성되게끔 하였다.
@Entity
@Table(name = "COMMENT")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column
private String content;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "board_id")
private Board board;
@Builder
public Comment(Long id, String content, Member member, Board board) {
this.id = id;
this.content = content;
this.member = member;
this.board = board;
}
}
b. Repository
조회는 Board(게시글)을 조회하도록 하고
Member(게시글의 작성자)와 Comment(댓글)을 함께 조회하도록 진행하였다.
- findAllFetchMemberAndComments : 게시글과 연관된 Member, Comment를 Fetch Join 하는 메소드
- findAllFetchMember : 게시글과 연관된 Member 만 Fetch Join 하는 메소드
comment는 데이터가 존재하지 않으면 inner join 으로는 데이터를 가져오지 않기 때문에 left join fetch 로 진행되었다.
3. Run Test
테스트에 앞서 현재 데이터베이스에 있는 데이터는 아래 이미지와 같다.
a. 모든 연관관계 Fetch Join
아래 코드는 연관 데이터 모두를 Fetch Join 하였을때 테스트 코드이다.
비지니스 로직을 따로 놔두었기 때문에 저번에 작성한 Member, Board 테스트와는 크게 다르지 않다.
@Test
void Board_전체_Fetch_Join() {
//given
//when
System.out.println("Board를 조회한다.");
List<Board> boards = boardService.getAllBoardsFetchAll();
System.out.println("연관 데이터를 확인한다. board size : "+boards.size());
for (Board board : boards) {
Member member = board.getMember();
List<Comment> comments = board.getComments();
System.out.println("writer : "+member.getName()+" comment size : "+comments.size());
}
//then
Assertions.assertThat(boards.size()).isSameAs(4);
}
실행 시켰을 때 fetch join 한 Member, Comment 테이블과 함께 조회되는 것을 볼 수 있고,
조회된 데이터의 row 수는 4개인 것을 볼 수 있다.
하지만 실제 데이터베이스에 조회 쿼리를 실행해보았을때는 다른 결과를 보여주는데,
댓글 갯수만큼 1번 게시글이 3개 댓글만큼 row 수가 늘어나 있는 것을 볼 수 있다.
왜 이런 차이를 보일까?
데이터 베이스 상에서는 어떤 객체가 주가 되는지 알수 없다.
그렇기 때문에 Hibernate가 Memory 상에서,
Comment를 Board 내에 컬렉션으로 모아줬기 때문에 이처럼 차이가 발생하게 된다.
이런 이유로 ToMany에서 fetch join 사용 시,
페이징처리를 DB 상이 아닌 Application Memory 상에서 처리하게 되는 것이다.
b. ToOne Fetch Join, ToMany Batch 조회
이번엔 ToOne 관계만 Fetch Join,
ToMany는 Lazy로 조회하였을때 테스트 코드이다.
@Test
void Board_toOne_패치_toMany_배치_조회() {
//given
//when
System.out.println("Board를 조회한다.");
List<Board> boards = boardService.getAllBoardsFetchMember();
System.out.println("연관 데이터를 확인한다.");
for (Board board : boards) {
Member member = board.getMember();
List<Comment> comments = board.getComments();
System.out.println("writer : "+member.getName()+" comment size : "+comments.size());
}
//then
Assertions.assertThat(boards.size()).isSameAs(4);
}
이대로 실행하면 JPA를 공부하신 분들은 예상하다시피,
Board 내에 Comment를 조회할때 Lazy Loading으로 쿼리를 Comment 마다 실행하게 된다.
여기서 application.yaml 에 JPA 설정을 하나 추가하게 되면 다른 방식으로 쿼리를 보내게 되는데,
이 옵션은 default_batch_fetch_size 옵션이다.
default_batch_fetch_size 옵션을 사용하게 되면,
연관 조회 시 쿼리에서 in 절을 사용해 연관 데이터를 쿼리 한번에 조회하게 되는데
아래 이미지를 보면 개별 Comment 조회가 아닌 Board의 ID 값으로 한번에 조회하는 것을 볼 수 있다.
실제 안에 어떤 값들이 들어갈까?
궁금해져서 로그를 찍어보았는데, 조회된 Board ID 값과 NULL값이 들어가 있는 것을 볼수 있었다.
근데 왜 조회된 값 이외에 NULL 값이 들어간 것일까?
이 부분에 대해서는 조금 더 공부가 필요한듯 하지만,
지금까지 알아본 바로는
Hibernate의 PreparedStatement 가 미리 사용될 SQL 을 캐싱해두는데,
우리가 설정한 default_batch_fetch_size 를 보고 해당 쿼리를 미리 갯수를 정해 캐싱한 다음에
조회된 연관 ID 만큼 NULL값을 치환하고 나머지는 처음 상태로 있는 것이 아닌가 예상 된다.
몇 개의 ID가 IN 절에 들어갈지 모르는 상태에서 모든 경우의 수만큼 쿼리를 미리 만들어 둔다면 그만큼 비효율적인것도 없으니 이렇게 나오는게 아닌가 싶다.
(이부분에 대해서는 추가적으로 공부해보고 적을 내용이 있으면 추가 수정)
PreparedStatement는 아래 링크를 참고해보고 추가적으로 검색해보면 좋을 듯 하다.
StatementPreparer (Hibernate JavaDocs)
prepareStatement java.sql.PreparedStatement prepareStatement(java.lang.String sql, java.lang.String[] columnNames) Prepare an INSERT statement, specifying columns which are auto-generated values to be returned. Generated keys are accessed afterwards via
docs.jboss.org
본론으로 돌아와서 이렇게 Batch Size 를 설정하여 조회하게 되면,
사이즈를 넘지 않을 경우,
연관 데이터의 수만큼 쿼리가 나가는것이 아닌,
1+1+1 식으로 연관 Entity Object 수만큼 쿼리가 발생하게 되어 좀 더 최적화 된 조회가 이루어 질 수 있게 된다.
application.yaml 에 전체 적용되는 방식 외에도 개별적으로 적용하고자 하면 @BatchSize 어노테이션을 활용하는 방식도 있으니 이에 대해서는 검색해보는 것을 추천한다.
c. Paging Test
위에서 Batch 로 조회한 방식에 Paging을 적용하게 어떻게 될까?
@Test
void Board_toOne_패치_toMany_배치_조회_페이징_테스트() {
//given
//when
System.out.println("Board를 조회한다.");
List<Board> boards = em.createQuery("select b from Board b join fetch b.member", Board.class)
.setFirstResult(0)
.setMaxResults(10)
.getResultList();
System.out.println("연관 데이터를 확인한다.");
for (Board board : boards) {
Member member = board.getMember();
List<Comment> comments = board.getComments();
System.out.println("writer : "+member.getName()+" comment size : "+comments.size());
}
//then
Assertions.assertThat(boards.size()).isSameAs(4);
}
이전 포스팅에서는 Fetch Join 시,
쿼리 상에서 진행되는 것이 아닌 Application 내에서 작업되는 것을 볼 수 있었는데,
이렇게 게시글과 나누어 연관된 Entity를 Batch 조회를 진행하게 되면
Board 객체에 대한 테이블에 대한 페이징 작업 후,
ToMany 에 대한 연관데이터를 가져오기 때문에 데이터베이스 상에서 Paging이 가능하다.
4. 마무리
이렇게 Fetch Join 을 진행하였을때,
어떤식으로 작동하고 어떻게 사용하면 좋을지에 대해서 알아보았다.
정리하자면,
- ToOne 관계는 Fetch Join
- ToMany 관계는 Batch Size를 조절하여 조회
- ToMany 관계를 Fetch Join 할 경우는 1개의 관계에서만
정도가 될 것 같다.
Batch Size 또한 데이터베이스에서 허용 가능한 크기가 있기 때문에,
그 부분에 대해서는 검색하거나 테스트해보면서 적절한 값을 찾아가는 것도 좋아보인다.
(김영한님 강의에서는 100~1000개 사이가 적절해보인다고 한다.)
JPA가 편의성 측면에서 많은 부분을 제공하고 있지만,
그만큼 공부해야하는 부분이 많다는 점을 한번 더 느끼게 되는 것 같았다.
(잘못 사용하게 되면 서비스 장애로 직결되기 때문!)
5. GitHub
GitHub - mk1-p/jpa-sample-code: Test for Spring JPA
Test for Spring JPA. Contribute to mk1-p/jpa-sample-code development by creating an account on GitHub.
github.com
6. 참고자료
StatementPreparer (Hibernate JavaDocs)
prepareStatement java.sql.PreparedStatement prepareStatement(java.lang.String sql, java.lang.String[] columnNames) Prepare an INSERT statement, specifying columns which are auto-generated values to be returned. Generated keys are accessed afterwards via
docs.jboss.org
DBA와 개발자가 모두 행복해지는 Hibernate의 in_clause_parameter_padding 옵션 : NHN Cloud Meetup
Java ORM 기술의 표준 명세인 JPA가 소개된 지 참 오래되었지만, 국내 현실상 대규모 시스템에서 적용되어 사용된 운영 경험이 충분히 쌓이지 않고 공유되지도 않는 것 같습니다.
meetup.nhncloud.com
[Spring] default_batch_fetch_size 대로 in쿼리가 나가지 않는 이유
개발을 하다가 이상한 상황을 봤다. default_batch_fetch_size을 1000으로 설정했음에도 불구하고, in절이 62개의 리스트로만 가져오고 나눠서 가져오는 것이다. 버그인가 싶었는데 구글링을 해보니 이유
toongri.tistory.com
'DEV > Spring' 카테고리의 다른 글
[JPA] QueryDSL 설정하기 (0) | 2023.10.16 |
---|---|
[JPA] JoinColumns 조회 방식 (0) | 2023.10.12 |
[JPA] Fetch Join (0) | 2023.10.05 |
[Spring] 권한 순서 정하기 (RoleHierarchy : 역할계층) (0) | 2023.09.17 |
[Spring] API Path Prefix 설정하기 (0) | 2023.09.14 |