개발을 하다보면 위의 그림 처럼 여러가지 조건을 가지고 검색을 해야할 경우가 많이 있습니다. 조건이 고정되어 있다면 조금 쉽겠지만 사용자가 필요에 따라 조건을 추가하거나 뺄 수 있다면 슬슬 머리가 아픕니다.
@GetMapping("/search")
public List<Cafe> search(@RequestParam(value = "city", required = false) String city,
@RequestParam(value = "area", required = false) String area,
@RequestParam(value = "keyword", required = false) String keyword){
if(city != null && area != null && keyword != null){
list= exhibitionService.findByAllCategory(city, area, keyword);
}
else if(city != null && area != null && keyword == null ) {
list= exhibitionService.findByCityAndArea(city, area);
}
else if ~~~~~
요런식으로 if문을 통해서 구현을 조건 검색을 구현 할 수 있으나 이 경우 조건이 3개만 되도 6개의 if문을 써야하고 4개가 되면 무려 24개의 if문을 사용해야합니다.
조금 더 프로그래밍 센스가 있으신 분들은 JPQL
과 Builder
패턴을 이용해서 SQL을 동적으로 생성할 수 있지만 SQL를 직접 작성 하는 것은 사소한 오타 하나로 에러를 만들어 낼 수 있는 위험이 있습니다.
본 단락에서는 QueryDSL의 개념과 설치에 대해 다룹니다. 구현 부분만 보실 분들은 2번으로 가주세요.
조건 검색을 만들기 위해서 QueryDSL
이라는 라이브러리를 사용하려고 합니다. QueryDSL은 JPA만으로는 복잡한 쿼리를 만들기 어렵고 JPQL과 같이 직접 SQL을 사용하는 방식은 SQL을 실행 전까지는 SQL을 검증할 수 없어 오류가 생기기 쉽습니다.
QueryDSL은 Java를 사용해서 SQL Query를 작성할 수 있게 해주는 라이브러리 입니다. SQL쿼리를 JAVA 코드를 통해 생성하여 실행하지 않고 컴파일 단계에서 문법 오류를 잡을 수 있으며, 동적으로 쿼리를 작성할 수 있어 코드가 유연해 질 수 있습니다.
gradle로 셋업하는 방식입니다. 먼저 build.gradle에 dependencies에 다음을 추가해줍시다.
// QueryDSL JPA
implementation 'com.querydsl:querydsl-jpa'
// QFile 생성 및 가져오기
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
정상 작동을 하기 위해선 build.gradle annotationProcessor를 추가해줘야 합니다
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
@Configuration
public class QuerydslConfiguration {
@PersistenceContext
private EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
Qfile은 QueryDSL에서 쿼리를 위해 사용하는 java 파일입니다. entity를 생성한 후에 gradle의compileJava
를 실행하면 자동으로 entity마다 Qfile을 생성하게 됩니다. IntellJ를 사용하면 src/generated
에 추가 될 것이고, 아니면 build 아래 생성되게 됩니다. build 폴더 아래 생겨 import에 문제가 생기면 build.gradle에 다음 추가 해주세요.
sourceSets {
main {
java {
srcDirs = ['src/main/java', buildDir.toString() + '/generated/sources/annotationProcessor/java/main']
}
}
}
QFile에 대한 자세한 사용법은 아래에서 다루겠습니다.
기존 JPARepository는 interface에서 메서드의 이름을 선언하는 방식으로 데이터 쿼리를 합니다. 그러나 이 방식으로는 QueryDSL을 사용할 수 없기 때문에 Repository에서 QueryDSL을 사용할 수 있도록 확장해야합니다.
기존에 JPARepository가 있다면 이 Repository의 이름에 Custom
을 붙인 interface를 선언하고 QueryDSL을 이용해 구현할 기능을 추가해줍니다. 위의 그림에서는 CafeRepositoryCustom입니다. 해당 CafeRepositoryCustom의 기능은 CafeRepositoryCustom을 상속받은 CafeRepositoryCustomImpl에 구현하여 줍니다. 여기서는 QueryDSL을 선언 할 수 있게 QuerydslRepositorySupport도 상속합니다.
기존 CafeRepository가 CafeRepositoryCustom을 추가로 상속하면 Repository확장은 끝입니다.
CafeRepository 에서 CafeRepositoryCustom의 기능들을 사용할 수 있습니다.
public interface CafeRepositoryCustom {
Page<Cafe> findBySearchOption(Pageable pageable, String name, String city, String gu);
}
public interface CafeRepository extends JpaRepository<Cafe, Long>, CafeRepositoryCustom {
}
QueryDSL에서는 JPAQueryFactory와 Qfile을 이용해서 Query를 생성합니다. JPAQueryFactory는 쿼리를 생성하는 Factory이고, Qfile은 조건을 생성할 수 있게 해줍니다.
예제를 한번 보시죠
JPQLQuery<Cafe> query = queryFactory.selectFrom(QCafe.cafe)
.where(QCafe.cafe.name.eq("cafe"));
queryFactory의 method는 selectFrom
, where
과 같은 SQL문이 메서드로 있습니다. 여기에 Qfile을 이용해 조건을 추가하면 쿼리가 생성되는 형식입니다.
selectFrom에 QCafe.cafe를 넣어 Cafe테이블을 조회하는 것을 만들었고, where에 QCafe.cafe.name.eq("cafe")를 넣어 name이 cafe인 Cafe데이터를 가져오게 하였습니다.
select * from cafe where name = 'cafe'
이는 위의 SQL절과 같습니다.
where절에는 BooleanExpress와 null을 여러개 파라미터로 넣을 수 있습니다. QCafe.cafe.name.eq("cafe")
와 같은 조건문을 BooleanExpress라 합니다. 또한 null은 파라미터로 들어오면 아무 조건도 추가되지 않습니다. 이를 이용해 동적 쿼리를 작성하는 예제는 다음과 같습니다.
import static com.couchcoding.querydsl.domain.cafe.QCafe.cafe;
public class CafeRepositoryImpl extends QuerydslRepositorySupport implements CafeRepositoryCustom {
@Autowired
private JPAQueryFactory queryFactory;
public CafeRepositoryImpl() {
super(Cafe.class);
}
@Override
public Page<Cafe> findBySearchOption(Pageable pageable, String name, String city, String gu) {
JPQLQuery<Cafe> query = queryFactory.selectFrom(cafe)
.where(eqCity(city), eqGu(gu), containName(name));
List<Cafe> cafes = this.getQuerydsl().applyPagination(pageable, query).fetch();
return new PageImpl<Cafe>(cafes, pageable, query.fetchCount());
}
private BooleanExpression eqCity(String city) {
if(city == null || city.isEmpty()) {
return null;
}
return cafe.city.eq(city);
}
private BooleanExpression containName(String name) {
if(name == null || name.isEmpty()) {
return null;
}
return cafe.name.containsIgnoreCase(name);
}
private BooleanExpression eqGu(String gu) {
if(gu == null || gu.isEmpty()) {
return null;
}
return cafe.gu.eq(gu);
}
}
밑의 BooleanExpression을 리턴해주는 메서드들은 조건이 없을경우 null을 리턴해 줍니다. 이렇게 되면 조건이 들어오지 않았을경우 where절에 null이 들어가 해당 조건이 들어가지 않습니다.
where절에는 eqCity
containName
eqGu
가 있는데 각 조건은 and로 연결되어 쿼리를 날립니다.
그 밑에
List<Cafe> cafes = this.getQuerydsl().applyPagination(pageable, query).fetch();
return new PageImpl<Cafe>(cafes, pageable, query.fetchCount());
는 Page 처리 구현체입니다. applyPagination을 통해 만들어진 query가 fetch 메서드를 통해 실행됩니다 . QuerydslRepositorySupport를 상속받으면 getQuerydsl()을 통해 페이징 처리와 같은 기능을 사용할 수 있습니다.
QueryDSL을 이용해 if문으로 쿼리를 추가할 때 보다 훨씬 코드가 간결해지고 변경도 쉽게 변경하였습니다. 전체 코드를 보고싶으면 여기서 확인할 수 있습니다.
카우치코딩에서는 1:1 코딩 문제해결 멘토링 서비스입니다. 가르치는데 관심있는 멘토분들이나 문제해결이 필요한 멘티분들 방문해주세요~
또한 별도로 6주 포트폴리오 수업을 진행중에있습니다. 혼자 포트폴리오 준비를 하는데 어려움이 있으면 관심가져주세요~