개발/java

QueryDsl SpringBoot JPA 레포지토리 만들기

코딩하는꽃개 2023. 5. 19. 22:37
반응형

querydsl은 JPA 쿼리를 동적 코드로써 작성할 수 있도록 도와주는 ORM 프레임워크입니다.

 

JPA에서 기본적으로 JPQL을 사용하여 레포지토리를 정의할 수 있습니다.

@Repository
public class ProductRepositoryImpl implements ProductRepository {
    @PersistenceContext
    private EntityManager entityManager;

    @Override
    public List<Product> findByName(String name) {
        String jpql = "SELECT p FROM Product p WHERE p.name = :name";
        return entityManager.createQuery(jpql, Product.class)
                .setParameter("name", name)
                .getResultList();
    }
}

이 경우 SQL 쿼리를 큰 수정 없이 JPA에 적용할 수 있다는 장점이 있지만 오타 및 복잡한 조건의 적용 등으로 인해 유지보수성이 떨어지게 됩니다.

ORM을 좀더 객체 지향적으로 이용하기 위해서 우리는 동적 쿼리 생성을 할 수 있게 querydsl을 사용할 수 있습니다.

 

먼저 JpaRepository를 상속받아 레포지토리를 선언합니다.

 

public interface ResourceRepository extends JpaRepository<ResourceEntity, Integer> {

}

 

단 위 형태로 선언하게 되면 JpaRepository가 제공하는 기본 레포지토리만 사용할 수 있습니다. (findById 등)

우리는 개발을 진행하면서 여러 커스텀한 레포지토리의 정의가 필요하므로 커스텀한 레포지토리를 정의해 줄 수 있어야 합니다.

 

public interface ResourceRepository extends JpaRepository<ResourceEntity, Integer>, ResourceRepositoryDsl {

}
public interface ResourceRepositoryDsl {
    // 리소스 조회
    Optional<ResourceEntity> findByUUID(String uuid);

    // 리소스 조회
    Optional<ResourceDetailDTO> findDetail(int resourceId);

    // 리소스 목록 조회
    Page<ResourceListDTO> findAllResources(Pageable pageable);

    // 리소스 목록 조회
    List<ResourceListDTO> findAllResources();
}

 

위 코드 처럼 queydsl용 레포지토리 인터페이스를 정의하고 기존 레포지토리에서 상속받도록 합니다.

이후 인터페이스에 정의한 레포지토리 메서드들을 구현해 줍니다.

 

@Repository
public class ResourceRepositoryImpl extends QuerydslRepositorySupport implements ResourceRepositoryDsl {
    private final JPAQueryFactory queryFactory;

    public ResourceRepositoryImpl(JPAQueryFactory queryFactory) {
        super(ResourceEntity.class);
        this.queryFactory = queryFactory;
    }

    /**
     * 리소스 조회
     * @param uuid UUID
     * @return 리소스
     */
    @Override
    public Optional<ResourceEntity> findByUUID(String uuid) {
        BooleanBuilder conditions = new BooleanBuilder();

        // 조회 대상 설정
        JPAQuery<ResourceEntity> query = queryFactory.select(resourceEntity);
        query.from(resourceEntity);

        // 조건 설정
        conditions.and(resourceEntity.uuid.eq(uuid));
        query.where(conditions);

        return Optional.ofNullable(query.fetchOne());
    }

    /**
     * 리소스 조회
     * @param resourceId 리소스 ID
     * @return 리소스
     */
    @Override
    public Optional<ResourceDetailDTO> findDetail(int resourceId) {
        BooleanBuilder conditions = new BooleanBuilder();

        // 조회 대상 설정
        JPAQuery<ResourceDetailDTO> query = queryFactory.select(Projections.fields(ResourceDetailDTO.class,
                resourceEntity.resourceName
        ));

        // 대상 설정
        query.from(resourceEntity);

        // 조건 설정
        conditions.and(resourceEntity.id.eq(resourceId));
        query.where(conditions);

        return Optional.ofNullable(query.fetchOne());
    }

    /**
     * 리소스 목록 조회
     * @return 리소스 목록
     */
    @Override
    public List<ResourceListDTO> findAllResources() {
        // 조회 대상 설정
        JPAQuery<ResourceListDTO> query = queryFactory.select(Projections.fields(ResourceListDTO.class,
                resourceEntity.id.as("resourceId"),
                resourceEntity.resourceName,
                resourceEntity.createdDate
        ));
        query.from(resourceEntity);

        return query.fetch();
    }

    /**
     * 리소스 목록 조회
     * @param pageable 페이지네이션 정보
     * @return 리소스 페이지네이션
     */
    @Override
    public Page<ResourceListDTO> findAllResources(Pageable pageable) {
        // 조회 대상 설정
        JPAQuery<ResourceListDTO> query = queryFactory.select(Projections.fields(ResourceListDTO.class,
                resourceEntity.id.as("resourceId"),
                resourceEntity.resourceName,
                resourceEntity.createdDate
        ));
        query.from(resourceEntity);

        // 결과 조회
        List<ResourceListDTO> results = query.fetch();

        // 전체 레코드 수 조회
        JPAQuery<Integer> countQuery = queryFactory.select(resourceEntity.id);
        countQuery.from(resourceEntity);
        int count = query.fetch().size();

        return new PageImpl(
                results,
                pageable,
                count
        );
    }
}

 

메서드 구현까지 완료 되었다면 아래 형태의 구조로 레포지토리가 구성되게 됩니다.

querydsl 레포지토리 생성 구조

여기서 우리는 Jpa 레포지토리인 ResourceRepository를 DI해서 사용할 수 있습니다.

우리가 레포지토리를 구현한 ResourceRepositoryDsl을 DI하여 사용할 경우 Jpa의 기본 제공 메서드들(findById 등)을 사용할 수 없으니 기존 레포지토리를 사용하는게 좋습니다.

 

/**
 * 리소스 조회
 * @param resourceId 번호
 */
public ResourceDetailDTO readResource(int resourceId) {
    // 리소스 조회
    ResourceDetailDTO resourceDetail = resourceRepository.findDetail(resourceId)
            .orElseThrow(NotFoundException::new);

    return resourceDetail;
}

 

하지만 querydsl은 서브 쿼리의 limit 옵션이 적용되지 않으며, union 또한 지원되지 않는 단점이 있습니다.

이 경우는 레포지토리 메서드를 분리해서 로직을 구현해야 할 것 같습니다.

 

처음 사용할 경우 엔티티 도메인의 관계 설정과 OneToMany 형태의 엔티티의 조건을 정의하는데 어려움을 겪게 될 수 있습니다. 

 

 

엔티티의 관계 설정에 대한 자세한 내용은 이 포스트를 참고해 주세요

 

 

Spring Data JPA 관계 매핑하기

JPA를 이용하기 위해서는 도메인 영역에 해당하는 엔티티를 정의해야 합니다. 이 엔티티는 테이블에 종속될 수도 혹은 분리해서 정의할 수도 있습니다. 엔티티 활용법에 대해서는 다음에 다시

hanainu.tistory.com

 

반응형