JPA

Querydsl을 모듈화하여 쉽게 사용하기

Beekei 2024. 6. 26. 18:28
반응형

개요

실무 중 Querydsl을 사용할 때 조금 더 편하게 사용할 방법이 없을까 생각하다가 모듈화(?)라고 하긴 그렇지만 사용하기 쉽고 가독성이 좋도록 구현해 보았습니다.

 

파일 구조

구현한 파일은 4가지입니다.

모듈화 파일

 

QuerydslConfig.java

@Configuration
public class QuerydslConfig {

    @PersistenceContext
    private EntityManager entityManager;

    @Bean
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(JPQLTemplates.DEFAULT, entityManager);
    }

}

해당 클래스는 아시다시피 JPAQueryFactory를 Bean으로 설정하는 클래스입니다.

Querydsl을 사용할 때 기본적인 설정이므로 따로 설명은 하지 않겠습니다.


QuerydslBase.java

@Getter
public class QuerydslBase<T> {

    private final QuerydslRepository querydslRepository;
    private final JPAQuery<?> jpaQuery;
    private Expression<T> select;
    private Expression<Long> countSelect;

    public QuerydslBase(QuerydslRepository querydslRepository, JPAQueryFactory queryFactory) {
        this.querydslRepository = querydslRepository;
        this.jpaQuery = queryFactory.query();
    }

    @SneakyThrows
    public <D extends QuerydslSelectDTO> QuerydslBase<T> select(Class<D> selectClass) {
        this.select = (Expression<T>) selectClass.getMethod("constructor").invoke(selectClass.getDeclaredConstructor().newInstance());
        return this;
    }

    public QuerydslBase<T> select(Expression<T> select) {
        this.select = select;
        return this;
    }

    public QuerydslBase<T> countSelect(Expression<Long> countSelect) {
        this.countSelect = countSelect;
        return this;
    }

    public QuerydslBase<T> selectFrom(EntityPath<T> selectFrom) {
        this.select = selectFrom;
        this.jpaQuery.from(selectFrom);
        return this;
    }

    public QuerydslBase<T> from(EntityPath<?> from) {
        this.jpaQuery.from(from);
        return this;
    }

    public QuerydslBase<T> innerJoin(EntityPath<?> joinTable, Predicate on) {
        this.jpaQuery.innerJoin(joinTable).on(on);
        return this;
    }

    public <D> QuerydslBase<T> innerJoin(EntityPath<D> joinTable, Path<D> path) {
        this.jpaQuery.innerJoin(joinTable, path);
        return this;
    }

    public QuerydslBase<T> leftJoin(EntityPath<?> joinTable, Predicate on) {
        this.jpaQuery.leftJoin(joinTable).on(on);
        return this;
    }

    public QuerydslBase<T> where(Predicate predicate) {
        this.jpaQuery.where(predicate);
        return this;
    }

    public <C> QuerydslBase<T> where(C value, SimpleExpression<C> column) {
        this.jpaQuery.where(column.eq(value));
        return this;
    }

    public <C> QuerydslBase<T> optionalWhere(C value, SimpleExpression<C> column) {
        Optional.ofNullable(value).ifPresent(v -> this.where(v, column));
        return this;
    }

    public QuerydslBase<T> whereOr(String value, StringPath ... columns) {
        this.jpaQuery.where(Expressions.anyOf(Arrays.stream(columns)
            .map(column -> column.eq(value))
            .toArray(BooleanExpression[]::new)));
        return this;
    }

    public QuerydslBase<T> optionalWhereOr(String value, StringPath ... columns) {
        Optional.ofNullable(value).ifPresent(v -> this.whereOr(v, columns));
        return this;
    }

    public <C> QuerydslBase<T> whereIn(Collection<C> value, SimpleExpression<C> column) {
        this.jpaQuery.where(column.in(value));
        return this;
    }

    public <C> QuerydslBase<T> optionalWhereIn(Collection<C> value, SimpleExpression<C> column) {
        Optional.ofNullable(value).ifPresent(v -> this.whereIn(v, column));
        return this;
    }

    public QuerydslBase<T> like(String value, StringPath column) {
        this.jpaQuery.where(column.contains(value));
        return this;
    }

    public QuerydslBase<T> optionalLike(String value, StringPath column) {
        Optional.ofNullable(value).ifPresent(v -> this.like(v, column));
        return this;
    }

    public QuerydslBase<T> likeOr(String value, StringPath ... columns) {
        this.jpaQuery.where(Expressions.anyOf(Arrays.stream(columns)
            .map(column -> column.contains(value))
            .toArray(BooleanExpression[]::new)));
        return this;
    }

    public QuerydslBase<T> optionalLikeOr(String value, StringPath ... columns) {
        Optional.ofNullable(value).ifPresent(v -> this.likeOr(v, columns));
        return this;
    }

    public <D extends Comparable> QuerydslBase<T> lessThan(D value, ComparableExpression<D> column) {
        this.jpaQuery.where(column.lt(value));
        return this;
    }

    public <D extends Comparable> QuerydslBase<T> optionalLessThan(D value, ComparableExpression<D> column) {
        Optional.ofNullable(value).ifPresent(v -> this.lessThan(v, column));
        return this;
    }

    public <D extends Comparable> QuerydslBase<T> lessThanEqual(D value, ComparableExpression<D> column) {
        this.jpaQuery.where(column.loe(value));
        return this;
    }

    public <D extends Comparable> QuerydslBase<T> optionalLessThanEqual(D value, ComparableExpression<D> column) {
        Optional.ofNullable(value).ifPresent(v -> this.lessThanEqual(v, column));
        return this;
    }

    public <D extends Comparable> QuerydslBase<T> moreThan(D value, ComparableExpression<D> column) {
        this.jpaQuery.where(column.gt(value));
        return this;
    }

    public <D extends Comparable> QuerydslBase<T> optionalMoreThan(D value, ComparableExpression<D> column) {
        Optional.ofNullable(value).ifPresent(v -> this.moreThan(v, column));
        return this;
    }

    public <D extends Comparable> QuerydslBase<T> moreThanEqual(D value, ComparableExpression<D> column) {
        this.jpaQuery.where(column.goe(value));
        return this;
    }

    public <D extends Comparable> QuerydslBase<T> optionalMoreThanEqual(D value, ComparableExpression<D> column) {
        Optional.ofNullable(value).ifPresent(v -> this.moreThanEqual(v, column));
        return this;
    }

    public QuerydslBase<T> orderBy(OrderSpecifier<?>... specifiers) {
        this.jpaQuery.orderBy(specifiers);
        return this;
    }

    public QuerydslBase<T> groupBy(Expression<?>... groupBy) {
        this.jpaQuery.groupBy(groupBy);
        return this;
    }

    public <D> Optional<D> transformRow(ResultTransformer<Map<Long, D>> transformer) {
        return Optional.ofNullable(this.jpaQuery.transform(transformer))
            .flatMap(map -> map.values().stream().findFirst());
    }

    public <D> List<D> transformList(ResultTransformer<List<D>> transformer) {
        return this.jpaQuery.transform(transformer);
    }

    public Optional<T> getRow() {
        return this.querydslRepository.getRow(this);
    }

    public List<T> getList() {
        return this.querydslRepository.getList(this);
    }

    public Page<T> getPage(PageRequest pageRequest) {
        return this.querydslRepository.getPage(this, pageRequest);
    }

}

QuerydslBase 클래스는 입력된 쿼리문을 가지고 있다가 QuerydslRepository에 전달해 주는 역할을 합니다.

생성자로 QuerydslRepository와 JPAQueryFactory를 받고 있습니다.

QuerydslRepository는 해당 클래스에 구현된 함수들을 사용하기 위해 받고 있고, JPAQueryFactory는 JPAQuery를 생성하기 위해 받고 있습니다.

QuerydslBase가 생성되면 JPAQuery를 생성해 클래스 내에 가지고 있다가 외부에서 쿼리문이 입력되면 해당 JPAQuery에 입력합니다.

get... 메서드를 통해 데이터 조회 시 기억하고 있던 QuerydslRepository에 구현된 메서드들을 호출합니다.

 


QuerydslSelectDTO.java

public interface QuerydslSelectDTO {
    ConstructorExpression<?> constructor();
}

QuerydslSelectDTO 클래스는 Querydsl에서 Projections을 사용해 DTO로 반환해 줄 때 해당 DTO에서 구현하는 인터페이스입니다.

조회 DTO에서 constructor이라는 함수를 구현하도록 만들어 조회할 QClass의 칼럼을 설정할 수 있습니다.

QuerydslBase 클래스에 select 메서드에서 해당 constructor 메서드를 사용해 설정한 QClass 컬럼을 자동적으로 select에 입력하고 있습니다.

 

아래 예제처럼 조회할 DTO에서 constructor을 구현하여 사용하면 됩니다.

@Getter
@AllArgsConstructor
@NoArgsConstructor
public class ShopInfoDTO implements QuerydslSelectDTO {

    private long shopId;
    private ShopStatus shopStatus;
    private String shopName;
    private String businessNumber;
    private String zipCode;
    private String address;
    private String detailAddress;
    private LocalDateTime createdDatetime;
    private LocalDateTime updatedDatetime;


    @Override
    public ConstructorExpression<ShopInfoDTO> constructor() {
        return Projections.constructor(
            ShopInfoDTO.class,
            shop.id,
            shop.status,
            shop.name,
            shop.businessNumber,
            shop.address.zipCode,
            shop.address.address,
            shop.address.detailAddress,
            shop.createdDatetime,
            shop.updatedDatetime
        );
    }

}

QuerydslRepository.java

@Repository
@RequiredArgsConstructor
public class QuerydslRepository {

    private final JPAQueryFactory queryFactory;

    public <T extends QuerydslSelectDTO> QuerydslBase<T> select(Class<T> selectClass) {
        QuerydslBase<T> querydslBase = new QuerydslBase<>(this, queryFactory);
        return querydslBase.select(selectClass);
    }

    public <T extends QuerydslSelectDTO> QuerydslBase<T> select(Class<T> selectClass, Expression<Long> countSelect) {
        QuerydslBase<T> querydslBase = new QuerydslBase<>(this, queryFactory);
        return querydslBase.select(selectClass).countSelect(countSelect);
    }

    public <T> QuerydslBase<T> select(Expression<T> select) {
        QuerydslBase<T> querydslBase = new QuerydslBase<>(this, queryFactory);
        return querydslBase.select(select);
    }

    public <T> QuerydslBase<T> from(EntityPath<T> from) {
        QuerydslBase<T> querydslBase = new QuerydslBase<>(this, queryFactory);
        return querydslBase.from(from);
    }

    public <T> Optional<T> getRow(QuerydslBase<T> querydslBase) {
        return Optional.ofNullable(querydslBase.getJpaQuery().select(querydslBase.getSelect()).fetchOne());
    }

    public <T> List<T> getList(QuerydslBase<T> querydslBase) {
        return querydslBase.getJpaQuery().select(querydslBase.getSelect()).fetch();
    }

    public <T> Page<T> getPage(QuerydslBase<T> querydslBase, PageRequest pageRequest) {
        long count = Optional.ofNullable(querydslBase.getJpaQuery()
            .select(querydslBase.getCountSelect())
            .fetchOne()).orElse(0L);
        List<T> list = count > 0 ? querydslBase.getJpaQuery()
            .select(querydslBase.getSelect())
            .offset(pageRequest.getOffset())
            .limit(pageRequest.getPageSize())
            .fetch() : new ArrayList<>();
        return new PageImpl<>(list, pageRequest, count);
    }

}

마지막으로 QuerydslRepository 클래스는 QuerydslBase를 생성하고 최종 마지막에 데이터를 조회하는 역할을 합니다.

select나 from 메서드를 통해 QuerydslBase를 생성해 반환하고 getRow, getList, getPage의 3가지 타입으로 데이터를 조회합니다.


사용 예제

아래는 서비스단에서 사용하는 예제 코드입니다.

// 조회할 DTO Class
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class ShopListDTO implements QuerydslSelectDTO {

    private long shopId;
    private ShopType shopType;
    private String shopName;
    private String categoryNames;
    private String businessNumber;
    private List<String> telNumber;
    private String zipCode;
    private String address;
    private String detailAddress;

    @Override
    public ConstructorExpression<ShopListDTO> constructor() {
        return Projections.constructor(
            ShopListDTO.class,
            shop.id,
            shop.type,
            shop.name,
            Expressions.stringTemplate("GROUP_CONCAT({0})", shopCategory.name),
            shop.businessNumber,
            shop.telNumber,
            shop.address.zipCode,
            shop.address.address,
            shop.address.detailAddress);
    }

}

// Service에서 QuerydslRepository 사용
@Component
@RequiredArgsConstructor
public class ShopServiceV1 implements ShopService {

    private final QuerydslRepository querydslRepository;

    @Override
    public Page<ShopListDTO> getShopList(GetShopListSearchParameterDTO searchParameter, PageRequest pageRequest) {
        return querydslRepository
            .select(ShopListDTO.class, shop.id.countDistinct())
            .from(shop)
            .leftJoin(shopCategoryConnect, shopCategoryConnect.id.shopId.eq(shop.id))
            .leftJoin(shopCategory, shopCategory.id.eq(shopCategoryConnect.id.categoryId))
            .optionalWhereIn(searchParameter.getCategoryIds(), shopCategory.id)
            .optionalLikeOr(searchParameter.getKeyword(), shop.name, shop.businessNumber)
            .groupBy(shop.id)
            .getPage(pageRequest);
    }
    
}

 

저는 보통 조회하는 조건 자체가 서비스 로직이라고 생각하기 때문에 서비스단에서 바로 Querydsl을 통해 쿼리문을 작성하는편 입니다.

사용할때 QuerydslBase를 빌더식으로 구현하였기 때문에 가독성이 더욱 좋다고 느껴졌고, 

항상 Projections를 사용해 DTO를 조회할때 컬럼이 바뀔경우 QClass 컬럼도 바꿔주어야 하는데 DTO 내부에서 함께 설정하기 때문에 유지보수에도 편리하다고 생각합니다.

 

아직 완벽히 검증이 되지 않은 코드라서 오류가 발생할 여지가 있는데 위에 설명드린 4가지 클래스들이 복잡성이 그렇게 높지 않아서 얼마든지 커스텀하여 사용할 수 있을 것 같습니다.

 

 

 


만약 해당 모듈을 사용하려면 라이브러리 의존성을 설정하여 사용하시면 됩니다.

implementation 'com.github.beekei-got:beekei-library:1.0.0'

더욱 자세한 코드는 아래 깃허브 링크를 통해 확인하실 수 있고 오류가 발견되는데로 업데이트 중입니다.

 

GitHub - beekei-got/beekei-library

Contribute to beekei-got/beekei-library development by creating an account on GitHub.

github.com

반응형