0. 시작하며
이전 프로젝트에 검색 로직을 JPQL로 개발했었습니다. 이를 JPQL로 구현하니 코드가 매우 복잡하고 길어졌고 실무에서 많이 사용한다는 Querydsl을 사용하여 리팩토링하여 기록하려 합니다.
1. JPQL이란?
- Java Persistence Query Language 로서 이를 기반으로 JPA의 하이버네이트가 SQL문으로 변환하여 사용하게됩니다.
- 기본 JPA 메서드 보다 섬세한 쿼리를 사용할때 사용하게됩니다.
- 문자열로 작성됩니다.
특징 (장점)
- 별칭
- 엔티티의 별칭을 필수적으로 명시해야합니다.(as 생략 가능)
- 대소문자 구분
- 엔티티와 그 속성은 대소문자를 구분하여 작성해야합니다.
- 하지만 SELECT, FROM, WHERE등의 JPQL문법은 구분하지 않습니다.
- 엔티티 이름
- JPQL에서 사용되는것은 엔티티이름입니다.
- 테이블이나 클래스 이름이사용되지 않습니다.
- 파라미터 바인딩 지원
- 객체 지향적 개발 가능
- DB 종류에 상관없이 동일하게 작성할 수 있다 등...
단점
- 런타임 시점에 에러가 발생됩니다.
- 코드를 작성하는 시점에 오타 혹은 틀린 문법에 대하여 에러가 나지 않습니다.
- 동적쿼리 작성이 어렵습니다.
- 코드에 if-else같은 조건문이 들어가 문자열을 제어해야합니다.
- 예를들어 어떤 상품의 카테고리, 이름, 상세 설명의 검색 조건이 있습니다. 하지만 이름으로만 검색 요청이 온경우와 카테고리가 추가된 경우 조건문은 다음과 같이 달라질것입니다.
- ~~ Where i.title LIKE :keyword
- ~~ Where i.title LIKE :keyword AND i.itemCategory IN :categoryList
- 따라서 각 검색 조건이 처음으로 적용해야한다면 이를 판단하고 참이면 "WHERE" 거짓이면 "AND"를 추가해주는 등 여러가지 신경써야할것이 많습니다.
- 그렇다보니 로직이 복잡해지고 가독성도 떨어지고 절대적인 코드량또한 늘어나게됩니다.
개인적으로 이 두가지 단점은 매우 치명적으로 느껴졌고 리팩토링을 진행하게 되었습니다.
2. Querydsl이란?
- 쿼리를 진짜 자바코드로 작성할 수 있도록 해줍니다. (문자열 X)
- 오픈소스 프레임워크
특징(장점)
- 동적 쿼리 문제 해결
- 진짜 자바코드여서 컴파일 시점에 에러를 표시해줍니다.
- SQL과 비슷해서 복잡한 쿼리도 쉽게 생성 가능
- 쿼리 작성 시 재사용하는 조건 등을 메서드를 통해추출하여 사용할 수 있다 등..
단점
- 추가적인 환경설정이 필요합니다.
Querydsl 사용
- Querydsl을 사용한 동적쿼리예시를 함께 살펴보겠습니다.
public List<MemberTeamDto> search(MemberSearchCondition condition) {
return queryFactory
.select(new QMemberTeamDto(
member.id,
member.username,
member.age,
team.id,
team.name))
.from(member)
.leftJoin(member.team, team)
.where(usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe()))
.fetch();
}
private BooleanExpression usernameEq(String username) {
return isEmpty(username) ? null : member.username.eq(username);
}
private BooleanExpression teamNameEq(String teamName) {
return isEmpty(teamName) ? null : team.name.eq(teamName);
}
- Where절내에 조건이 null인경우 조건이 적용되지 않습니다.
- fetch()를 통해 결과를 List를 반환하게됩니다.( 없으면 빈List )
- 참고로 결과 조회의 경우 다음과 같이 사용될 수 있습니다.
- fetchOne()인 단건일 경우 사용합니다. ( 없으면 null, 둘 이상이면 error )
- fetchFirtst(): 가장 첫 번째 data를 반환합니다.
- fetchResults(): 페이징 정보 포함합니다. total count 쿼리 추가 실행 즉 쿼리를 2번 실행합니다. ( 이때 total count가 같도록 조건만 동일하게 만들고 정렬같은 로직은 포함하지 않아도 좋습니다.)
- fetchCount(): count쿼리로 변경되어 결과값을 반환합니다.
- 또한 Pageable을 활용한다면 다음과 같이 Page객체로 반환하여 Pagenation을 구현할 수 있습니다.
public Page<MemberTeamDto> search(MemberSearchCondition condition, Pageable pageable) {
List<MemberTeamDto> content = queryFactory
.select(new QMemberTeamDto(
member.id,
member.username,
member.age,
team.id,
team.name))
.from(member)
.leftJoin(member.team, team)
.where(usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe()))
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
long total = queryFactory
.select(member)
.from(member)
.leftJoin(member.team, team)
.where(usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe()))
.fetchCount();
return PageableExecutionUtils.getPage(memberList, pageable , total);
}
private BooleanExpression usernameEq(String username) {
return isEmpty(username) ? null : member.username.eq(username);
}
private BooleanExpression teamNameEq(String teamName) {
return isEmpty(teamName) ? null : team.name.eq(teamName);
}