서론
JPA와 MyBatis의 자바 ORM을 모두 경험한 결과,
MyBatis는 SQL 쿼리에 더 직접적인 접근을 가지고 JPA는 객체지향적인 접근을 가지고 있어서
정적 쿼리를 다루는 것은 JPA가 비교적 간단하고 직관적이지만
동적 쿼리를 다루는 것은, JPA가 상대적으로 어려움(엔티티와 속성의 제약)이 있다.
JPA의 장점을 더 살리기 위해 쿼리를 분리화 해서 유지 관리하기 쉬운 방법을 모색했고,
RepositoryImpl 클래스 내부에서
QueryDSL을 사용하여 쿼리를 생성하고 실행하는 방식을 찾았다.
이번 포스팅은 그 방식을 spring 코드 예시로 설명하며 정리해보려고 한다.
Repository에서 QueryDSL 사용하기
public interface OrderRepository extends JpaRepository<Order, Long> {
@Query("select o from Order o join fetch o.orderItems")
List<Order> findAllInnerFetchJoin();
@Query("select distinct o from Order o join fetch o.orderItems")
List<Order> findAllInnerFetchJoinWithDistinct();
//...
}
현재 OrderRepository가 사용 중인 정적 쿼리(JPQL)들을 QueryDSL로 교체하려고 한다.
Spring Data JPA는 JpaRepository를 상속한 Repository 클래스에서,
Custom Repository 기능을 사용할 수 있도록 하는 기능을 제공한다.
이를 RepositoryImpl에 추가하여 사용하면
Spring Data JPA의 편리함과 장점을 그대로 유지하면서도 쿼리 작성에 더 많은 유연성과 편의성을 줄 것이라고 생각한다.
Custom Repository
public interface OrderCustomRepository {
List<Order> findAllInnerFetchJoin();
List<Order> findAllInnerFetchJoinWithDistinct();
}
기존의 OrderRepository 인터페이스의
findAllInnerFetchJoin(),
findAllInnerFetchJoinWithDistinct() 메서드와
동일한 메서드를 가지는 새로운 커스텀 인터페이스에 정의한다.
RepositoryImpl
import static jpabook.jpashop.domain.QMember.member;
import static jpabook.jpashop.domain.QOrder.order;
import javax.persistence.EntityManager;
import com.querydsl.jpa.impl.JPAQueryFactory;
@Repository
public class OrderRepositoryImpl implements OrderCustomRepository{
private final EntityManager em;
private final JPAQueryFactory query;
public OrderRepositoryImpl(EntityManager em) {
this.em = em;
this.query = new JPAQueryFactory(em);
}
@Override
public List<Order> findAllInnerFetchJoin() {
return query.selectFrom(order)
.innerJoin(Order.orderItems)
.fetchJoin()
.fetch();
}
@Override
public List<Order> findAllInnerFetchJoinWithDistinct() {
return query.selectFrom(order)
.distinct()
.innerJoin(order.orderItems)
.fetchJoin()
.fetch();
}
}
EntityManager는 JPA를 사용하여 데이터베이스와 상호작용하는 데 사용되는 객체
- JPAQueryFactory는 QueryDSL을 사용하여 쿼리를 생성하고 실행하는 데 사용되는 객체
이 클래스들은 Spring의 의존성 주입(DI)을 통해 주입되며, 생성자에서 초기화한다.
커스텀 인터페이스를 구현하는 클래스에 QueryDSL 쿼리를 작성하면 복잡한 쿼리를 더욱 가독성 있게 작성하고,
fetch join을 사용하여 성능을 최적화를 할 수 있다.
이때, 해당 구현 클래스 이름을 Impl로 하는 이유
"Impl"은 Spring Data JPA에서 Repository 인터페이스의 구현체를 나타내는 데 사용되며,
사용자 정의 쿼리나 동작을 구현하기 위해 추가되는 클래스를 의미
변경된 OrderRepository
public interface OrderRepository extends JpaRepository<Order, Long>, OrderCustomRepository {
/*
삭제된 JPQL 정적 쿼리 메서드
@Query("select o from Order o join fetch o.orderItems")
List<Order> findAllInnerFetchJoin();
@Query("select distinct o from Order o join fetch o.orderItems")
List<Order> findAllInnerFetchJoinWithDistinct();
*/
//...
}
JpaRepository를 상속하는 OrderRepository가 OrderCustomRepository 인터페이스를 상속하도록 한다.
OrderCustomRepositoryImpl에 작성된 QueryDSL 코드를 OrderRepository가 자동으로 사용할 수 있게 된다.
RepositoryImpl의 장점
- 코드 분리 및 모듈화
- 동적인 쿼리 로직을 별도의 클래스에 모듈화함으로써 코드의 가독성을 높이고 유지보수 용이
- 단일 책임 원칙
- RepositoryImpl 클래스는 쿼리 로직에 집중하게 되고, Repository 인터페이스는 데이터 액세스 관련 메서드에 집중
- 재사용성
- 동적인 쿼리 로직이 별도의 클래스에 캡슐화되면 이를 다른 Repository에서도 재사용 가능
- 테스트 용이성
- 인터페이스 구현체로 Mocking을 통한 테스트가 용이
- 동적 쿼리 관리
- RepositoryImpl에서 중앙 집중적으로 관리 가능
정리
이러한 접근 방식을 선택하기 전에 프로젝트의 구조와 요구 사항을 고려해야 한다고 한다.
일부 프로젝트에서는 QueryDSL을 사용하는 동적인 쿼리 로직을
Repository 내부에 넣는 것을 선호하는 경우도 있지만
JPA를 다루며 만드는 프로젝트이니 Impl로 분리화하여 코드를 작성하고 난 후 어느 것이 적합한지 확인하고
프로젝트 회고를 남기도록 하자.
참고자료:
'Back-End > Spring' 카테고리의 다른 글
[Exception] Custom Exception을 언제 써야할까? (0) | 2023.09.18 |
---|---|
[Spring] Rest-Assured 알아보기 (0) | 2023.08.30 |
[MVC] DTO 역할 구분, 프로젝트 흐름도 분석 (0) | 2023.05.09 |
[JPA] Entity를 Dto로 변환(리턴)의 중요성 (0) | 2023.05.08 |
[JPA] 도메인 설계, 엔티티 매핑 (0) | 2023.04.19 |