JPA

12장 Spring Data JPA

iksadnorth 2023. 9. 6. 19:41

해당 게시물은 책 '자바 ORM 표준 프로그래밍'을 읽고 작성했습니다.

👣 개요

실제로 Spring Boot에서 JPA를 사용할 때는 직접 메서드를 구현할 필요가 없다.
Spring Data JPA를 이용하면 이미 많이 정형화된 Id를 이용한 단일 조회, 전체 조회, 생성, 삭제 등등
영속성 컨텍스트를 이용하는 메서드는 구현이 된 상태로 사용 가능하다.

어째서 JpaRepository를 상속받은 Repository를 정의하는 것만으로도 
위와 같은 혜택을 누릴 수 있는지와 QueryDSL과 같은 기술과 접목시키는 방법 등을
해당 게시물에 기록할 예정이다.

 

👣 환경 설정

Spring Boot의 Gradle에 다음과 같은 설정이면 Spring Data JPA를 사용할 수 있다.

dependencies {
    ...

    // JPA
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
}

Repository 설정에 관해 @EnableJpaRepositories를 이용해서 각종 설정을 수행할 수 있다.

@Configuration
@EnableJpaRepositories(
    basePackages = "com.example.root", // Repository를 스캔할 최상위 디렉토리.
    repositoryImplementationPostfix = "Impl", // 추가 메서드 구현 시, 구현체의 접미사.
    ...
)
public class Appconfig { ... }

 

👣 공통 인터페이스 기능

JpaRepository는 간단한 CRUD 기능을 공통으로 처리하는 인터페이스와 구현체를 제공한다.
JpaRepository는 SimpleJpaRepository이며 해당 구현체는 미리 Bean으로 등록되기 때문에 
사용자는 단순히 JpaRepository를 상속한 인터페이스를 정의하기만 하면 된다.

public interface OrderRepository extends JpaRepository<Order, Long> {}

JpaRepository<T, ID>에서
T는 CRUD에 사용될 엔티티 클래스를 의미하고
ID는 해당 엔티티의 @Id가 매핑된 필드의 타입을 의미한다.

JpaRepository는 위와 같은 인터페이스들을 상속받고 있으며
기본적으로 제공하는 것들은 모두 JPQL이 아닌 영속성 컨텍스트를 이용한 연산들이다.

 

👣 쿼리 메서드 기능

CRUD 기능만 제공한다면 너무 제한적인 기능만 제공하는 것이기에
각종 복잡한 쿼리를 사용하는 메서드를 사용자가 정의하면
일정 규칙에 따라 정의된 메서드명을 토대로 쿼리를 자동으로 작성해주는
기능이 존재한다. 해당 기능을 쿼리 메서드 기능이라고 하는데 크게 3가지 기능이 있다.

1. 메서드명으로 쿼리 생성
2. 메서드명으로 NamedQuery 호출
3. @Query Annotation으로 직접 JPQL 문 작성.

 

👣 1. 메서드명으로 쿼리 생성

Order 엔티티가 다음과 같을 때,

@Entity @Table(name = "order")
@Getter @Setter @NoArgsConstructor
public class Order {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;

    @Column(name = "created_at")
    private LocalDateTime createdAt;

    @ManyToOne
    @JoinColumn(nullable = false)
    private Member member;

    @ManyToOne
    @JoinColumn(nullable = false)
    private Item item;
}

Order의 날짜가 특정 범위 내부에 있는 데이터를 조회하는 쿼리는 다음과 같이 작성할 수 있다.

public interface OrderRepository extends JpaRepository<Order, Long> {
    List<Order> findByCreatedAtBetween(LocalDateTime from, LocalDateTime to);
}

물론 메서드명명 규칙이 따로 정해져 있기에 해당 규칙에 맞춰서 메서드를 정해야 한다.
물론 해당 방법은 JPQL로 실행되기에 영속성 컨텍스트를 활용하지 않는다.
충분히 JpaRepository의 메서드로도 구현되는 기능이면 해당 기능은 이용하지 않는 것이 좋다.

 

Spring Data JPA - Reference Documentation

Example 121. Using @Transactional at query methods @Transactional(readOnly = true) interface UserRepository extends JpaRepository { List findByLastname(String lastname); @Modifying @Transactional @Query("delete from User u where u.active = false") void del

docs.spring.io

 

👣 2. 메서드명으로 NamedQuery 호출

만약 다음과 같이 정의된 NamedQuery가 존재한다면,

@NamedQueries({
        @NamedQuery(
                name = "Order.findByDateTimeRange",
                query = "select o from Order as o where o.createdAt between :from and :to"
        )
})
@Entity @Table(name = "order")
public class Order { ... }

JpaRepository에서는 다음과 같이 정의하면 된다.
물론 해당 방법은 JPQL로 실행되기에 영속성 컨텍스트를 활용하지 않는다.
충분히 JpaRepository의 메서드로도 구현되는 기능이면 해당 기능은 이용하지 않는 것이 좋다.

public interface OrderRepository extends JpaRepository<Order, Long> {
    List<Order> findByDateTimeRange(
        @Param("from") LocalDateTime from, 
        @Param("to") LocalDateTime to
    );
}

 

👣 3. @Query Annotation으로 직접 JPQL 문 작성

만약 직접 메서드에 쿼리를 작성하고 싶다면 해당 방법을 사용할 수도 있다.
1번 방법처럼 명명 규칙을 따르게 된다면 가독성이 떨어져서 한 눈에 메서드가 의도하는 바를 
깨닫지 못할 수도 있으며 구현 내용을 쉽사리 변경하기 어려워 Repository로 계층을 분리한
이유가 퇴색될 수도 있다. 때문에 해당 방법을 이용해서 결합도는 떨구고 가독성은 높일 수 있다.

1번의 쿼리를 그대로 적용한 메서드는 다음과 같이 구현할 수 있다.

public interface OrderRepository extends JpaRepository<Order, Long> {
    @Query("select o from Order as o where o.createdAt between :from and :to")
    List<Order> findByDateTimeRange(@Param("from") LocalDateTime from, @Param("to") LocalDateTime to);
}

 

👣 벌크성 삽입, 수정, 삭제 쿼리

만약 기존의 CRUD가 아닌 방법으로 Insert, Update, Delete 쿼리를 호출하고 싶다면 
@Query 어노테이션 위에 @Modifying 어노테이션을 붙이면 된다.

public interface OrderRepository extends JpaRepository<Order, Long> {
    @Modifying
    @Query("delete from Order as o where o.createdAt between :from and :to")
    void deleteAllByDateTimeRange(@Param("from") LocalDateTime from, @Param("to") LocalDateTime to);
}

 

👣 페이징, 정렬

단순히 복수 개의 결과값을 List로 출력하는 것이 아닌
Page로 출력해서 최적화를 이루고 싶다면
Pageable 객체를 파라미터로 부여하고 출력 다입을 Page로 반환하는 메서드로
새롭게 재정의하면 페이징은 물론이고 정렬 기능도 부여된다.

Page를 반환 타입으로 사용하면 전체 페이지 갯수를 따지기 위해
count 쿼리가 추가로 호출되므로 주의해야 한다.

// count 쿼리 사용
@Query("select o from Order as o where o.createdAt between :from and :to")
Page<Order> findByDateTimeRange(
        @Param("from") LocalDateTime from, 
        @Param("to") LocalDateTime to,
        Pageable pageable
);

// count 쿼리 사용 X
@Query("select o from Order as o where o.createdAt between :from and :to")
List<Order> findByDateTimeRange(
        @Param("from") LocalDateTime from, 
        @Param("to") LocalDateTime to,
        Pageable pageable
);

 

👣 사용자 정의 리포지토리 구현

Spring Data JPA를 이용하면 언젠가 직접 메서드 구현을 할 필요가 생긴다.
[Ex) QueryDSL을 이용한 DB 통신 구현]
때문에 Spring Data JPA는 일정 규칙 하에 메서드 직접 구현을 지원한다.

JpaRepository를 확장한 Entity Repository가 있을 것이다.
이것을 이하 'entity repo'라고 명명한다.

public interface OrderRepository extends JpaRepository<Order, Long> {
}

사용자가 직접 구현하고 싶은 메서드를 머금은 Custom Repository가 있을 것이다.
이것을 이하 'custom repo'라고 명명한다.

public interface CustomOrderRepository {
    List<Order> findRandom(Pageable pageable);
}

custom repo를 entity repo가 상속하게 만든다.

public interface OrderRepository extends JpaRepository<Order, Long>, CustomOrderRepository {
}

그리고 custom repo의 구현체를 만든다.
이 때, 구현체의 이름은 'entity repo의 이름' + 'Impl' 여야 한다.
구현체명으로 매핑을 하는 것이므로 이것을 꼭 지켜야 한다.

public class OrderRepositoryImpl implements CustomOrderRepository {
    @PersistenceContext
    EntityManager em;
    Random random = new Random();
    
    @Override
    public List<Order> findRandom(Pageable pageable) {
        JPAQuery<Order> query = new JPAQuery<>(em);
        QOrder order = QOrder.order;

        Long count = query.select(order.count())
                .from(order)
                .fetchOne();

        List<Long> randoms = random.longs(count/5, 0, count)
                .boxed().toList();

        return query.from(order)
                .where(order.id.in(randoms))
                .fetch();
    }
}

 

👣 Spring Data JPA와 QueryDSL 통합

Spring Data JPA에서 QueryDSL을 쉽게 사용하는 방법은 2가지가 있다.
1. QuerydslPredicateExecutor
2. QuerydslRepositorySupport

1. QuerydslPredicateExecutor
해당 인터페이스는 entity repo에 확장시켜서 사용하는 것으로 
아래 그림처럼 findByXxx, findAllByXxx 같은 메서드에
QueryDSL의 where 절을 넣을 수 있게 설계되어 있다.

하지만 이런 형태는 QueryDSL의 프로젝션, from, join 등등의 기능을 사용할 수 없다.

2. QuerydslRepositorySupport
해당 추상 클래스는 custom repo의 구현체에서 사용하는 것으로
기존의 QueryDSL과 다르게 JPAQuery를 미리 만들어주고
심지어 QClass 객체도 getBuilder() 메서드로 받을 수 있게 도와준다.

해당 방법은 JPAQuery 부터 지원하는 것이기에 QueryDSL의 모든 기능을 사용할 수 있게 도와준다.
다만, 1번 방법에 비해 사용법이 복잡하고 생성해야 하는 파일이 많다.

 

👣 '우아한 형제들'에서의 QueryDSL 사용 방법

 

우아한 형제들의 Querydsl 사용법

이 글은 "우아한테크콘서트2020 수십억건에서 Querydsl 사용하기" 와 발표자이신 이동욱님의 기술 블로그를 보고 작성한 글입니다. 모든 예제와 추가로 Querydsl 사용 문법은 https://github.com/Youngerjesus/Q

velog.io

우아한 형제들은 QueryDSL을 위에서 제시한 방법과는 살짝 다른 방식으로
Spring Data JPA와 연동해서 사용한다.

기존 방법의 경우, 불필요한 상속과 구현이 많기 때문에 
코드 양이 많아지고 가독성을 떨어뜨리는 경우가 존재한다.

그래서 우아한 형제들은 QueryDSL을 사용하기 위해 다음 방법을 사용한다.
아래와 같이 JPAQueryFactory를 Bean으로 등록하고
필요한 구현체에서는 QuerydslRepositorySupport를 상속하는 것이 아니라
JPAQueryFactory로 Query 객체를 생성한다.

아래와 같이 JPAQueryFactory로 쿼리를 만들면
훨씬 실제 쿼리와 비슷한 어순으로 작성할 수도 있다.

@Configuration
public class QuerydslConfiguration {
    @Autowired
    EntityManager em;

    @Bean
    public JPAQueryFactory jpaQueryFactory() {
       return new JPAQueryFactory(em);
    }
}
@RequiredArgsConstructor
public class OrderRepositoryImpl implements CustomOrderRepository {
    private final JPAQueryFactory queryFactory;
    private final Random random = new Random();

    @Override
    public List<Order> findRandom(Pageable pageable) {
        Long count = queryFactory
                .select(order.count()).from(order)
                .fetchOne();

        List<Long> randoms = random.longs(count/5, 0, count)
                .boxed().toList();

        return queryFactory
                .select(order).from(order)
                .where(order.id.in(randoms))
                .fetch();
    }
}

 

'JPA' 카테고리의 다른 글

14장 컬렉션과 부가 기능  (0) 2023.09.08
13장 웹 애플리케이션과 영속성 관리  (0) 2023.09.07
영속성 컨텍스트 vs JPQL  (0) 2023.09.06
QueryDSL  (0) 2023.09.06
10장 객체지향 쿼리 언어  (0) 2023.09.04