본문 바로가기

[ BE ] 기술

[BE-기술] Spring Data JPA와 QueryDSL

안녕하세요 NOT-ERROR팀의 백엔드 개발자 홍민정입니다. 🫧


☑️ Spring Data JPA

▪️ Spring Data JPA

  • 지루하게 반복되는 CRUD문제를 세련된 방법으로 해결하여 개발자는 인터페이스만 작성하면 됨
  • Spring data JPA가 구현 객체를 동적으로 생성해서 주입함
  • JpaReapository 인터페이스를 제공하여 이를 상속받아 우리가 상상할 수 있는 모든 API를 처리할 수 있음
public interface CartDetailRepository extends JpaRepository<CartDetail, Long> {
	}

▪️ Spring Data JPA의 쿼리 메서드 기능

  • 기본적으로 CRUD 메서드 및
  • 메서드 이름으로 쿼리를 생성함
    • @Query 애너테이션으로 쿼리를 직접 정의할 수 있음
  • 메서드 이름만으로 JPQL 쿼리를 생성할 수 있음
  • 선언된 메서드에 대해서는 애플리케이션 로딩 시점에 쿼리를 다 만들어버림
public interface CartDetailRepository extends JpaRepository<CartDetail, Long> {
	CartDetail findByProductId(Long productId);
}
  • 기본적으로 JPA가 CRUD 메서드 및 쿼리 메서드 기능을 사용하더라도 원하는 조건의 데이터를 수집하기 위해서는 필연적으로 쿼리를 작성하게 됨

▪️ @Query 어노테이션을 사용해서 직접 쿼리를 작성

① JPQL로 작성 (nativeQuery = false)

public interface CartDetailRepository extends JpaRepository<CartDetail, Long> {
	@Query(value = "select cd from CartDetail cd where cd.product.productId > : productId")
	CartDetail findByProductId(@Param(value="productId") Long productId);
}
  • JPA의 일부분으로 정의된 플랫폼 독립적인 객체지향 쿼리 언어
  • 일반 SQL이 데이터베이스를 바라보고 작성한다면 JPQL은 엔티티클래스를 바라보고 작성
  • ② SQL로 작성 (nativeQuery = true)
public interface CartDetailRepository extends JpaRepository<CartDetail, Long> {
@Query( value = "select cart_detail_id, cart_id, product_id, count"
							+ "from cart_detail"
							+ "where product_id = :productId", nativeQuery = true)	
CartDetail findByProductId(@Param(value="productId") Long productId);
  • 어떠한 이유로 JPQL을 사용할 수 없을 때 JPA는 native SQL을 통해 직접 SQL을 사용할 수 있는 기능을 제공함
  • SQL을 개발자가 직접 정의

⛔️ SQL, JPQL의 문제점

  • SQL과 JPQL은 문자열로 type-check가 불가능함
  • 잘 해봐야 애플리케이션 로딩 시점에 알 수 있으며 컴파일 시점에 알 수 있는 방법이 없음
  • 해당 로직을 실행하기 전까지 작동여부를 확인할 수 없음
  • 해당 쿼리 실행 시점에 오류를 발견

☑️ Query DSL

  • SQL, JPQL을 코드로 작성할 수 있도록 도와주는 빌더 API
  • 정적 타입을 이용해서 SQL 등의 쿼리를 생성해주는 프레임워크

▪️ QueryDSL의 장점

  • 문자가 아닌 코드로 작성함으로써 컴파일 시점에 문법 오류를 쉽게 발견
  • 코드 자동완성 등 IDE의 도움을 받을 수 있음
  • 동적인 쿼리 작성이 단순하고 쉬움
  • 코드모양이 JPQL과 거의 비슷
  • 쿼리 작성 시 제약조건 등을 메서드 추출을 통해 재사용할 수 있음

🔻 약간의 단점은, Gradle 설정 및 사용법 등을 익혀야 한다는 점 .. !!

▪️QueryDSL 설정

build.gradle

dependencies{
		// QueryDSL 추가
    implementation "com.querydsl:querydsl-jpa:${queryDslVersion}"
    annotationProcessor(
            "javax.persistence:javax.persistence-api",
            "javax.annotation:javax.annotation-api",
            "com.querydsl:querydsl-apt:${queryDslVersion}:jpa")
}
// QueryDSL 추가
sourceSets {
    main {
        java {
            srcDirs = ["$projectDir/src/main/java", "$projectDir/build/generated"]
        }
    }
}

▪️ Query DSL 사용

** 가장 먼저 Q 클래스 생성

  • build.gradle을 통해 생성됨
  • CartDetail.java @Entity를 가지고 QCartDetil이라는 QueryDSL 전용 객체를 만들 수 있음
  • 엔티티매니저를 JPAQueryFactory에 넣고 QCartDetail 객체를 가지고 쿼리를 코드로 짤 수 있음
  • 가장 큰 장점은 IDE의 도움을 받을 수 있다는 것과 컴파일 타임에 오류를 잡아서쿼리 때문에 실수를 할 일이 없음

① QueryDslConfig.java

  • 먼저 JPAQueryFactory를 Bean으로 등록하여 프로젝트 전역에서 QueryDSL을 작성할 수 있도록 함

@Configuration
public class QueryDslConfig {

	@PersistenceContext
	private EntityManager entityManager;

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

② CartDetailRepository.java

  • 기존의 CartDetailRepository에서 사용한 쿼리 메서드를 삭제하고 동일한 메서드 시그니처를 새로운 커스텀 인터페이스에 정의
public interface CartDetailRepository {
	 CartDetail findByProductId(Long productId);
}

③ CartDetailRepositoryImpl.java

@Repository
public class CartDetailRepositoryImpl implements CartDetailRepository {
	
	private final JPAQueryFactory jpaQueryFactory;

	public CartDetailRepositoryImpl(JPAQueryFactory jpaQueryFactory) {
		this.jpaQueryFactory = jpaQueryFactory;
	}

	@Override
	public CartDetail findByProductId(Long productId) {
		return jpaQueryFactory.selectFrom(QCartDetail.cartDetail)
							.Join(QCartDetail.cartDetail.product,QProduct.product)
							.where(QCartDetail.cartDetail.Product.productId.eq(productId)
							.fetch();
	}
}