Project/알고 사용하자

[Project - 알고 사용하자] QueryDSL

Yn3(인삼) 2024. 5. 13. 21:21

계기

Repository를 만들 때 사용하지 않는 메서드들이 많은 JpaRepository대신 Repository를 상속받아서 쓰려고 했다.

/*
public interface MemberRepository extends JpaRepository<Member, Long> {
}
*/

public interface MemberRepository extends Repository<Member, Long> {
}

 

이렇게 MemberRepository 인터페이스를 만들고 나서

OCP를 지키기 위해 Config 클래스 코드를 짜다 보니 문제가 생겼다.

 

Config 클래스에서 인터페이스를 사용하고자 @RequiredArgsConstructor와 MemberRepository 필드로 의존성을 주입해서 하자니 MemberRepository가 아닌 다른 MemberJpaRepository 같은 거를 쓰면 유연해지지 않게 되고,

@Configuration
@RequiredArgsConstructor
public class MemberConfig {

    private final MemberRepository memberRepository;
//    private final MemberJpaRepository memberJpaRepository;
    
    @Bean
    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository);
    }

}

 

Repository 구현체를 만들어서 사용하면 return 할 때 MemberRepositoryImpl나 MemberJpaRepositoryImpl을 넣어주면 되니 유연성이 좋겠지만, 상속받은 Repository 인터페이스를 구현체에서 다시 재정의해야 하는 상황이 생긴다.

@Configuration
@RequiredArgsConstructor
public class MemberConfig {
    
    @Bean
    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository() {
        return new MemberRepositoryImpl();
 //       return new MemberJpaRepositoryImpl();
    }
}

 

이렇게 되는 문제를 해결하기 위해

일단 구현체는 만드는 것으로 해서 Repository를 상속하는 인터페이스 대신 아무것도 상속받지 않는 인터페이스와

이 인터페이스를 사용할 구현체 클래스를 생성했다.

 

그럼 구현체는 어떻게 개발할까?

EntityManager를 사용한 JPQL 생각했는데 더 좋은 건 없을까 찾아보고 QueryDSL을 사용하기로 했다.

 

따라서 JPQL과 QueryDSL을 비교해 보고, QueryDSL에 대해 알아보자.


 

JPQL

JPQL(Java Persistence Query Language)은 JPA의 일부로 Query를 Table이 아닌 Entity 객체 기준으로 작성하는 객체지향 쿼리 언어이다.

JPQL은 객체를 기준으로 모든 것이 움직이기 때문에 개발할 때 테이블에 매핑되는 객체가 반드시 존재해야 한다.

따라서 검색할 때 테이블이 아닌 객체를 대상으로 검색해야 한다.

특징

  • SQL을 추상화한 JPA의 객체지향 쿼리
  • 테이블이 아닌 Entity 객체를 대상으로 개발
  • Entity와 속성은 대소문자 구분
  • 별치(alias) 사용 필수

구현 1 (EntityManager)

@Repository
@RequiredArgsConstructor
public class MemberRepositoryImpl implements MemberRepository {

    private final EntityManager entityManager;
    
    @Override
    public void save(Member member) {
        entityManager.persist(member);
    }
    
    @Override
    public List<Member> findByName(String firstName, String lastName) {
        TypedQuery<Member> tqMember = entityManager.createQuery(
            "select m from Member m where m.firstName = :firstName and m.lastName = :lastName", Member.class);
        tqMember.setParameter("firstName", firstName);
        tqMember.setParameter("lastName", lastName);
        return tqMember.getResultList();
    }
}
public interface MemberRepository {
    void save(Member member);
    List<Member> findByName(String firstName, String lastName);
}

구현 2 (repository interface)

public interface MemberRepository extends JpaRepository<Member, Long>{

   @Query("select m from Member m where m.firstName = ?1 and m.lastName = ?2")
   Member findByName1(String firstName, String lastName);
   
   @Query("select m from Member m where m.firstName = :firstName and m.lastName = :lastName")
   Member findByName2(@Param("firstName") String firstName, @Param("lastName") String lastName);
}

문제점

구현 1, 2를 보면 쿼리를 String 형태로 작성하고 있다는 문제점이 있다.

 

  • JPQL은 String 형태이기 때문에 개발자 의존적 상태
  • Compile 단계에서 Type-Check 불가능
  • RunTime 단계에서 오류 발견 가능 (장애 risk 상승)

QueryDSL

QueryDSL은 정적 타입을 이용해서 SQL, JPQL을 코드로 작성할 수 있도록 도와주는 오픈소스 빌더 API로, JPQL의 문제점을 보완하기 위해 나온 것이다.

 

QueryDSL을 사용하는 목적은 뚜렷하다.

JPQL은 문자열 형태로 쿼리가 작성되었고, 이로 인해 Compile 단계에서 Type-Check가 불가능했다.

이러한 risk를 줄이기 위해 QueryDSL이 등장했고, 이를 통해 Compile 단계에서 Type-Check가 가능해졌다.

 

QueryDSL 예시를 보자.

@PersistenceContext
EntityManager entityManager;
 
public List<Member> findByName(String firstName, String lastName){
	JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
	QMember member = QMember.member;
  
	List<Member> memberList = jpaQueryFactory
                                .selectFrom(member)
                                .where(member.firstName.eq(firstName)
                                .and(member.lastName.eq(lastName))
                                .fetch();
	return memberList;
}

JPQL과는 달리 QueryDSL은 모든 쿼리에 대한 내용이 함수 형태로 제공된다.

이렇게 Entity 객체와 함수 형태로 구성된 QueryDSL을 통해 구현된 코드는 오류가 존재하면 Compile 단계에서 바로 확인 가능하며, 이에 따른 후속 조치가 가능하기 때문에 그만큼 risk가 줄어들게 된다.

단점은 코드수가 많아진다는 것인데 가독성 측면만 생각해도 QueryDSL이 JPQL보다 나은 것 같다.

특징

  • 문자열이 아닌 코드로 작성
  • Compile 단계에서 문법 오류를 확인 가능
  • 코드 자동 완성 기능 활용 가능
  • 동적 쿼리 구현 가능

QClass

QueryDSL 예시에 QMember가 있다.

이처럼 Entity명 앞에 Q가 붙어있는 것을 QClass라고 한다.

 

이 QClass는 어떻게 만들어지고, Entity 대신 사용하는 이유는 뭘까?

  • QClass는 Compile 단계에서 Entity를 기반으로 생성되며, JPA_APT(JPAAnnotationProcessTool)가 @Entity와 같은 특정 어노테이션을 찾고 해당 클래스를 분석해서 만든다. 따라서 Compile 시점에 오류를 확인할 수 있다.
  • QClass는 Entity 클래스의 메타 정보를 담고 있는 클래스로, QueryDSL은 이를 이용해서 타입 안정성을 보장하며 쿼리를 작성할 수 있게 된다. 따라서 Entity 속성을 직접 참조하고 조합하여 쿼리를 구성할 수 있다.
  • IDE의 자동완성 기능을 활용하여 속성 이름을 직접 기억하지 않고 쿼리 작성을 보다 편리하게 할 수 있다.

 

QueryDSL 프로젝트에 적용

1. build.Gradle 의존성 추가

현재 Spring Boot 3.2.2 버전을 사용 중이며, 아래 의존성 코드를 추가한다.

dependencies {
    ...
    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"
    ...
}

2. QClass 생성

 

IntelliJ 우측 Gradle 탭 클릭 후 Project > Tasks > build에 있는 clean 실행 후 build를 실행한다.

위 작업을 끝낸 후 Project > build > generated > sources > annotationProcessor > java > main 디렉터리 아래 Entity가 있는 디렉터리와 동일한 디렉터리가 생성되어 그 안을 확인하면 QClass가 생성된 것을 확인할 수 있다.

3. QueryDSL Configuration

QueryDSL는 전체 Repository에서 사용할 것이니 Config 설정 클래스에서 Bean 등록하여 사용하는 것이 좋을 것 같다.

@Configuration
@RequiredArgsConstructor
public class QueryDSLConfig {

    private final EntityManager entityManager;

    @Bean
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(entityManager);
    }
}

4. QueryDSL Repository에 구현

@Repository
@RequiredArgsConstructor
public class MemberRepositoryImpl implements MemberRepository {

    private final JPAQueryFactory jpaQueryFactory;

    @Override
    public Optional<Member> findOneByName(String name) {
        return Optional.ofNullable(
                jpaQueryFactory
                .selectFrom(member)
                .where(member.name.eq(name))
                .fetchOne());
    }
    
    ...
}
public interface MemberRepository {

    Optional<Member> findOneByName(String name);
    
    ...
}

 

References

https://velog.io/@cho876/JPQL-vs-query-DSL

 

https://velog.io/@kimsundae/Gradle-SpringBoot-3.x-QueryDSL-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0

 

https://medium.com/mo-zza/spring-data-jpa-querydsl-%EC%A0%81%EC%9A%A9-22a0364cd579

 

https://turtle-codingstudy.tistory.com/54