해결된 질문
23.11.02 12:05 작성
·
553
·
수정됨
0
[질문 템플릿]
1. 강의 내용과 관련된 질문인가요? (예)
2. 인프런의 질문 게시판과 자주 하는 질문에 없는 내용인가요? (예)
3. 질문 잘하기 메뉴얼을 읽어보셨나요? (예)
[질문 내용]
코드는 마지막에 첨부되어 있습니다.
게시글 엔티티 Post가 게시글에 달린 댓글 엔티티 Comment의 컬렉션을 프로퍼티로 갖습니다. 댓글 엔티티 Comment는 COMMENT, REPLY 타입으로 구분되며, 프로퍼티로 parentComment와 replies를 갖습니다. COMMENT 타입은 parentComment == null이며, REPLY 타입은 replies == null입니다. Post.comments는 Comment의 타입을 고려하지 않고 모두 갖고 있습니다.
위 상황에서 게시물 상세 정보를 가져올 때, comments를 fetch join하여 Post 엔티티를 가져와 댓글들을 타입 계층에 따라 분리하는 작업을 합니다. 이때 REPLY 타입의 replies에 접근하지 않습니다. COMMENT 타입의 replies에는 접근하며 해당 replies는 모두 같은 게시물에 속해있습니다.
예상했던 쿼리의 개수는 1 혹은 (1 + COMMENT 타입 수) 였습니다. 애초에 둘 중 무엇인지 궁금해서 일을 진행했었습니다.
테스트를 실행해보면, post.comments의 요소에 처음 접근할 때 REPLY 타입을 가져오는 쿼리가 발생합니다. 그런데 이후에 다른 COMMENT 타입에 접근할 때는 REPLY를 가져오는 쿼리가 발생하지 않습니다. 만약 Post의 COMMENT 타입 Comment.replies에 접근할 때 쿼리가 발생하는 거라면 COMMENT 타입의 개수만큼 발생해야 하는게 아닌가요? 제가 무엇을 놓치고 있는지 궁금합니다.
논외로, Spring data JPA의 @Query를 통해 작성한 단순한 정적 쿼리가 계속 반복된다면 QeuryDSL로 옮겨 반복을 줄이는게 합리적일까요? 아니면 spring data jpa의 편리함을 유지하는게 합리적인가요?
감사합니다.
public class Post extends BaseEntity {
public static final int TITLE_MAX_LENGTH = 50;
public static final int CONTENT_MAX_LENGTH = 10000;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "post_id", nullable = false, updatable = false)
private Long id;
@Column(name = "title", nullable = false, length = TITLE_MAX_LENGTH, updatable = false)
private String title;
@Column(name = "content", nullable = false, length = CONTENT_MAX_LENGTH)
private String content;
@Column(name = "hits", nullable = false)
private Long hits;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "writer_id")
private Member writer;
@OneToMany(fetch = FetchType.LAZY, mappedBy = "post", orphanRemoval = true)
@ToString.Exclude
private List<Comment> comments = new ArrayList<>();
}
public class Comment extends BaseDeleteEntity {
public static final int CONTENT_LENGTH = 1000;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "comment_id", nullable = false)
private Long id;
@Column(name = "content", nullable = false, length = CONTENT_LENGTH)
private String content;
@Column(name = "type", nullable = false, updatable = false)
private CommentType type;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id", nullable = false, updatable = false)
private Post post;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "writer")
private Member writer;
@OneToMany(fetch = FetchType.LAZY, mappedBy = "parent", orphanRemoval = true)
private List<Comment> replies = new ArrayList<>();
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent_comment_id", updatable = false)
@ToString.Exclude
private Comment parent;
}
@Query(value = """
SELECT p FROM Post p
LEFT JOIN FETCH p.writer AS w
LEFT JOIN FETCH w.profile
LEFT JOIN FETCH p.comments
WHERE p.id = :postId
"""
)
Optional<Post> findPostById(@Param("postId") final Long id);
@Autowired
EntityManager em;
@Autowired
PostRepository postRepository;
@Test
public void 정상작동테스트_추가적인_쿼리_발생_x() throws Exception {
//given
Post post = PostTest.create("username", "nickname");
Member member = post.getWriter();
em.persist(member);
em.persist(post);
Long postId = post.getId();
int commentCount = 10;
int replyCount = 2;
createComment(post, member, commentCount, replyCount);
em.flush();
em.clear();
//when
int totalCommentCount = commentCount * (replyCount + 1);
Post findPost = postRepository.findPostById(postId).get();
System.out.println();
//then
for (Comment comment : findPost.getComments().stream().filter(Comment::isCommentType).toList()) { // 이때 REPLY 타입을 조회하는 쿼리가 1회 발
System.out.println("comment = " + comment);
System.out.println("comment.getReplies().get(0) = " + comment.getReplies().get(0));
}
Assertions.assertThat(post.getComments().size()).isEqualTo(totalCommentCount);
}
[테스트 실행 시 쿼리]
2023-11-02T11:45:00.154+09:00 DEBUG 51844 --- [ main] org.hibernate.SQL :
select
p1_0.post_id,
c1_0.post_id,
c1_0.comment_id,
c1_0.content,
c1_0.created_by,
c1_0.created_date,
c1_0.deleted_date,
c1_0.is_deleted,
c1_0.last_modified_by,
c1_0.last_modified_date,
c1_0.parent_comment_id,
c1_0.type,
c1_0.writer,
p1_0.content,
p1_0.created_by,
p1_0.created_date,
p1_0.hits,
p1_0.last_modified_by,
p1_0.last_modified_date,
p1_0.title,
w1_0.member_id,
w1_0.created_by,
w1_0.created_date,
w1_0.last_modified_by,
w1_0.last_modified_date,
w1_0.login_id,
w1_0.login_type,
w1_0.profile_id,
p2_0.profile_id,
p2_0.created_by,
p2_0.created_date,
p2_0.last_modified_by,
p2_0.last_modified_date,
p2_0.nickname,
w1_0.user_role,
w1_0.social_login_id,
w1_0.username
from
post p1_0
left join
member w1_0
on w1_0.member_id=p1_0.writer_id
left join
profile p2_0
on p2_0.profile_id=w1_0.profile_id
left join
comment c1_0
on p1_0.post_id=c1_0.post_id
where
p1_0.post_id=?
2023-11-02T11:45:00.162+09:00 INFO 51844 --- [ main] p6spy
2023-11-02T11:45:01.221+09:00 DEBUG 51844 --- [ main] org.hibernate.SQL :
select
r1_0.parent_comment_id,
r1_0.comment_id,
r1_0.content,
r1_0.created_by,
r1_0.created_date,
r1_0.deleted_date,
r1_0.is_deleted,
r1_0.last_modified_by,
r1_0.last_modified_date,
r1_0.post_id,
r1_0.type,
r1_0.writer
from
comment r1_0
where
array_contains(?,r1_0.parent_comment_id)
[반복되는 spring data jpa 쿼리]
public interface PostRepository extends JpaRepository<Post, Long>, PostQueryRepository {
/**
* Post 반환 시 Member, Profile을 fetch join한다. ~ToOne 매핑관계에 대한 fetch join은 별명을 사용할 수 있고, 연계하여 fetch
* join할 수 있다.
*
* @param id must not be {@literal null}.
* @return
*/
@Query(value = """
SELECT p FROM Post p
LEFT JOIN FETCH p.writer AS w
LEFT JOIN FETCH w.profile
LEFT JOIN FETCH p.comments
WHERE p.id = :postId
"""
)
Optional<Post> findPostById(@Param("postId") final Long id);
/**
* Post를 페이징 처리하여 Page<Post>로 반환한다. 이때 Member와 Profile을 fetch join한다.
*
* @param pageable the pageable to request a paged result,
* can be {@link Pageable#unpaged()},
* must not be {@literal null}.
*/
@Query(value = """
SELECT p FROM Post p
LEFT JOIN FETCH p.writer AS w
LEFT JOIN FETCH w.profile
""",
countQuery = "SELECT count(p) FROM Post p"
)
@Override
Page<Post> findAll(final Pageable pageable);
/**
* WriterId가 memberId와 같은 Post를 페이징 처리하여 Page<Post>로 반환한다. 이때 Member와 Profile을 fetch join한다.
*
* @param writerId writerId가 일치하는 Post들을 반환한다.
* @param pageable 페이징 정보
*/
@Query(value = """
SELECT p FROM Post p
LEFT JOIN FETCH p.writer AS w
LEFT JOIN FETCH w.profile
WHERE w.id = :writerId
""",
countQuery = "SELECT count(p) FROM Post p WHERE p.writer.id = :writerId"
)
Page<Post> findAllByWriterId(@Param("writerId") final Long writerId, final Pageable pageable);
}
답변 1
0
안녕하세요 김민규 님, 인프런 AI 인턴이에요.
간단하게 설명해 드리자면, FETCH JOIN 쿼리를 실행할 때는 일반적으로 엔티티 사이의 관계에 따라 JOIN이 발생합니다. 하지만 해당 쿼리에서는 REPLY 타입의 replies에는 접근하지 않으며, COMMENT 타입의 replies에만 접근한다고 설명하셨습니다.
실제 테스트 결과를 보면 처음 post.comments 컬렉션에 접근할 때 REPLY 타입의 쿼리가 발생하였다는 것을 확인하였습니다. 그러나 그 이후에 다른 COMMENT 타입에 접근할 때는 REPLY를 가져오는 쿼리가 발생하지 않았습니다.
COMMENT 타입의 replies의 경우 모두 같은 게시물에 속해 있기 때문에 처음에 한번만 조회하여 재사용되는 것으로 보입니다. 따라서 예상하셨던 쿼리 개수는 (1 + COMMENT 타입의 개수)가 아니라 1과 같습니다.
또한, 귀하께서 Spring Data JPA의 @Query를 통해 작성한 단순한 정적 쿼리가 계속 반복된다면 QueryDSL로 변경해주는 것이 반복을 줄일 수 있는 합리적인 접근일 수 있습니다. QueryDSL은 동적 쿼리 작성에도 강력한 기능을 제공해주기 때문에 유용하게 활용될 수 있습니다. 하지만 Spring Data JPA는 CRUD 메소드를 자동으로 제공하고 메소드명으로 쿼리를 생성하는 등 개발 편의성을 제공하기 때문에 편리한 접근 방식이기도 합니다.
다시 한번, 질문에 대한 답변을 드려서 감사합니다. 추가적인 문의가 있다면 언제든지 물어보세요!
2023. 11. 04. 14:11
고마워요! 나도 인턴하고 싶다