Soft delete 위한 QueryDSL 적용하기

qpwoeiru·2024년 7월 3일
0
post-thumbnail

Soft delete를 적용하는데 삭제 된 데이터를 접근 하는 조건이 여러가지였다. 그래서 JPA를 사용해도 @Query를 사용해 대부분의 메서드에 직접 조회 쿼리를 작성해야 했다. JPQL 쿼리는 오타, 문법 오류 외에는 컴파일 타임에 쿼리 오류를 발견할 수 없기에, 런타임 시점까지 가서 쿼리 오류를 발견한 적이 여러 번 있었어서 번거로웠다. 이를 개선하고자 컴파일 시점에서 쿼리 오류를 발견할 수 있는 QueryDSL을 적용해보고자 한다.


QueryDSL

정적 타입을 이용해 SQL 같은 쿼리를 생성할 수 있도록 해주는 프레임워크이다. 이를 이용해 쿼리 생성을 자동화하고 Java 코드의 형태로 쿼리를 작성할 수 있어 컴파일 시점에 문법 오류를 쉽게 확인할 수 있다.

아래는 QueryDSL 작동 원리이다.
Entity 정보를 갖는 Q 클래스를 사용해 JPQL를 생성하는 것이 목적이다.


QueryDSL 설정

내 프로젝트는 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에

  1. Tasks > build > clean 실행
  2. 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);
	}
}

Repository에 QueryDsl 적용하기

기존에 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

0개의 댓글

관련 채용 정보