클론코딩을 진행하면서 발생했던 트러블 슈팅에 대해 정리해볼려고 합니다.
메인페이지의 필터링 기능을 구현하기 위해 데이터배이스의 데이터를 조건에 맞게 가져오는 방법을 구현해야 했습니다. query문을 사용하여 데이터를 가져오려 했지만 조건이 있는 경우와 없는 경우를 고려해야 했기때문에 저의 현재 능력으로는 query를 잘 사용하지 못할 거 같았습니다. 조건이 null경우도 수행할 수 있는 동적쿼리 방식을 찾아 현재 구현해야하는 기능을 구현해야겠다고 생각하였습니다.
구글링을 통해 우아한 형제들의 동적쿼리 보고 기능을 구현을 시작하였습니다.
JPQL 대신 Querydsl을 사용하는 이유 중 하나가 type-safe(컴파일 시점에 알 수 있는) 쿼리를 날리기 위해서 라고 한다.
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);
}
}
이와 같은 이유로 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;
}
}
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))); } }
문제 해결 : 전달되는 리스트의 크기가 0이 아닐경우, 또한 리스트안에 null값이 포함되어있지 않는 경우 일때 조건이 주어진 것이라고 생각하여 아래와 같이 수정하였습니다.
private BooleanExpression innerType(List<Integer> type) { return type.size() != 0 && !type.contains(null) ? hotel.type.in(type) : null; }