TIL ... day 40 6주차 - day 5. 설계의 중요성, querydsl … 22.06.23

BYEONGMIN CHOI·2022년 6월 25일
0

TIL(Today I Learned)

목록 보기
22/24
post-thumbnail

클론코딩을 진행하면서 발생했던 트러블 슈팅에 대해 정리해볼려고 합니다.

메인페이지의 필터링 기능을 구현하기 위해 데이터배이스의 데이터를 조건에 맞게 가져오는 방법을 구현해야 했습니다. query문을 사용하여 데이터를 가져오려 했지만 조건이 있는 경우와 없는 경우를 고려해야 했기때문에 저의 현재 능력으로는 query를 잘 사용하지 못할 거 같았습니다. 조건이 null경우도 수행할 수 있는 동적쿼리 방식을 찾아 현재 구현해야하는 기능을 구현해야겠다고 생각하였습니다.

Querydsl

구글링을 통해 우아한 형제들의 동적쿼리 보고 기능을 구현을 시작하였습니다.
JPQL 대신 Querydsl을 사용하는 이유 중 하나가 type-safe(컴파일 시점에 알 수 있는) 쿼리를 날리기 위해서 라고 한다.

  • JPQL : 쿼리에서 오타가 발생해도 컴파일 시점에서 알기 힘들며 오로지 런타임에서만 체크가 가능하다
  • Querydsl : 컴파일 시점에 오류를 잡아줄 수 있기 때문에 좋다

build.gradle

plugins {
    // dsql 사용하기 위해 의존성 추가
    id 'com.ewerk.gradle.plugins.querydsl' version '1.0.10'
}

dependencies {
     ...
     
    // query dsql 사용하기 위한 dependency 주입
    // 3. querydsl dependencies 추가
    implementation "com.querydsl:querydsl-jpa:${queryDslVersion}"
    implementation "com.querydsl:querydsl-apt:${queryDslVersion}"
}


/*
* querydsl 설정 추가
* */
def querydslDir = "$buildDir/generated/querydsl"
// JPA 사용 여부와 사용할 경로를 설정
querydsl {
    jpa = true
    querydslSourcesDir = querydslDir
}

// build 시 사용할 sourceSet 추가
sourceSets {
    main.java.srcDirs querydslDir
}
// querydsl 컴파일시 사용할 옵션 설정
compileQuerydsl {
    options.annotationProcessorPath = configurations.querydsl
}

// querydsl 이 compileClassPath 를 상속하도록 설정
configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
    querydsl.extendsFrom compileClasspath
}

의존성 추가후 업데이트를 해줍니다. 그 후

위 사진에 보이는 compileQuerydsl 을 실행하여 Qclass를 생성해 줍니다.

우아한형제들에서 사용하는 방식으로 JpaQueryFactory를 빈으로 등록하고 구현체를 직접 구현하여 사용하는 방법으로 사용하였다.

@Configuration
public class QueryConfig {

    @PersistenceContext
    private EntityManager em;

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

동적쿼리

  • BooleanBuilder를 작성하는 방법 : BooleanBuilder를 사용하는 방법은 어떤 쿼리가 나가는지 예측하기 어려움
  • Where 절과 파라미터로 Predicate를 이용하는 방법
  • Where 절과 파라미터로 Predicate를 상속한 BooleanExpression을 사용하는 방법 : and / or 같은 메소드들을 이용해서 BooleanExpression을 조합해서 새로운 BooleanExpression을 만들 수 있다는 장점이 있음 -> 재사용성이 높음, BooleanExpression 은 null 을 반환하게 되면 Where 절에서 조건이 무시되기 때문에 안전하다.

이와 같은 이유로 BooleanExpression 방식의 querydsl을 사용하여 아래와 같은 코드를 적용하였다.

@Repository
@RequiredArgsConstructor
public class HotelRepositoryCustom {
    private final JPAQueryFactory jpaQueryFactory;

    public List<MainPageHotelInfoDto> filteringHotels(RequestHotelsDto condition) {
        return jpaQueryFactory
                .select(new QMainPageHotelInfoDto(
                        hotel.id.as("hotelId"),
                        hotel.mainImage.as("mainImage"),
                        hotel.title.as("title"),
                        hotel.defaultPrice.as("price"),
                        hotel.score.as("score")
                ))
                .from(hotel)
                .join(hotel.images)
                .join(hotel.categories, category1)
                .join(hotel.facilities, facility)
                .where(
                        eqCategory(condition.getCategory()),
                        betweenPrice(condition.getMinPrice(), condition.getMaxPrice()),
                        innerType(condition.getType()),
                        innerFacilities(condition.getFacilities())
                ).orderBy(hotel.id.desc())
                .fetch();
    }

    private BooleanExpression eqCategory(Integer category) {
        return category != null ? category1.id.eq(category) : null;
    }

    private BooleanExpression betweenPrice(Integer minPrice, Integer maxPrice) {
        if(maxPrice == null && minPrice == null) return null;
        return QHotel.hotel.defaultPrice.between(minPrice, maxPrice);
    }

    private BooleanExpression innerType(List<Integer> type) {
        return type.size() != 0 && !type.contains(null) ? hotel.type.in(type) : null;
    }

    private BooleanExpression innerFacilities(List<Integer> facilities) {
        return facilities.size() != 0 && !facilities.contains(null) ? facility.id.in(facilities) : null;
    }
}

Trouble shotting

  1. between 메소드를 사용하면서 발생한 문제 :
  • 처음 사용했던 코드
  private BooleanExpression betweenPrice(Integer maxPrice, Integer minPrice) {
        if(maxPrice == null && minPrice == null) return null;
        return QHotel.hotel.defaultPrice.between(minPrice, maxPrice);
    }
  • 수정한 코드
  private BooleanExpression betweenPrice(Integer minPrice, Integer maxPrice) {
        if(maxPrice == null && minPrice == null) return null;
        return QHotel.hotel.defaultPrice.between(minPrice, maxPrice);
    }

between 메소드의 파라미터 자리에 대한 이해부족을 파라미터의 순서를 잘못 기입하여 조건을 주어도 항상 조회되는 데이터가 null 값이 발생하는 이슈가 있었습니다.

문제해결 : 메소드의 상세내역을 살펴보고 Parameter의 순서와 리턴값을 확인하여 문제를 해결 하였습니다.

 public final <A extends Number & Comparable<?>> BooleanExpression between(@Nullable A from, @Nullable A to) {
        if (from == null) {
            if (to != null) {
                return loe(to);
            } else {
                throw new IllegalArgumentException("Either from or to needs to be non-null");
            }
        } else if (to == null) {
            return goe(from);
        } else {
            return between(ConstantImpl.create(cast(from)), ConstantImpl.create(cast(to)));
        }
    }

  1. List 형식의 값을 BooleanExpression 으로 처리 할때 값이 존재하지 않는 경우 빈 리스트가 전달되는 것이 아니라, [null, null]의 리스트가 전달되어 size() 가진다는 이슈가 있었습니다.

    처음에 생각했을 때 조건이 주어지지 않는다면 type.size() == 0 이라고 생각 하여 조건문에 이와 같이 적용하여 return을 해주었습니다. 하지만 항상 null이 반환되는게 아니라 리스트에 null이 들어가 있는 type.size() 를 갖는 것을 확인하였습니다.

    잘못 생각한점 :
  • 조건이 없다면 요청값이 없다는 것인데 type = null, 즉 query string이 api/hotels?type=&type= 이렇게 들어온다고 생각하고 있었다는 점이 잘못이였습니다.

문제 해결 : 전달되는 리스트의 크기가 0이 아닐경우, 또한 리스트안에 null값이 포함되어있지 않는 경우 일때 조건이 주어진 것이라고 생각하여 아래와 같이 수정하였습니다.

    private BooleanExpression innerType(List<Integer> type) {
        return type.size() != 0 && !type.contains(null) ? hotel.type.in(type) : null;
    }

추가 공부해야할 사항 : query, querydsl

참고 : https://github.com/Youngerjesus/Querydsl/blob/master/docs/woowahwan.md

profile
스스로 성장하는 개발자가 되겠습니다.

0개의 댓글