생각정리/Spring

[JPA] 데이터 조회

생각중임 2024. 4. 28. 00:44

JPA, Spring Data JPA, QueryDSL을 동일한 조회 조건으로 사용해 보면서 차이점을 확인해 보면서 다양하게 숙달해 보기

임시 데이터

  • 유저 게시글 양방향 매핑
  • 게시글 댓글 단방향 매핑
  • 게시글 카테고리 단방향 매핑
  • 댓글 유저 단방향 매핑

유저 3명

게시글 카테고리 2개

유저당 게시글 5개

게시글당 댓글 유저당 3개씩

1. 유저가 작성한 게시글 조회

마이페이지등에서 자신이 작성한 게시글들을 확인한다.

1-1. 기본 조회 - 엔티티를 이용한 DTO 생성

게시글 1회, 유저 1회, 카테고리 N회의 쿼리를 보낸다.

// JPA
public List<PostResponseDto> findUserPost(String userId) {
    return em.createQuery("select new study.querydsl.dto.PostResponseDto(p) " +
                    "from Post p " +
                    "where p.user.userId = :userId", PostResponseDto.class)
            .setParameter("userId", userId)
            .getResultList();
}
    
// Spring Data JPA
@Query("select new study.querydsl.dto.PostResponseDto(p) from Post p where p.user.userId = :userId")
List<PostResponseDto> findUserPost(@Param("userId") String userId);
    
// QueryDSL
public List<PostResponseDto> findUserPost(String userId) {
    return queryFactory
            .select(Projections.constructor(PostResponseDto.class, p))
            .from(p)
            .where(p.user.userId.eq(userId))
            .fetch();
}

1-2. 페치 조인을 이용한 쿼리 최적화 - 엔티티를 이용한 DTO 생성

1회 쿼리로 모든 데이터를 가져온다.

하지만, 게시글, 유저, 카테고리 정보를 한 번에 조회하기 때문에 데이터의 크기가 커진다.

// JPA
public List<PostResponseDto> findUserPost(String userId) {
    return em.createQuery("select new study.querydsl.dto.PostResponseDto(p) " +
                    "from Post p " +
                    "join fetch p.user u " +
                    "join fetch p.category c " +
                    "where p.user.userId = :userId", PostResponseDto.class)
            .setParameter("userId", userId)
            .getResultList();
}
    
// Spring Data JPA
@Query("select new study.querydsl.dto.PostResponseDto(p) from Post p join fetch p.user u join fetch p.category where p.user.userId = :userId")
List<PostResponseDto> findUserPost(@Param("userId") String userId);
    
// QueryDSL
public List<PostResponseDto> findUserPost(String userId) {
    return queryFactory
            .select(Projections.constructor(PostResponseDto.class, p))
            .from(p)
            .join(p.user, u).fetchJoin()
            .join(p.category, c).fetchJoin()
            .where(p.user.userId.eq(userId))
            .fetch();
}

1-3. 외부 조인을 이용한  쿼리 최적화 - 필드를 이용한 DTO

1회 쿼리로 모든 데이터를 가져온다.

DTO에 직접 필요한 필드만 추가한 생성자가 필요하고 코드가 길어지지만, 필요한 데이터만 조회가 가능해서 데이터 크기도 정량화할 수 있고 코드도 시각적 측면에서는 필요한 필드를 알 수 있어 가독성이 올라갈 수 있다.

// JPA
public List<PostResponseDto> findUserPost(String userId) {
    return em.createQuery("select new study.querydsl.dto.PostResponseDto(" +
                    "p.id," +
                    "p.title," +
                    "u.userId," +
                    "p.contents," +
                    "p.postLike," +
                    "p.createdTime," +
                    "p.modifiedTime," +
                    "c.name) " +
            "from Post p " +
            "left join p.user u " +
            "left join p.category c " +
            "where p.user.userId = :userId", PostResponseDto.class)
            .setParameter("userId", userId)
            .getResultList();
}
    
// Spring Data JPA
@Query("select new study.querydsl.dto.PostResponseDto(" +
        "p.id, p.title, u.userId, p.contents, p.postLike, p.createdTime, p.modifiedTime, c.name) " +
        "from Post p left join p.user u left join p.category c where p.user.userId = :userId")
List<PostResponseDto> findUserPost(@Param("userId") String userId);
    
// QueryDSL
public List<PostResponseDto> findUserPost(String userId) {
    return queryFactory
            .select(Projections.constructor(PostResponseDto.class,
                    p.id,
                    p.title,
                    u.userId,
                    p.contents,
                    p.postLike,
                    p.createdTime,
                    p.modifiedTime,
                    c.name))
            .from(p)
            .leftJoin(p.user, u)
            .leftJoin(p.category, c)
            .where(p.user.userId.eq(userId))
            .fetch();
}

1-4. 발생하는 쿼리 차이

JPA와 Spring Data JPA는 차이가 없다. 당연하게도 Spring Data JPA가 JPA를 자동으로 구현을 하는 방식이기 때문이다.

반면, QueryDSL은 다른 JPQL 빌더이기 때문에 쿼리 방식이 다른 걸 볼 수 있다.

// JPA
select
    new study.querydsl.dto.PostResponseDto(p.id, p.title, u.userId, p.contents, p.postLike, p.createdTime, p.modifiedTime, c.name) 
from
    Post p 
left join
    p.user u 
left join
    p.category c 
where
    p.user.userId = :userId

// Spring Data JPA
select
    new study.querydsl.dto.PostResponseDto(p.id, p.title, u.userId, p.contents, p.postLike, p.createdTime, p.modifiedTime, c.name) 
from
    Post p 
left join
    p.user u 
left join
    p.category c 
where
    p.user.userId = :userId 
            
// QueryDSL      
select
    p.id,
    p.title,
    u.userId,
    p.contents,
    p.postLike,
    p.createdTime,
    p.modifiedTime,
    c.name 
from
    Post p   
left join
    p.user as u   
left join
    p.category as c 
where
    p.user.userId = ?1

2. 게시글 상세 조회

게시글의 상세 정보를 조회하고 해당 게시글의 댓글 정보를 가지고 온다.

2-1. 기본 조회 - 엔티티를 이용한 DTO 생성

게시글 - 댓글이 단방향 매핑으로 되어 엔티티를 조회는 상관이 없었지만, DTO로 생성하는 방식으로 하기에는 게시글과 댓글을 따로 조회해 DTO로 생성을 하고 댓글 DTO를 게시글 DTO에 담아주어야 하는데, 해당 게시글의 댓글을 조회할 때, 단방향 매핑이기 때문에 게시글의 정보를 알 수가 없어 일반적으로 조회가 불가능했다.

생각을 했을 땐, 양방향 매핑이었으면 댓글의 개수만큼 조회를 하면서 N + 1 문제가 발생하는 것을 원했는데, 단방향 매핑을 해서 다른 방법으로 조회를 하면서 게시글 상세 조회를 하면서 게시글 1회, 유저 1회, 카테고리 1회 댓글 조회를 게시글 1회, 댓글을 쓴 유저 N회의 쿼리가 발생하면서 댓글의 유저정보를 가져오면서만 N + 1문제가 발생했다.

// JPA
public List<PostDetailResponseDto> findPost(Long postId) {
    return em.createQuery("select new study.querydsl.dto.PostDetailResponseDto(p) " +
                    "from Post p " +
                    "where p.id = :postId", PostDetailResponseDto.class)
            .setParameter("postId", postId)
            .getResultList();
}

public List<CommentResponseDto> findComment(Long postId) {
    return em.createQuery("select new study.querydsl.dto.CommentResponseDto(c) " +
                    "from Post p " +
                    "join p.commentList c " +
                    "where p.id = :postId", CommentResponseDto.class)
            .setParameter("postId", postId)
            .getResultList();
}
    
// Spring Data JPA
@Query("select new study.querydsl.dto.PostDetailResponseDto(p) from Post p where p.id = :postId")
List<PostDetailResponseDto> findPost(@Param("postId") Long postId);

@Query("select new study.querydsl.dto.CommentResponseDto(c) from Post p join p.commentList c where p.id = :postId")
List<CommentResponseDto> findComment(@Param("postId") Long postId);
    
// QueryDSL
public List<PostDetailResponseDto> findPost(Long postId) {
    return queryFactory
            .select(Projections.constructor(PostDetailResponseDto.class, post))
            .from(post)
            .where(post.id.eq(postId))
            .fetch();
}

public List<CommentResponseDto> findComment(Long postId) {
    return queryFactory
            .select(Projections.constructor(CommentResponseDto.class, comment1))
            .from(post)
            .join(post.commentList, comment1)
            .where(post.id.eq(postId))
            .fetch();
}

1-2. 페치 조인을 이용한 쿼리 최적화 - 엔티티를 이용한 DTO 생성

유저가 작성한 게시글 조회와 동일하게 쿼리는 1번 조회에 1회로 줄어들지만, 데이터 크기가 커지는 문제가 있다.

// JPA
public List<PostDetailResponseDto> findPost(Long postId) {
    return em.createQuery("select new study.querydsl.dto.PostDetailResponseDto(p) " +
                    "from Post p " +
                    "join fetch p.user u " +
                    "join fetch p.category c " +
                    "where p.id = :postId", PostDetailResponseDto.class)
            .setParameter("postId", postId)
            .getResultList();
}

public List<CommentResponseDto> findComment(Long postId) {
    return em.createQuery("select new study.querydsl.dto.CommentResponseDto(c) " +
                    "from Post p " +
                    "join p.commentList c " +
                    "join fetch c.user u " +
                    "where p.id = :postId", CommentResponseDto.class)
            .setParameter("postId", postId)
            .getResultList();
}
    
// Spring Data JPA
@Query("select new study.querydsl.dto.PostDetailResponseDto(p) from Post p join fetch p.user u join fetch p.category c where p.id = :postId")
List<PostDetailResponseDto> findPost(@Param("postId") Long postId);

@Query("select new study.querydsl.dto.CommentResponseDto(c) from Post p join p.commentList c join fetch c.user u where p.id = :postId")
List<CommentResponseDto> findComment(@Param("postId") Long postId);
    
// QueryDSL
public List<PostDetailResponseDto> findPost(Long postId) {
    return queryFactory
            .select(Projections.constructor(PostDetailResponseDto.class, post))
            .from(post)
            .join(post.user, user).fetchJoin()
            .join(post.category, category).fetchJoin()
            .where(post.id.eq(postId))
            .fetch();
}

public List<CommentResponseDto> findComment(Long postId) {
    return queryFactory
            .select(Projections.constructor(CommentResponseDto.class, comment1))
            .from(post)
            .join(post.commentList, comment1)
            .join(comment1.user).fetchJoin()
            .where(post.id.eq(postId))
            .fetch();
}

1-3. 외부 조인을 이용한  쿼리 최적화 - 필드를 이용한 DTO

해당 방법도 위와 같은 방식으로 해결이 가능하다.

// JPA
public List<PostDetailResponseDto> findPost(Long postId) {
    return em.createQuery("select new study.querydsl.dto.PostDetailResponseDto(" +
                    "p.id," +
                    "p.title," +
                    "u.userId," +
                    "p.contents," +
                    "p.postLike," +
                    "p.createdTime," +
                    "p.modifiedTime," +
                    "c.name) " +
                    "from Post p " +
                    "left join p.user u " +
                    "left join p.category c " +
                    "where p.id = :postId", PostDetailResponseDto.class)
            .setParameter("postId", postId)
            .getResultList();
}

public List<CommentResponseDto> findComment(Long postId) {
    return em.createQuery("select new study.querydsl.dto.CommentResponseDto(" +
                    "c.id," +
                    "c.comment," +
                    "u.userId," +
                    "c.commentLike," +
                    "c.createdTime," +
                    "c.modifiedTime) " +
                    "from Post p " +
                    "join p.commentList c " +
                    "left join c.user u " +
                    "where p.id = :postId", CommentResponseDto.class)
            .setParameter("postId", postId)
            .getResultList();
}
    
// Spring Data JPA
@Query("select new study.querydsl.dto.PostDetailResponseDto(" +
        "p.id, p.title, u.userId, p.contents, p.postLike, p.createdTime, p.modifiedTime, c.name) " +
        "from Post p left join p.user u left join p.category c where p.id = :postId")
List<PostDetailResponseDto> findPost(@Param("postId") Long postId);

@Query("select new study.querydsl.dto.CommentResponseDto(" +
        "c.id, c.comment, u.userId, c.commentLike, c.createdTime, c.modifiedTime) " +
        "from Post p join p.commentList c left join c.user u where p.id = :postId")
List<CommentResponseDto> findComment(@Param("postId") Long postId);
    
// QueryDSL
public List<PostDetailResponseDto> findPost(Long postId) {
    return queryFactory
            .select(Projections.constructor(PostDetailResponseDto.class,
                    post.id,
                    post.title,
                    user.userId,
                    post.contents,
                    post.postLike,
                    post.createdTime,
                    post.modifiedTime,
                    category.name))
            .from(post)
            .leftJoin(post.user, user)
            .leftJoin(post.category, category)
            .where(post.id.eq(postId))
            .fetch();
}

public List<CommentResponseDto> findComment(Long postId) {
    return queryFactory
            .select(Projections.constructor(CommentResponseDto.class,
                    comment1.id,
                    comment1.comment,
                    user.userId,
                    comment1.commentLike,
                    comment1.createdTime,
                    comment1.modifiedTime))
            .from(post)
            .join(post.commentList, comment1)
            .leftJoin(comment1.user, user)
            .where(post.id.eq(postId))
            .fetch();
}

1-4. 게시글과 댓글을 동시 DTO로 해결할 수 있는가? 

DTO변환 중 안에서 추가적으로 댓글도 DTO로 바로 넣으려고 해 보았지만 해당 방법으로는 불가능했다.

생성자안에 List <CommentResponseDto>로 값이 들어있다 하더라도 쿼리 안에서는 new 형태의 새로운 생성자가 필요하기 때문에 해당 필드를 찾을 수 없어 오류가 나는 듯하다.

DTO안에 DTO가 있어야 할 경우는 어쩔 수 없이 조회를 따로 한 후에 합치는 수밖에 없겠다.