Soft delete를 적용하는데 삭제 된 데이터를 접근 하는 조건이 여러가지였다. 그래서 JPA를 사용해도 @Query를 사용해 대부분의 메서드에 직접 조회 쿼리를 작성해야 했다. JPQL 쿼리는 오타, 문법 오류 외에는 컴파일 타임에 쿼리 오류를 발견할 수 없기에, 런타임 시점까지 가서 쿼리 오류를 발견한 적이 여러 번 있었어서 번거로웠다. 이를 개선하고자 컴파일 시점에서 쿼리 오류를 발견할 수 있는 QueryDSL을 적용해보고자 한다.
정적 타입을 이용해 SQL 같은 쿼리를 생성할 수 있도록 해주는 프레임워크이다. 이를 이용해 쿼리 생성을 자동화하고 Java 코드의 형태로 쿼리를 작성할 수 있어 컴파일 시점에 문법 오류를 쉽게 확인할 수 있다.
아래는 QueryDSL 작동 원리이다.
Entity 정보를 갖는 Q 클래스를 사용해 JPQL를 생성하는 것이 목적이다.
내 프로젝트는 Springboot 3.0 이상이다. 여러 사람들 글을 보니 다들 정상적으로 작동되는 설정이 다들 다양(?)한 것 같다.. 내 환경에서 적용된 설정은 아래와 같다.
// build.gradle
dependencies {
...
// queryDSL
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}
설정 후 intellij의 오른쪽 Gradle에
Tasks > build > clean
실행Tasks > other > compileJava
실행위 두 순서를 진행하면 프로젝트의 build/generated
경로 안에 Q 클래스가 생성된다. 아래는 build/generated
경로 아래 생긴 파일이다.
QueryDslConfig
도 설정해준다.
@Configuration
public class QueryDslConfig {
@PersistenceContext
private EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory(){
return new JPAQueryFactory(entityManager);
}
}
기존에 JPA로만 구성된 repository는 아래와 같다. 아래 repository에 QueryDsl을 적용해서 수정할 것이다.
public interface UserRepository extends JpaRepository<User,Long> {
@Query("SELECT CASE WHEN count(u) > 0 THEN true ELSE false END "
+ "FROM User u "
+ "WHERE u.email = :email AND u.deletedAt is null")
boolean existsByEmail(String email);
@Query("SELECT CASE WHEN count(u) > 0 THEN true ELSE false END "
+ "FROM User u "
+ "WHERE u.nickname = :nickname AND u.deletedAt is null")
boolean existsByNickname(String nickname);
@Query("SELECT u FROM User u WHERE u.email = :email AND u.deletedAt IS NULL")
Optional<User> findByEmail(String email);
}
먼저 QueryDsl을 적용하기 위해 UserCustomRepository
인터페이스를 생성해야 한다.
public interface UserCustomRepository {
boolean existsByEmail(String email);
boolean existsByNickname(String nickname);
Optional<User> findByEmail(String email);
}
위 인터페이스를 구현하는 UserCustomRepositoryImpl
를 만든다.
@Repository
@AllArgsConstructor
public class UserCustomRepositoryImpl implements UserCustomRepository{
private final JPAQueryFactory queryFactory;
@Override
public boolean existsByEmail(String email) {
return queryFactory.selectFrom(user)
.where(user.email.eq(email)
.and(user.deletedAt.isNull()))
.fetchOne() != null;
}
@Override
public boolean existsByNickname(String nickname){
return queryFactory.selectFrom(user)
.where(user.nickname.eq(nickname)
.and(user.deletedAt.isNull()))
.fetchOne() != null;
}
@Override
public Optional<User> findByEmail(String email) {
return Optional.ofNullable(queryFactory.selectFrom(user)
.where(user.email.eq(email)
.and(user.deletedAt.isNull()))
.fetchOne());
}
}
exist는 select로 조회한 fetchOne 결과에서 null이 아니면 존재하고(true), null이면 존재하지 않는 것(false)으로 판단하기로 했다.
이제 기존에 사용하던 UserRepository
에 JpaRepository와 함께 상속 받으면 된다.
public interface UserRepository extends JpaRepository<User,Long>, UserCustomRepository {
}
이렇게 만들면 기존 Service 계층에서 UserRepository 관련된 코드는 수정할 필요도 없고, QueryDsl은 적용되도록 수정했다.
QueryDsl로 쿼리도 빠르고 정확하게 작성할 수 있고, 긴 쿼리도 작성하기 편리하게 됐다!
참고
https://lordofkangs.tistory.com/461
https://lordofkangs.tistory.com/456
https://sjh9708.tistory.com/174