본문 바로가기
Spring

JPA+QueryDsl 게시판 CRUD 구현(2)

by 하르싼 2023. 3. 9.
반응형

JPA+QueryDsl 게시판 CRUD 구현(1)

목적

  1. 신규 프로젝트에서 사용 되었던 JPA, QueryDsl 을 정리
  2. Spring Data Jpa 로 간단한 게시판 구현을 통해 정리
  3. User, Post Entity 에서 Fetch LAZY, EAGER 변경해가면서 이해
  4. QueryDsl 로 동적쿼리 작성

예제소스

https://github.com/devHjlee/devHjBlog/tree/main/springJpaCrud

QueryDsl 이란?

정적타입을 이용하여 SQL 과 같은 쿼리를 생성할 수 있도록 제공하는 오픈소스(JPQL 을 만들어주는 builder 최종적으로는 JPQL 로 변환)
예시 : 학교 1:N 학급 1:N 학생 연관관계를 갖고있을때 JPA만을 통해 조회를 진행하게되면 학교에 해당하는 학생을 찾으려면 학교를 통해 학급을 조회하게되고 다시 학급을 통해 학생을 조회하기때문에 수만흔 select 쿼라가 발생할수도있다.
또한 연관관계가 없어도 join을 통해 쿼리를 작성 할 수 있고 DTO를 생성하여 원하는 레코드만 반환할 수 있다.

JPQL QueryDsl 비교

JPQL

  • JPQL 은 문자(String)이며, Type-Check가 불가능.
  • 해당 로직 실행 전까지 작동여부 확인이 불가
  • 파라미터 바인딩은 문자열로 입력하기에 런타임 시점(실행시점오류)
    QueryDSL
  • 문자가 아닌 코드로 작성
  • 파라미터바인딩 자동(sqlinjection에대해 안전)
  • 컴파일 시점 문법 오류 발견이 가능
  • 코드 자동 완성
  • 동적쿼리 가능
  • JPQL 단점 보완

사용시 주의점

1차캐시 이슈

JPA 에서 findBy 는 영속성 컨텍스트를 먼저 조회한 후, 없으면 데이터베이스를 조회하며 식별자가 아니면 1차 캐시를 사용하지 않는다. 식별자가 아닌 값으로 조회했을 때, 중복된 값을 조회해버리면 상당히 복잡해지기 때문
JPQL 의 경우 데이터베이스를 먼저 조회한 후, 영속성 컨텍스트에 값이 들어있으면 버리고 없으면 저장.
QueryDsl 은 JPQL 로 변화되며 조회할 때, 영속성 컨텍스트를 먼저 조회하지 않고, 데이터베이스를 먼저 조회.
데이터베이스에서 조회해온 결과를 영속성 컨텍스트에 넣으려고하는데 이때 영속성 컨텍스트에 이미 해당 데이터가 있다면 데이터베이스를 통해 가져온 결과는 버린다.
이렇게 하는 이유는 영속성에도 값과, 데이터베이스에 읽어온 값이 충돌하며 이는 DIRTY READ 가 발생한는 것이며, (영속성 컨텍스트에 있는 값을 변경중일 수 있으니) 데이터베이스에서 조회한 값을 버림으로써 NON-REPEATABLE READ 발생 도 막는 것
그렇기에 JPA 내부에서 위와 같이 동작함으로 애플리케이션 레벨에서 REPEATABLE READ 레벨로 동작하게 되므로 이와같은 문제를 해결하기 위해서 데이터를 조회하기 전에 영속성 컨텍스트를 초기화하는 방법으로 해결이 가능하다.

동시성 문제

동시성 문제는 JPAQueryFactory를 생성할 때 제공하는 EntityManager(em)에 달려있다.
스프링 프레임워크는 여러 쓰레드에서 동시에 같은 EntityManager에 접근해도, 트랜잭션 마다 별도의 영속성 컨텍스트를 제공하기 때문에, 동시성 문제는 걱정하지 않아도 된다.

기존 예시 소스중 Post(게시글)를 Querydsl 관련 변경

1.Querydsl Config

@Configuration
public class QuerydslConfig {

@PersistenceContext
private EntityManager entityManager;

@Bean
public JPAQueryFactory jpaQueryFactory(){
  return new JPAQueryFactory(entityManager);
}
}

2.Post Repository

  • https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.custom-implementations
  • PostRepositoryCustom,PostRepositoryImpl 추가
    • QType 선언 방법
      • QPost p = new QPost("p");
      • QPost qpost = QPost.post;
      • import static com.springjpacrud.domain.QPost.post; >>>> 권장
    • 결과 조회 메소드
      • fetch() : 리스트 조회, 데이터 없으면 빈 리스트 반환
      • fetchOne() : 단 건 조회
      • 결과가 없으면 : null
      • 결과가 둘 이상이면 : com.querydsl.core.NonUniqueResultException
      • fetchFirst() : limit(1).fetchOne()
      • fetchResults() : 페이징 정보 포함, total count 쿼리 추가 실행 -- deprecated
      • fetchCount() : count 쿼리로 변경해서 count 수 조회 -- deprecated
        실무에서 페이징 쿼리를 작성할 때, 데이터를 조회하는 쿼리는 여러 테이블을 조인해야 하지만, count 쿼리는 조인이 필요 없는 경우도 있다. 그런데 이렇게 자동화된 count 쿼리는 원본 쿼리와 같이 모두 조인을 해버리기 때문에 성능이 안나올 수 있다.
        count 쿼리에 조인이 필요없는 성능 최적화가 필요하다면, count 전용 쿼리를 별도로 작성해야 한다.
public interface PostRepositoryCustom {
    List<Post> getPosts();
    List<Post> getPostsFetchJoin();
    List<Post> getPostsNoRelation();
    List<PostUserDTO> getDto();
}

import static com.springjpacrud.domain.QPost.post;
import static com.springjpacrud.domain.QUser.user;

@RequiredArgsConstructor
public class PostRepositoryImpl implements PostRepositoryCustom {
    private final JPAQueryFactory jpaQueryFactory;

    private final JPAQueryFactory jpaQueryFactory;

    @Override
    public List<Post> getPosts() {
        return jpaQueryFactory
                .selectFrom(post)
                .join(post.user, user)
                .fetch();
    }

    @Override
    public List<Post> getPostsFetchJoin() {
        return jpaQueryFactory
                .selectFrom(post)
                .join(post.user, user)
                .fetchJoin()
                .fetch();
    }

    @Override
    public List<Post> getPostsNoRelation() {
        return jpaQueryFactory
                .selectFrom(post)
                .join(user).on(post.user.id.eq(user.id))
                .fetch();
    }

    @Override
    public List<PostUserDTO> getDto() {
        return jpaQueryFactory
                .select(new QPostUserDTO(post.title,post.content, user.email, user.userName))
                .from(post)
                .join(user).on(post.user.id.eq(user.id))
                .fetch();
    }
}
  • PostRepository 변경
public interface PostRepository extends JpaRepository<Post,Long>, PostRepositoryCustom {
    List<Post> findPostByTitleOrContent(String content, String title);
    Post findPostById(Long id);
}
  • Test
    @Test
    void getPosts(){
        boolean loaded;
        //연관관계 Entity 조회
        //JpaRepository
        List<Post> posts = postRepository.findAll();
        loaded = emf.getPersistenceUnitUtil().isLoaded(posts.get(0).getUser());
        assertThat(false).isEqualTo(loaded);

        //join(1)
        List<Post> posts2 = postService.getPosts();
        loaded = emf.getPersistenceUnitUtil().isLoaded(posts2.get(0).getUser());
        assertThat(false).isEqualTo(loaded);

        posts2.get(0).getUser().toString();//(1.1)
        loaded = emf.getPersistenceUnitUtil().isLoaded(posts2.get(0).getUser());
        assertThat(true).isEqualTo(loaded);

        //FetchJoin(2)
        List<Post> posts3 = postService.getPostsFetchJoin();
        loaded = emf.getPersistenceUnitUtil().isLoaded(posts3.get(0).getUser());
        assertThat(true).isEqualTo(loaded);

        //연관관계 없을시 join on(3)
        List<Post> posts4 = postService.getPostsNoRelation();
        loaded = emf.getPersistenceUnitUtil().isLoaded(posts4.get(0).getUser());
        assertThat(true).isEqualTo(loaded);

        //@QueryProjection DTO(4)
        List<PostUserDTO> posts5 = postService.getDto();
        loaded = emf.getPersistenceUnitUtil().isLoaded(posts5.get(0).getUserName());
        assertThat(true).isEqualTo(loaded);
    }
  • (1) : 연관관계가 있는 엔티티와 Join을 하여 사용할 수 있고 연관관계에 따라 on이 붙는다.
  • (1.1) : 이때 Lazy로 설정 해놨기에 User는 Proxy 객체 상태에서 직접 접근시 데이터베이스를 통해 조회한다.
select
post0_.post_id as post_id1_0_,
post0_.content as content2_0_,
post0_.email as email3_0_,
post0_.title as title4_0_,
post0_.user_no as user_no5_0_
from
post post0_
inner join
user user1_
on post0_.user_no=user1_.user_no   

select
    user0_.user_no as user_no1_2_0_,
    user0_.email as email2_2_0_,
    user0_.password as password3_2_0_,
    user0_.user_name as user_nam4_2_0_ 
from
    user user0_ 
where
    user0_.user_no=1   
  • (2) : fetchJoin 사용시 User를 프록시객체가 아닌 실제 객체로 가져온다.
    select
        post0_.post_id as post_id1_0_0_,
        user1_.user_no as user_no1_2_1_,
        post0_.content as content2_0_0_,
        post0_.email as email3_0_0_,
        post0_.title as title4_0_0_,
        post0_.user_no as user_no5_0_0_,
        user1_.email as email2_2_1_,
        user1_.password as password3_2_1_,
        user1_.user_name as user_nam4_2_1_ 
    from
        post post0_ 
    inner join
        user user1_ 
            on post0_.user_no=user1_.user_no
  • (3) : Join on을 이용하여 연관관계가 없는 테이블과 Join하여 사용할 수 있다.
    select
        post0_.post_id as post_id1_0_,
        post0_.content as content2_0_,
        post0_.email as email3_0_,
        post0_.title as title4_0_,
        post0_.user_no as user_no5_0_ 
    from
        post post0_ 
    inner join
        user user1_ 
            on (
                post0_.user_no=user1_.user_no
            )
  • (4) : QueryProjection 을 통해 QDTO 가 생성되고 리턴받을 수 있다.
    • 컴파일러 타입을 체크할 수 있어 안전하지만 QueryDsl에 의존성이 심해진다. 귀찮다.
    select
        post0_.title as col_0_0_,
        post0_.content as col_1_0_,
        user1_.email as col_2_0_,
        user1_.user_name as col_3_0_ 
    from
        post post0_ 
    inner join
        user user1_ 
            on (
                post0_.user_no=user1_.user_no
            )
반응형

'Spring' 카테고리의 다른 글

Spring ControllerAdvice 활용  (0) 2023.05.04
Spring Event 활용  (0) 2023.03.21
JPA Batch JDBC Batch  (0) 2023.03.03
JPA+QueryDsl 게시판 CRUD 구현(1)  (0) 2023.02.15
Srping Boot Quartz DB Cluster,Log 구현  (0) 2023.01.18

댓글