Spring Data R2DBC | 머리말

Spring Data R2DBC 프로젝트는 핵심 Spring 개념을 관계형 데이터베이스로 R2DBC 드라이버를 사용하는 솔루션 개발에 적용한다. 행(row)을 저장 및 조회하기 위한 고레벨(High-leval)의 추상화로 DatabaseClient를 제공한다.

이 문서는 Spring Data-R2DBC 지원 레퍼런스 가이드이다. R2DBC 모듈의 개념과 의미에 대해 설명한다.

이 섹션에서는 Spring 및 데이터베이스의 기본 개요를 제공한다.

1. Spring 학습

Spring Data는 다음을 포함한 Spring 프레임워크의 코어 기능을 사용한다.

Spring API를 알 필요는 없지만 그 배경이 되는 개념을 이해하는 것이 중요하다. 최소한, Inversion of Control(IoC)의 배경이 되는 개념은 알아야 하며, 사용하는 IoC 컨테이너에도 익숙해야 한다.

Spring 컨테이너의 IoC 서비스를 호출할 필요 없이, R2DBC 지원의 코어 기능을 직접 사용할 수 있다. 이는 Spring 컨테이너의 다른 서비스 없이 “standalone(독립 실행형)“으로 사용할 수 있는 JdbcTemplate과 매우 유사하다. 리포지토리 지원과 같은 Spring Data R2DBC의 모든 기능을 사용하려면 Spring을 사용하도록 라이브러리의 일부를 구성해야 한다.

Spring에 대한 자세한 내용은 Spring Framework에 대해 자세히 설명하는 포괄적인 문서를 참조하여라. 이 주제에 관한 기사, 블로그 항목, 책은 많이 있다. 자세한 내용은 Spring 프레임워크 홈페이지를 참조하여라.


2. R2DBC란 무엇인가?

R2DBC는 Reactive Relational Database Connectivity의 약자이다. R2DBC는, 드라이버 공급업체가 관계형 데이타베이스에 액세스 하기 위해서 구현한 리액티브 API를 선언하는 API 사양 이니셔티브(initiative)이다.

이니셔티브(initiative)이란? 주도권이라는 뜻을 가지고 있다.

R2DBC가 만들어진 이유 중 하나는 적은 스레드로 동시성을 처리하기 위해, 더 적은 하드웨어 리소스로 확장할 수 있는 논-블로킹 애플리케이션 스택이 필요해서 이다. JDBC는 완전한 블로킹 API 이기 때문에, 표준화된 관계형 데이타베이스 액세스 API(즉, JDBC)를 재이용하더라도 이 요구를 충족할 수가 없다. ThreadPool를 사용하여 블로킹 동작을 비슷하게 구현할 수도 있지만 이는 제약 사항이 많을 것이다.

또 다른 이유는 대부분의 많은 응용 프로그램이 관계형 데이터베이스를 사용하여 데이터를 저장을 하고 있다는 것이다. 여러 NoSQL 데이터베이스 공급업체에서는 데이터베이스에 반응형 데이터베이스 클라이언트를 제공하고 있지만, 대부분의 프로젝트가 NoSQL로의 마이그레이션을 할 수는 없다. 이것이 새로운 공통 API가 논-블로킹 데이터베이스 드라이버의 기초 역할을 하게 된 동기였다. 오픈 소스 에코시스템은 다양한 논-블록킹 관계형 데이터베이스 드라이버 구현을 호스팅하지만, 각 클라이언트에는 공급 업체별 API가 제공되므로, 이러한 라이브러리 위에 일반 계층을 배치하는 것을 불가능하다.


3. 리액티브란 무엇인까?

“리액티브(반응형)“이라는 용어는 I/O 이벤트에 반응하는 네트워크 컴포넌트, 마우스 이벤트에 반응하는 UI 컨트롤러, 사용 가능한 리소스 등에 대한 변경, 가용성 및 처리 가능성에 대한 반응을 기반으로 구축된 프로그래밍 모델을 의미한다. 그런 의미에서 논-블로킹은 “리액티브"라고 할 수 있다. 왜냐하면, 차단되는 것이 아니라 작업이 완료되거나 데이터가 사용할 수 있게 되었을 때에 알림에 반응하는 모드로 되기 때문이다.

또한, Spring 팀에서 리액티브와 연결하는 또 다른 중요한 메커니즘도 있는데, 그것은 논-블로킹 백-프레셔이다. 동기식 명령형 코드에서, 호출을 차단하는 것은 호출하는 측에서 대기시키는 백-프레셔의 자연적인 형태로서 기능하다. 논-블로킹 코드를 사용하면 빠른 프로듀서(생산자)가 대상을 압도하지 않도록 이벤트의 속도를 제어하는 것은 필수이다.

Reactive Streams는 백-프레셔가 있는 비동기 구성 요소 간의 상호 작용을 정의하는 작은 사양(Java 9에서도 채택)이다. 예를 들어, 데이터 리포지토리(Publisher기능)은 HTTP 서버(Subscriber로 기능)이 응답에 쓸 수 있는 데이터를 생성할 수 있다. Reactive Streams의 주요 목적은 구독자가 게시자가 데이터를 생성하는 속도를 제어할 수 있도록 하는 것이다.


4. 리액티브 API

Reactive Streams는 상호 운용성에 중요한 역할을 수행한다. 라이브러리와 인프라 컨포넌트(구성 요소)에 관심이 있지만, 너무 낮기 레벨이어서 응용 프로그램 API로는 유용하지 않다. 애플리케이션은 비동기 로직을 구성하기 위해 더 높은 레벨의 더 많은 함수 API가 필요하다. 이는 Java 8 Stream API와 유사하지만 테이블용이 아니다. 이것이 리액티브 라이브러리가 하는 역할이다.

Project Reactor는 Spring Data R2DBC에서 선택한 리액티브 라이브러리이다. 연산자의 ReactiveX 어휘와 정렬된 풍부한 연산자 집합을 통해 0..1(Mono)와 0..N(Flux) 의 데이터 시퀀스에 대해 작업할 수 있는 MonoFlux API 유형을 제공한다. Reactor는 Reactive Streams 라이브러리이므로 모든 연산자가 논-블로킹 백=프레셔를 지원한다. Reactor는 서버 사이드 Java에 중점을 두고 있고, Spring과 긴밀히 협력하여 개발되다.

Spring Data R2DBC는 코어 종속성으로 Project Reactor를 필요로 하지만 Reactive Streams 사양을 통해 다른 리액티브 라이브러리와 상호 운용 가능하다. 일반적으로 Spring Data R2DBC 리포지토리는 일반 Publisher을 입력으로 받아들이고 내부적으로 Reactor 유형에 적용하여 이를 사용하고 Mono 혹은 Flux 중에 하나를 출력으로 반환한다. 그래서 모든 Publisher를 입력으로 전달하고 출력에 작업을 적용할 수 있다. 다만, 다른 리액티브 라이브러리와 함께 사용하려면 출력을 조정해야 한다. 가능한 경우에 언제든지 Spring Data는 RxJava 또는 다른 리액티브 라이브러리의 사용에 투명하게 적응한다.


5. 요구 사항

Spring Data R2DBC 1.x 바이너리에는 다음이 필요하다.


6. 추가 도움말 리소스

새로운 프레임워크를 배우는 것은 항상 쉬운 일이 아니다. 이 섹션에서는 Spring Data R2DBC 모듈을 시작하기 위해 따라하기 쉬운 가이드를 제공하려고 한다. 혹시 하다가 문제가 발생하거나 조언이 필요하다면, 다음 링크 중 하나를 사용하도록 하자.

  • 커뮤니티 포럼
    스택 오버플로에서 Spring Data는 R2DBC뿐만 아니라 모든 Spring Data 사용자가 정보를 공유하고 서로 돕기 위한 태그이다. 참고로 계정 등록은 게시를 하는 경우에는 필요하다.
  • 전문적인 지원
    응답 시간이 보장된 소스로부터의 전문적인 지원은 Spring Data 및 Spring을 지원하는 회사인 Pivotal Sofware, Inc.에서 제공된다.

7. 개발 팔로우

  • Spring Data R2DBC 소스 코드 리포지토리, 최신 빌드(nightly build) 및 스냅샷 아티팩트(snapshot artifacts)에 대한 자세한 내용은 Spring Data R2DBC 홈페이지를 참조하여라.
  • 스택 오버플로의 커뮤니티를 통해 개발자와 상호 작용하여, Spring Data가 Spring 커뮤니티의 요구 사항에 가장 잘 부합하도록 만들 수 있다.
  • 버그가 발생하거나 개선을 제안하고 싶다면, Spring Data R2DBC 이슈 트레킹 시스템에서 티켓을 생성한다.
  • Spring 생태계에 대한 최신 뉴스와 최신 정보를 얻으려면 Spring 커뮤니티 포털을 구독한다.
  • Twitter(SpringData)에서 Spring 블로그 또는 Spring Data 프로젝트 팀을 팔로우할 수도 있다.

8. 프로젝트 메타데이터


9 주목할만한 새로운 기능

9.1. Spring Data R2DBC 1.3.0의 새로운 기능

  • 예시에 의한 문의(Query By Example)를 소개한다.

9.2. Spring Data R2DBC 1.2.0의 새로운 기능

  • Spring Data R2DBC DatabaseClient를 더 이상 사용하지 않고, Spring R2DBC를 위해 패지된 API로 이동되었다. 자세한 내용은 이전 가이드를 참조한다.
  • 엔티티 콜백 지원.
  • @EnableR2dbcAuditing을 통한 검사.
  • 영속성 생성자 @Value의 지원.
  • Oracle의 R2DBC 드라이버 지원.

9.3. Spring Data R2DBC 1.1.0의 새로운 기능

  • 엔티티 지향 조작을 위한 R2dbcEntityTemplate 소개.
  • 쿼리 파생.
  • DatabaseClient.as(...)로 인터페이스 프로젝션(interface projections)을 지원.
  • DatabaseClient.filter(...)에 의하여 ExecuteFunction, StatementFilterFunction 지원.

9.4. Spring Data R2DBC 1.0.0의 새로운 기능

  • R2DBC 0.8.0.RELEASE에서 업그레이드된다.
  • @Modifying 영향을 받는 행 수를 사용하는 쿼리 메소드의 어노테이션.
  • 연결된 ID가 있는 저장소에 데이터베이스에 행이 없으면 save(...)를 실행하면 TransientDataAccessException이 발생한다.
  • 연결 싱글 톤을 사용한 테스트에 SingleConnectionConnectionFactory 추가되었다.
  • @Query에서 SpEL 표현식 지원.
  • AbstractRoutingConnectionFactory를 통해 ConnectionFactory 라우팅.
  • ResourceDatabasePopulator 그리고 ScriptUtils 스키마를 초기화하는 유틸리티.
  • TransactionDefinition를 통한 Auto-Commit 및 분리 레벨 컨트롤 전파 및 재설정.
  • 엔티티 레벨 컨버터 지원.
  • 구상 제네릭스와 코루틴의 Kotlin 확장.
  • 다이얼렉트(dialects)를 등록하기 위한 플러그 가능한 메커니즘을 추가.
  • 명명된 매개변수 지원.
  • DatabaseClient에 의한 초기 R2DBC 지원.
  • TransactionalDatabaseClient에 의한 초기 거래 지원.
  • R2dbcRepository에 의한 초기 R2DBC 리포지토리 지원.
  • Postgres 및 Microsoft SQL Server의 첫 번째 다이얼렉트 지원.

10. 종속성

개별 Spring Data 모듈의 시작일이 다르기 때문에, 대부분은 다른 메이저 및 마이너 버전 번호를 가지고 있다. 호환이 되는 것을 찾는 가장 간단한 방법은, 호환이 가능한 버전과 함께 제공되는 Spring Data 릴리스 트레인 BOM에 의존하는 것이다. Maven 프로젝트는 다음과 같이 <dependencyManagement /> POM 섹션에서 이 종속성을 선언한다.

예 1: Spring Data 릴리스 트레인 BOM 사용

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.data</groupId>
      <artifactId>spring-data-bom</artifactId>
      <version>2021.1.0</version>
      <scope>import</scope>
      <type>pom</type>
    </dependency>
  </dependencies>
</dependencyManagement>

현재 릴리스 트레인(train) 버전는 2021.1.0이다. 트레인 버전에서, 패턴 YYYY.MINOR.MICROcalver를 사용하고 있다. 버전 이름은 GA 릴리스 및 서비스 릴리스 ${calver}에 따라, 다른 모든 버전에서는 다음 패턴을 따른다. ${calver}-${modifier}. modifier는 다음 중 하나이다.

  • SNAPSHOT: 현재 스냅샷
  • M1, M2 등: 마일드스톤
  • RC1, RC2 등: 출시 후보(Release Candidate)

Spring Data 샘플 리포지토리에서 BOM 사용 예제를 찾을 수 있다. 이를 사용하여 다음과 같이 <dependencies /> 블록으로 버전 없이 사용하려는 Spring Data 모듈을 선언할 수 있다.

예 2: Spring Data 모듈에 대한 종속성 선언

<dependencies>
  <dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-jpa</artifactId>
  </dependency>
<dependencies>

10.1. Spring Boot를 사용한 종속성 관리

Spring Boot는 Spring Data 모듈의 최신 버전을 선택한다. 이보다 더 새 버전으로 업그레이드하려면, spring-data-releasetrain.version 속성을 사용려는 트레인 버전(train version)과 인터렉션(iteration)을 설정하면 된다.

10.2. Spring Framework

현재 버전의 Spring Data 모듈에는 Spring Framework 5.3.13 이상이 필요하다. 모듈은 마이너 버전의 이전 버그 수정 버전에서도 작동할 수 있다. 그러나 해당 세대에서 가장 최신 버전을 사용하는 것이 좋다.


11. Spring Data 저장소 작업

Spring Data 리포지토리의 추상화의 목표는, 다양한 영속 스토어의 데이터 액세스 계층을 구현하기데 필요한 보일러 플레이트(boilerplate) 코드의 양을 대폭 줄이는 것이다.

11.1. 핵심 개념

Spring Data 저장소 추상화의 핵심 인터페이스는 Repository이다. 관리할 도메인 클래스와 도메인 클래스의 ID 유형을 유형 인수로 사용한다. 이 인터페이스는 주로 사용할 유형을 캡처하고 이 인터페이스를 상속하는 인터페이스를 찾는데 도움이 되는 마커 인터페이스 역할을 한다. CrudRepository 인터페이스는 관리되고 있는 엔티티 클래스에 고도의 CRUD 기능을 제공한다.

예 3: CrudRepository 인터페이스

public interface CrudRepository<T, ID> extends Repository<T, ID> {

  <S extends T> S save(S entity);      // (1)

  Optional<T> findById(ID primaryKey); // (2)

  Iterable<T> findAll();               // (3)

  long count();                        // (4)

  void delete(T entity);               // (5)

  boolean existsById(ID primaryKey);   // (6)

  // ... more functionality omitted.
}

(1) 지정된 엔티티를 저장한다.
(2) 지정된 ID로 식별되는 엔티티를 반환한다.
(3) 모든 엔티티를 반환한다.
(4) 엔티티의 수를 반환한다.
(5) 지정된 엔티티를 삭제한다.
(6) 지정된 ID의 엔티티가 존재하는지에 대한 여부를 반환한다.

CrudRepository를 상위로 하여 PagingAndSortingRepository는 엔티티에 페이지 붙일 수 있었던 액세스를 용이하게 하는 추가의 메소드를 추가한 추상화가 있다.

예 4: PagingAndSortingRepository 인터페이스

public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {

  Iterable<T> findAll(Sort sort);

  Page<T> findAll(Pageable pageable);
}

페이지 크기가 20인 User의 두번째 페이지에 액세스하려면 다음을 수행할 수 있다.

PagingAndSortingRepository<User, Long> repository = // ... get access to a bean
Page<User> users = repository.findAll(PageRequest.of(1, 20)); // 페이지는 0부터 시작하기에 1이 들어간다.

쿼리 메소드 외에도 카운트 쿼리와 삭제 쿼리 모두 쿼리 파생을 사용할 수 있다. 다음 목록은 파생 카운트 쿼리의 인터페이스 정의를 보여 준다. 예 5: 파생 카운트 쿼리

interface UserRepository extends CrudRepository<User, Long> {

  long countByLastname(String lastname);
}

다음 목록은 파생된 삭제 쿼리의 인터페이스 정의를 보여 준다. 예 6: 파생 삭제 쿼리

interface UserRepository extends CrudRepository<User, Long> {

  long deleteByLastname(String lastname);

  List<User> removeByLastname(String lastname);
}

11.2. 쿼리 메소드

일반적으로 표준 CRUD 기능 리포지토리에는 기본 데이터 저장소에 대한 쿼리가 있다. Spring Data에서는 이러한 쿼리를 선언하는 것은 4단계 프로세스이다.

  1. 다음 예와 같이 리포지토리 또는 하위 인터페이스 중 하나를 상속하는 인터페이스를 선언하고, 처리할 도메인 클래스와 ID 유형을 입력한다.

    interface PersonRepository extends Repository<Person, Long> { ... }
    
  2. 인터페이스에서 쿼리 메소드를 선언한다.

    interface PersonRepository extends Repository<Person, Long> {
      List<Person> findByLastname(String lastname);
    }
    
  3. JavaConfig 또는 XML 구성을 사용하여 Spring을 설정하여 이러한 인터페이스의 프록시 인스턴스를 작성한다.

    a. Java 구성을 사용하려면 다음과 유사한 클래스를 작성한다.

    import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
    
    @EnableJpaRepositories
    class Config { ... }
    

    b. XML 구성을 사용하려면 다음과 같은 Bean을 정의한다.

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:jpa="http://www.springframework.org/schema/data/jpa"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
         https://www.springframework.org/schema/beans/spring-beans.xsd
         http://www.springframework.org/schema/data/jpa
         https://www.springframework.org/schema/data/jpa/spring-jpa.xsd">
    
       <jpa:repositories base-package="com.acme.repositories"/>
    
    </beans>
    

    이 예에서는, JPA 네임스페이스를 사용하고 있다. 저장소의 추상화를 다른 저장소에 사용하는 경우 저장소 모듈의 적절한 네임스페이스 선언으로 변경해야 한다. 즉, 예를 들어 mongodb를 위해 jpa를 교환해야 한다.

    또, 어노테이션 첨부 클래스의 패키지가 디폴트로 사용되기 때문에, JavaConfig 변형(variant)은 패키지를 명시적으로 구성하지 않는 것에 주의해야 한다. 스캔할 패키지를 사용자 지정하려면 데이터스토어별 리포지토리의 @Enable${store}Repositories 어노테이션의 basePackage ... 속성으로 하나를 사용한다.

  4. 다음 예와 같이 리포지토리 인스턴스를 삽입하고 사용한다.

    class SomeClient {
    
      private final PersonRepository repository;
    
      SomeClient(PersonRepository repository) {
        this.repository = repository;
      }
    
      void doSomething() {
        List<Person> persons = repository.findByLastname("Matthews");
      }
    }
    

다음 섹션에서는 각 단계에 대해 자세히 설명한다.

  • 리포지토리 인터페이스 정의
  • 쿼리 메소드 정의
  • 리포지토리 인스턴스 만들기
  • Spring Data 리포지토리의 사용자 지정 구현

11.3. 리포지토리 인터페이스 정의

리포지토리 인터페이스를 정의하려면, 먼저 도메인 클래스별 리포지토리 인터페이스를 정의해야 한다. 인터페이스는 Repository을 상속하고, 도메인 클래스와 ID 유형에 입력해야 한다. 해당 도메인 유형의 CRUD 메소드를 공개하는 경우 Repository 대신에 CrudRepository를 상속한다.

11.3.1. 리포지토리 정의 미세 조정

일반적으로 저장소 인터페이스 RepositoryCrudRepository 또는 PagingAndSortingRepository을 상속받고 있다. 또는 Spring Data 인터페이스를 상속하지 않으려면 리포지토리 인터페이스에 @RepositoryDefinition 어노테이션을 선언할 수 있다. CrudRepository를 상속하면, 엔티티를 조작하기 위한 메소드의 완전한 세트가 공개된다. 선택적으로 메소드를 공개하려면ㅡ 공개할 메소드를 CrudRepository 도메인 리포지토리로 복사한다.

다음 예제에서는 CRUD 메소드(여기에서는 findByIdsave)를 선택적으로 공개하는 방법을 보여 준다.

예 7: CRUD 메소드를 선택적으로 게시

@NoRepositoryBean
interface MyBaseRepository<T, ID> extends Repository<T, ID> {

  Optional<T> findById(ID id);

  <S extends T> S save(S entity);
}

interface UserRepository extends MyBaseRepository<User, Long> {
  User findByEmailAddress(EmailAddress emailAddress);
}

앞의 예에서는 모든 도메인 리포지토리와 공개된 findById(...)save(...)는 공통 기본 인터페이스를 정의하였다. 이런 메소드는, Spring Data 가 제공하는 선택의 스토어의 베이스 리포지터리 구현에 라우팅된다(예를 들어, JPA 를 사용하는 경우, 구현은 SimpleJpaRepository이다). 이는 CrudRepository의 메소드 서명(signatures)과 일치하기 때문이다. 따라서 UserRepository는 사용자를 저장하고, ID로 개별 사용자를 검색하고 이메일 주소로 Users 검색하는 쿼리를 트리거할 수 있다.

11.3.2. 여러 Spring Data 모듈에서 리포지토리 사용

미리 정의된 범위의 모든 리포지토리 인터페이스가 Spring Data 모듈에 바인딩되므로, 애플리케이션에서 고유한 Spring Data 모듈을 사용하면 작업이 간단해진다. 어플리케이션에 따라서는, 복수의 Spring Data 모듈을 사용할 필요가 있는 경우가 있다. 그런 경우에는 리포지토리 정의는 영속성 기술을 구별해야 한다. 클래스 경로에 여러 리포지토리 팩토리를 감지하면, Spring Data는 엄격한 리포지토리 구성 모드로 들어간다. 엄격한 구성에서는 리포지토리 또는 도메인 클래스의 상세를 사용하여 리포지토리 정의의 Spring Data 모듈 바인딩에 대해 결정한다.

  1. 리포지토리 정의가 모듈 고유의 리포지토리를 상속하는 경우, 특정의 Spring Data 모듈의 유효한 후보이다.
  2. 도메인 클래스에 모듈 고유의 형태 어노테이션이 붙어 있는 경우, 특정의 Spring Data 모듈의 유효한 후보이다. Spring Data 모듈은 타사 어노테이션(JPA @Entity 등)을 받아들이거나 독자적인 어노테이션(Spring Data MongoDB @Document 또는 Spring Data Elasticsearch 등)을 제공한다.

다음 예는 모듈 특정 인터페이스(이 경우 JPA)를 사용하는 리포지토리를 보여 준다.

예 8: 모듈별 인터페이스를 사용하여 리포지토리 정의

interface MyRepository extends JpaRepository<User, Long> { }

@NoRepositoryBean
interface MyBaseRepository<T, ID> extends JpaRepository<T, ID> { ... }

interface UserRepository extends MyBaseRepository<User, Long> { ... }

MyRepositoryUserRepository는 유형 계층 구조로 JpaRepository를 상속한다. 이 클래스들은 Spring Data JPA 모듈의 유효한 후보이다.

다음 예는 일반 인터페이스를 사용하는 리포지토리를 보여 준다. 예 9: 제네릭 인터페이스를 사용한 리포지토리 정의

interface AmbiguousRepository extends Repository<User, Long> { ... }

@NoRepositoryBean
interface MyBaseRepository<T, ID> extends CrudRepository<T, ID> { ... }

interface AmbiguousUserRepository extends MyBaseRepository<User, Long> { ... }

AmbiguousRepositoryAmbiguousUserRepository는 유형 계층에서 RepositoryCrudRepository만을 상속 받는다. 고유의 Spring Data 모듈을 사용하는 경우는 이는 문제 없겠지만, 복수의 모듈에서는 이러한 리포지터리를 어느 특정의 Spring Data에 바인드해야 할지를 구별할 수 없게 된다.

다음 예제는 어노테이션이 달린 도메인 클래스를 사용하는 리포지토리를 보여 준다. 예 10: 어노테이션이 있는 도메인 클래스를 사용하여 리포지토리 정의

interface PersonRepository extends Repository<Person, Long> { ... }

@Entity
class Person { ... }

interface UserRepository extends Repository<User, Long> { ... }

@Document
class User { ... }

PersonRepository는 JPA의 @Entity 어노테이션이 선언된 Person을 참조하기 때문에, 이 리포지토리는 분명히 Spring Data JPA에 속하게 된다. User를 참조하고 있는 UserRepository는 Spring Data MongoDB의 @Document 어노테이션이 선언된 대로 처리가 된다.

다음의 나쁜 예로 어노테이션이 혼합된 도메인 클래스를 사용하는 리포지토리를 보여 준다. 예 11: 어노테이션이 혼합된 도메인 클래스를 사용하여 리포지토리 정의

interface JpaPersonRepository extends Repository<Person, Long> { ... }

interface MongoDBPersonRepository extends Repository<Person, Long> { ... }

@Entity
@Document
class Person { ... }

이 예제는 JPA 어노테이션과 Spring Data MongoDB 어노테이션을 모두 사용하는 도메인 클래스(Person)를 볼 수 있다. JpaPersonRepositoryMongoDBPersonRepository 2개의 리포지토리를 정의되어 있다. 하나는 JPA 용이고, 다른 하나는 MongoDB에서 사용하기 위한 것이다. Spring Data는 리포지토리를 구별할 수 없게 되어, 정의되지 않은 것으로 간주하고 동작된다.

리포지토리 유형의 세부 정보 및 식별 도메인 클래스 어노테이션은, 특정의 Spring Data 모듈의 리포지토리 후보를 식별하기 위한 엄격한 리포지토리 구성에 사용된다. 동일한 도메인 유형에서 다중 영속성 기술 특정된 어노테이션을 사용할 수 있으며, 다중 영속성 기술로 도메인 유형을 재사용할 수 있다. 다만, Spring Data는 리포지토리를 바인딩할 고유 모듈을 결정할 수 없게 된다.

리포지토리를 구별하는 마지막 방법은 리포지토리 기반 패키지를 범위 지정하는 것이다. 기본 패키지는 저장소 인터페이스 정의를 스캔의 시작점에 정의한다. 이는 적절한 패키지에 리포지토리 정의가 있음을 의미한다. 기본적으로 어노테이션 기반 구성은 구성 클래스 패키지를 사용한다. XML 기반 구성의 기본 패키지는 필수이다.

다음 예제는 기본 패키지의 어노테이션 구동 구성을 보여 준다.

예 12: 기본 패키지 어노테이션 구동 구성

@EnableJpaRepositories(basePackages = "com.acme.repositories.jpa")
@EnableMongoRepositories(basePackages = "com.acme.repositories.mongo")
class Configuration { ... }

11.4. 쿼리 메소드 정의

리포지토리 프록시에는 메소드 이름에서 저장소별 쿼리를 파생시키는 두 가지 방법이 있다.

  • 메소드 이름에서 직접 쿼리를 파생한다.
  • 수동으로 정의된 쿼리를 사용한다.

사용 가능한 옵션은 실제 상점에 따라 다르다. 다만, 만드는 실제 쿼리를 결정하는 전략이 필요하다. 다음 섹션에서는 사용 가능한 옵션에 대해 설명한다.

11.4.1. 쿼리 검색 전략

쿼리를 해결하는 리포지토리 인프라에서는 다음 전략을 사용할 수 있다. XML 구성에서는 query-lookup-strategy 속성을 사용하여 네임스페이스에서 전략을 구성할 수 있다. Java 구성의 경우 Enable${store}Repositories 어노테이션의 queryLookupStrategy 속성을 사용할 수 있다. 특정 데이터 스토어는 일부 전략을 지원하지 않을 수 있다.

  • CREATE는 쿼리 메소드 이름에서 저장소별 쿼리를 만들려고 한다. 일반적인 접근(approach)은 메소드 이름에서 알려진 접두사의 특정 집합을 제거하고 메소드의 나머지를 구문 분석하는 것이다. 쿼리 작성에 대한 자세한 내용은 “쿼리 작성"을 참조한다.

  • USE_DECLARED_QUERY는 선언된 쿼리를 찾으려고 하며, 찾을 수 없는 경우 예외를 throw 한다. 쿼리는 어딘가의 어노테이션으로 정의하거나, 다른 방법으로 선언할 수 있다. 해당 저장소에서 사용 가능한 옵션을 찾으려면, 특정 저장소의 도큐먼트를 참조하여라. 리포지토리 인프라가 부트스트랩 될 시에, 메소드에 대해 선언된 쿼리를 찾아 없을 경우 실패한다.

  • CREATE_IF_NOT_FOUND(기본값)은 CREATE와의 USE_DECLARED_QUERY의 조합이다. 먼저 선언된 쿼리를 검색하고 선언된 쿼리를 찾을 수 없는 경우 사용자 지정 메소드 이름 기반 쿼리를 만든다. 이는 기본 조회 전략이므로, 명시적으로 아무 것도 구성하지 않는 경우에 사용된다. 메소드 이름으로 쿼리 정의를 신속하게 수행할 뿐만 아니라, 필요에 따라 선언된 쿼리를 도입하여 이러한 쿼리를 사용자 지정할 수도 있다.

11.4.2. 쿼리 생성

Spring Data 리포지토리 인프라에 내장된 쿼리 빌더 메커니즘은 리포지토리의 엔터티에 대한 제약 쿼리를 만들 수 있다.

다음 예제에서는 몇 가지 쿼리를 만드는 방법을 보여 준다.

예 13: 메소드 이름에서 쿼리 작성

interface PersonRepository extends Repository<Person, Long> {

  List<Person> findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);

  // Enables the distinct flag for the query
  List<Person> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);
  List<Person> findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname);

  // Enabling ignoring case for an individual property
  List<Person> findByLastnameIgnoreCase(String lastname);
  // Enabling ignoring case for all suitable properties
  List<Person> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname);

  // Enabling static ORDER BY for a query
  List<Person> findByLastnameOrderByFirstnameAsc(String lastname);
  List<Person> findByLastnameOrderByFirstnameDesc(String lastname);
}

쿼리 메소드 이름의 구문 분석은 주어와 술어로 구분된다. 첫 번째 부분(find...By, exists...By)은 쿼리의 주제를 정의하고, 두 번째 부분은 술어를 형성한다. 도입구(주어)에는 추가 표현식이 포함될 수 있다. find(또는 다른 도입 키워드)와 By 사이의 텍스트는 Distinct와 같은 결과를 제한하는 키워드 중 하나를 사용하여 만든 쿼리 또는 “Top/First는 쿼리 결과"를 제한하는 키워드 중 하나를 사용하지 않는 한 설명적인 것으로 간주한다.

부록에는 “쿼리 메소드의 주제 키워드"와 “쿼리 메소드의 술어 키워드"의 “전체 목록"이 포함되어 있다. 여기에는 “정렬 및 대소문자 한정자"가 포함된다 . 다만, 최초의 By는, 실제의 조건 술어의 시작을 나타내는 단락 문자로서 기능하다. 매우 기본적인 레벨에서는, 엔티티 프로퍼티의 조건을 정의하고 AndOr을 결합할 수 있다.

메소드 구문 분석의 실제 결과는 쿼리를 만드는 영구 저장소에 따라 다르다. 다만, 주의해야 할 몇 가지 일반적인 사항이 있다.

  • 표현식은 일반적으로 연결 가능한 연산자와 결합된 속성 순회(property traversals)이다. 속성 표현식은 ANDOR로 결합할 수 있다. 속성 표현식은 Between, LessThan, GreaterThan, Like 등의 연산자도 지원된다. 지원되는 연산자는 데이터 스토어에 따라 다를 수 있으므로 참조 문서의 적절한 부분을 참조하여라.

  • 메소드 파서는 개별 속성(예: findByLastnameIgnoreCase(...)) 또는 대소문자 구분을 지원하는 유형의 모든 속성(일반적으로 String 인스턴스 - findByLastnameAndFirstnameAllIgnoreCase(...) 등)에 대한 IgnoreCase 플래그 설정을 지원한다. 케이스의 무시가 지원되는지 여부는 저장소마다 다르므로 저장소별 쿼리 메소드는 참조 문서의 관련 섹션을 참조하여라.

  • 속성을 참조하는 쿼리 메소드에 OrderBy 절을 추가하고, 정렬 방식(Asc 또는 Desc)을 제공하여, 정적 순서를 적용할 수 있다. 동적 정렬을 지원하는 쿼리 메소드를 만들려면 “특수 매개 변수 처리"를 참조하여라.

11.4.3. 속성 표현식

앞의 예와 같이 속성 표현식은 관리되는 엔터티의 직접 속성만 참조할 수 있다. 쿼리를 생성할 시에 구문 분석된 속성이 관리되는 도메인 클래스의 속성임을 이미 확인하였다. 다만, 중첩된 속성을 스캔하여 제약 조건을 정의할 수도 있다. 다음 메소드 시그니처를 보도록 하자.

List<Person> findByAddressZipCode(ZipCode zipCode);

PersonZipCode포함된 Address이 있다고 가정한다. 이 경우 메소드는 x.address.zipCode 속성 순회를 만든다. 해결 알고리즘은 전체 파트(AddressZipCode)를 속성으로 해석하는 것으로 시작하고, 도메인 클래스에서 해당 이름(대문자가 아님)의 속성을 확인한다. 알고리즘이 성공하면 해당 속성이 사용된다. 그렇지 않으면 알고리즘이 Camel 케이스 부분의 소스를 오른쪽에서 머리와 꼬리로 나누고, 해당 속성(이 예에서는 AddressZipCode)이 찾으려고 한다. 알고리즘이 그 머릴를 가지는 프로퍼티를 발견하면, 꼬리를 찾아서 거기로부터 트리를 구축하고, 방금 설명한 메소드로 꼬리를 분할한다. 첫 번째 분할이 일치하지 않으면 알고리즘은 분할 지점을 왼쪽으로 이동하고(Address, ZipCode) 계속하게 된다.

이는 대부분의 경우에 작동해야 하지만 알고리즘이 잘못된 속성을 선택할 수 있다. Person 클래스에도 addressZip 속성이 있다고 가정한다. 알고리즘은 첫 번째 분할 라운드에서 이미 일치하며 잘못된 속성을 선택하여 실패한다(addressZip 유형에는 code 속성이 없기 때문에).

이 모호성을 해결하려면 메소드 이름 내에서 _를 사용하여 순회점을 수동으로 정의한다. 메소드 이름은 다음과 같다.

List<Person> findByAddress_ZipCode(ZipCode zipCode);

밑줄 문자를 예약 문자로 취급하기 때문에 표준 Java 명명 규칙을 따르는 것이 좋다(즉, 속성 이름에 밑줄을 사용하지 않고 대신 낙타 케이스를 사용한다).

11.4.4. 특별한 매개 변수 처리

쿼리에서 매개 변수를 처리하려면, 이전 예제에서 이미 보았듯이 메소드 매개 변수를 정의한다. 또한 인프라는 PageableSort 등의 특정 유형을 인식하여, 페이지네이션(pagination)과 정렬(sorting)를 동적으로 쿼리를 적용한다. 다음 예제는 이러한 기능을 보여 준다.

예 14: 쿼리 메소드 Pageable, Slice, Sort 사용

Page<User> findByLastname(String lastname, Pageable pageable);

Slice<User> findByLastname(String lastname, Pageable pageable);

List<User> findByLastname(String lastname, Sort sort);

List<User> findByLastname(String lastname, Pageable pageable);

첫 번째 메소드에서는 org.springframework.data.domain.Pageable 인스턴스를 쿼리 메소드에 전달하여, 정적으로 정의된 쿼리에 페이징을 동적으로 추가할 수 있다. Page는 사용 가능한 요소와 페이지의 총 수를 알고 있다. 전체 수를 계산하기 위해 카운트 쿼리를 트리거되어 인프라에 의해 수행된다. 이는 (사용하는 저장소에 따라) 비용이 많이 들 수 있으므로, 대신에 Slice을 반환 할 수 있다. Slice는 다음 Slice가 사용 가능한지에 대한 여부만 알고 있으며, 이는 더 큰 결과 집합을 탐색할 때 충분할 수 있다.

정렬 옵션도 Pageable 인스턴스를 통해 처리된다. 정렬만 필요한 경우는 메소드에 org.springframework.data.domain.Sort 매개 변수를 추가한다. 보시다시피, List를 반환하는 것도 가능하다. 이 경우 실제에 Page 인스턴스를 빌드하는데 필요한 추가 메타데이터는 생성되지 않는다(즉, 필요했던 추가 카운트 쿼리는 발행되지 않음). 오히려 지정된 범위의 엔티티만 검색하도록 쿼리를 제한한다.

페이징 및 정렬

속성 이름을 사용하여 간단한 정렬 식을 정의할 수 있다. 표현식을 연결하여 여러 조건을 하나의 표현식으로 수집할 수 있다.

예 15: 정렬 식 정의

Sort sort = Sort.by("firstname").ascending()
  .and(Sort.by("lastname").descending());

정렬 식을 정의하는데 더 안전한 방법은, 정렬 식을 정의하는 형식으로 시작하고 메소드 참조를 사용하여 정렬하는 속성을 정의한다.

예 16: 유형 안전 API를 사용하여 정렬 식 정의

TypedSort<Person> person = Sort.sort(Person.class);

Sort sort = person.by(Person::getFirstname).ascending()
  .and(person.by(Person::getLastname).descending());

저장소 구현이 Querydsl을 지원하는 경우는 생성된 메타모델 유형을 사용하여 정렬 식을 정의할 수도 있다. 예 17: QuerydslAPI를 사용하여 정렬 식 정의

QSort sort = QSort.by(QPerson.firstname.asc())
  .and(QSort.by(QPerson.lastname.desc()));

11.4.5. 쿼리 결과 제한

first 또는 top 키워드를 사용하여 쿼리 메소드의 결과를 제한할 수 있다. 이 키워드는 같은 의미로 사용할 수 있다. 옵션의 숫자를 top 또는 first에 추가하여 반환되는 최대 결과 크기를 지정할 수 있다. 숫자가 생략되면 결과 크기는 1로 간주된다. 다음 예는 쿼리 크기를 제한하는 방법을 보여 준다.

예 18: TopFirst를 사용한 쿼리 결과 크기 제한

User findFirstByOrderByLastnameAsc();

User findTopByOrderByAgeDesc();

Page<User> queryFirst10ByLastname(String lastname, Pageable pageable);

Slice<User> findTop3ByLastname(String lastname, Pageable pageable);

List<User> findFirst10ByLastname(String lastname, Sort sort);

List<User> findTop10ByLastname(String lastname, Pageable pageable);

제한 표현식은 개별 쿼리를 지원하는 데이터 저장소 Distinct 키워드도 지원한다. 또한 결과 집합을 1개의 인스턴스로 제한하는 쿼리의 경우는 결과를 Optional 키워드로 래핑하는 것이 지원된다.

페이지네이션 또는 슬라이스가 제한 쿼리 페이지네이션(및 사용 가능한 페이지 수 계산)에 적용되는 경우는 제한된 결과 내에서 적용된다.

11.4.6. 컬렉션 또는 이터러블을 반환하는 리포지토리 메소드

여러개의 결과를 반환하는 메소드는 자바의 표준의 Iterable, List, Set을 사용할 수 있다. 또한 Spring Data의 Streamable, Iterable의 사용자 정의 확장과 Vavr에서 제공하는 컬렉션 유형을 반환하는 것을 지원한다. 가능한 모든 “쿼리 메소드의 반환 유형"에 대해 설명하는 부록을 참조하여라.

Streamable을 쿼리 메소드의 반환 형식으로 사용

Iterable 또는 모든 컬렉션 유형 대신에 Streamable을 사용할 수 있다. 이는 병렬이 아닌 Stream(Iterable에는 없다)에 액세스 하기 위한 편리한 메소드와, 요소를 통해 ... .filter(...), ... .map(...)를 직접 액세스 하여 Streamable를 다른 요소에 연결하는 기능을 제공한다.

예 19: Streamable을 사용하여 쿼리 메소드 결과 결합

interface PersonRepository extends Repository<Person, Long> {
  Streamable<Person> findByFirstnameContaining(String firstname);
  Streamable<Person> findByLastnameContaining(String lastname);
}

Streamable<Person> result = repository.findByFirstnameContaining("av")
  .and(repository.findByLastnameContaining("ea"));
커스텀 스트리밍 가능한 래퍼 유형 반환

컬렉션 전용의 래퍼 타입을 제공하는 것은 복수의 요소를 반환하는 쿼리 결과의 API를 제공하기 위해서 일반적으로 사용되는 패턴이다. 일반적으로 이러한 유형은 컬렉션과 같은 유형을 반환하는 리포지토리 메소드를 호출하고, 래퍼 유형의 인스턴스를 수동으로 만들면 사용된다. Spring Data 에서는 다음의 조건을 채우는 경우, 이러한 래퍼형을 쿼리 메소드의 반환값의 형태로서 사용할 수 있으므로, 이 추가의 순서를 회피할 수 있다.

  1. 유형은 Streamable을 구현한다.
  2. 이 유형은 Streamable을 인수로 하는 of(...) 또는 valueOf(...)이라는 이름의 생성자 혹은 정적 팩토리 메소드를 공개하고 있다.

다음 목록은 예를 보여 준다.

class Product {                                         // (1)
  MonetaryAmount getPrice() { ... }
}

@RequiredArgsConstructor(staticName = "of")
class Products implements Streamable<Product> {         // (2)

  private final Streamable<Product> streamable;

  public MonetaryAmount getTotal() {                    // (3)
    return streamable.stream()
      .map(Priced::getPrice)
      .reduce(Money.of(0), MonetaryAmount::add);
  }


  @Override
  public Iterator<Product> iterator() {                 // (4)
    return streamable.iterator();
  }
}

interface ProductRepository implements Repository<Product, Long> {
  Products findAllByDescriptionContaining(String text); // (5)
}

(1) 제품 가격에 접근하기 위한 API를 공개하는 Product 엔터티이다.
(2) Products.of(...)(Lombok 어노테이션을 사용하여 만든 팩토리 메소드)를 사용하여 빌드할 수 있는 Streamable<Product>의 래퍼 유형. Streamable<Product>를 사용하는 표준 생성자도 마찬가지로 작동한다.
(3) 래퍼 유형은 추가 API를 공개하고, Streamable<Product>으로 새로운 값을 계산한다.
(4) Streamable 인터페이스를 구현하고, 실제 결과에 위임한다.
(5) 그 래퍼형 Products은, 쿼리 메소드의 반환값의 형태로서 직접 사용할 수 있다. Streamable<Product>를 반환하고, 리포지토리 클라이언트로 쿼리 후에 수동으로 래핑할 필요가 없다.

Vavr 컬렉션 지원

Vavr는 Java의 함수형 프로그래밍의 개념을 도입한 라이브러리이다. 다음 표와 같이 쿼리 메소드의 반환 형식으로 사용할 수 있는 컬렉션 유형의 사용자 지정 집합이 제공된다.

Vavr 컬렉션 유형 사용되는 Vavr 구현 유형 유효한 Java 소스 유형
io.vavr.collection.Seq io.vavr.collection.List java.util.Iterable
io.vavr.collection.Set io.vavr.collection.LinkedHashSet java.util.Iterable
io.vavr.collection.Map io.vavr.collection.LinkedHashMap java.util.Map

실제 쿼리 결과의 Java 유형(3번째 열)에 따라, 첫 번째 열 유형(또는 하위 유형)을 쿼리 메소드의 반환 유형으로 사용하고, 구현 유형으로 사용되는 두 번째 열의 유형을 얻을 수 있다. 또는 Traversable(Vavr Iterable과 동등)을 선언하여 실제 반환 값에서 구현 클래스를 파생시킬 수 있다. 즉, java.util.List는 Vavr List 또는 Seq로 변환되고, java.util.Set는 Vavr LinkedHashSetSet로 변환된다.

11.4.7. 리포지토리 메소드의 null 처리

Spring Data 2.0 이후, 개별 집계 인스턴스를 반환하는 리포지터리 CRUD 메소드는 Java 8의 Optional를 사용하여, 값이 존재하지 않을 가능성이 있는 것을 나타낸다. 또한 Spring Data는 쿼리 메소드에서 다음 래퍼 유형을 반환하는 것을 지원한다.

  • com.google.common.base.Optional
  • scala.Option
  • io.vavr.control.Option

또는 쿼리 메소드는 래퍼 유형을 전혀 사용하지 않도록 선택할 수 있다. null를 반환하면 쿼리 결과가 없음을 나타낸다. 컬렉션, 컬렉션의 대체, 래퍼, 스트림을 돌려주는 리포지터리(repository) 메소드는 null이 아닌 비어 있는 상태를 반환하지 하도록 보장하고 있다. 자세한 내용은 “리포지토리 쿼리 반환 유형“을 참조하여라.

Nullability(null 허용) 어노테이션

Spring Framework의 nullability 어노테이션을 사용하여 리포지터리 메소드의 nullability 제약을 표현할 수 있다. 이들은 다음과 같이 런타임에 도구 친화적 인 접근법과 옵트 인 null 검사를 제공합니다.

  • @NonNullApi : 패키지 레벨에서 사용되고, 매개 변수와 반환 값의 기본 동작이 각각 null 값을 수락하거나 생성하지 않음을 선언한다.
  • [@NonNull](https://docs.spring.io/spring-framework/docs/5.3.13/javadoc-api/org/springframework/lang/NonNull.html) : null 이어서는 안되는 매개 변수 또는 반환 값에 사용된다(@NonNullApi`이 적용되는 매개 변수 및 반환 값에는 필요하지 않음).
  • @Nullable : null일 수 있는 매개 변수 또는 반환 값에 사용된다.

Spring 어노테이션은 JSR 305 어노테이션(잠재적이지만 널리 사용되는 JSR)로 메타 어노테이션이 선언되어 있다. JSR 305 메타 어노테이션으로 툴 벤더(IDEA, Eclipse, Kotlin 등)은 Spring 어노테이션 지원을 하드 코딩하지 않고 일반적인 방법으로 null-safety 지원하도록 제공되고 있다. 쿼리 메소드의 null 가능성 제약의 런타임 체크를 유효하게 하려면, 다음의 예제와 같이 package-info.java에서 Spring @NonNullApi를 사용하여 패키지 레벨에서 null이 아닌 가능성을 활성화해야 한다.

예 20: package-info.java에서 null이 아닌 가능성 선언

@org.springframework.lang.NonNullApi
package com.acme;

null 이외의 디폴트가 설정되면, 리포지터리 쿼리 메소드의 호출은 null 허용 여부 제약 조건에 대해 런타임에 유효성이 검증된다. 쿼리 결과가 정의된 제약 조건을 위반하는 경우에는 예외가 발생한다. 이는 메소드가 null를 반환되는데, null 허용이 아닌 것으로 선언되고 있는 경우에 발생한다(리포지터리가 존재하는 패키지로 정의된 어노테이션의 디폴트). null 허용의 결과를 다시 설정하려면, 개별로 메소드에 @Nullable를 선택적으로 사용한다. 이 섹션의 시작 부분에서 언급한 결과 래퍼 유형을 사용하면 예상대로 작동한다. 빈 결과는 부재를 나타내는 값으로 변환된다.

다음 예제는 앞에서 설명한 몇 가지 기술을 보여 준다.

예 21: 다양한 null 값 제약 조건 사용

package com.acme;                                                       // (1)

import org.springframework.lang.Nullable;

interface UserRepository extends Repository<User, Long> {

  User getByEmailAddress(EmailAddress emailAddress);                    // (2)

  @Nullable
  User findByEmailAddress(@Nullable EmailAddress emailAdress);          // (3)

  Optional<User> findOptionalByEmailAddress(EmailAddress emailAddress); // (4)
}

(1) 리포지토리는 null이 아닌 동작을 정의한 패키지(또는 하위 패키지)에 있다.
(2) 쿼리에서 결과가 생성되지 않으면 EmptyResultDataAccessException가 throw 된다. 메소드 emailAddress에 전달된 null인 경우에는 IllegalArgumentException이 throw 된다.
(3) 쿼리가 결과를 생성하지 않는 경우 null를 반환한다. emailAddress의 값으로 null을 허용한다.
(4) 쿼리가 결과를 생성하지 않는 경우 Optional.empty()를 반환한다. 메소드 emailAddress에 전달된 값이 null인 경우 IllegalArgumentException이 throw 된다.

Kotlin 기반 저장소의 Nullability

Kotlin은 언어에 내장된 null 허용 여부 제약 조건의 정의가 있다. Kotlin 코드는 바이트 코드로 컴파일된다. 이는 메소드 시그니쳐는 아니고, 컴파일 된 메타데이타를 통해서 nullability 제약을 표현하지 않는다. Kotlin의 null 허용 여부(nullability) 제약 조건을 검사할 수 있도록 프로젝트에 kotlin-reflect JAR을 포함해야 한다. Spring Data 리포지토리는 언어 메카니즘을 사용해 이러한 제약을 정의하여 다음과 같이 같은 런타임 체크를 적용합니다.

예 22 : Kotlin 저장소에서 null 가능성 제약 조건 사용

interface UserRepository : Repository<User, String> {

  fun findByUsername(username: String): User     // (1)

  fun findByFirstname(firstname: String?): User? // (2)
}

(1) 이 메소드는 매개 변수와 결과의 양쪽 모두를 nullable이 아닌 것(Kotlin 의 디폴트)으로 정의한다. Kotlin 컴파일러는 null 메소드를 전달하는 메소드 호출을 거부한다. 쿼리에서 빈 결과를 얻으면 EmptyResultDataAccessException가 throw 된다.
(2) 이 메소드는 firstname 매개 변수에 대해 null를 허용하고, 쿼리에서 결과가 생성되지 않는 경우 null를 반환한다.

11.4.8. 스트리밍 쿼리 결과

Java 8의 Stream<T>를 반환값의 유형으로 사용하여 쿼리 메소드의 결과를 단계적으로 처리할 수 있다. 다음 예제와 같이 쿼리 결과를 Stream 래핑하는 대신 데이터 저장소별 메소드를 사용하여 스트리밍을 수행한다.

예 23: Java 8 Stream<T>을 사용한 쿼리 결과 스트리밍

@Query("select u from User u")
Stream<User> findAllByCustomQueryAndStream();

Stream<User> readAllByFirstnameNotNull();

@Query("select u from User u")
Stream<User> streamAllPaged(Pageable pageable);

예 24: Stream<T> 조작하면 try-with-resources 블록이 생성된다.

try (Stream<User> stream = repository.findAllByCustomQueryAndStream()) {
  stream.forEach(...);
}

11.4.9. 비동기 쿼리 결과

Spring의 비동기 메소드 실행 기능을 사용하면 리포지토리 쿼리를 비동기적으로 실행할 수 있다. 이는 Sprin TaskExecutor에 전송된 태스크에서 실제 쿼리가 발생하는 동안 메소드가 호출 직후에 돌아온다는 것을 의미한다. 비동기 쿼리는 반응형 쿼리와 다르므로 혼합되어서는 안된다. 리액티브 지원에 대한 자세한 내용은 저장소별 문서를 참조하여라. 다음 예제에서는 몇 가지 비동기 쿼리를 보여 준다.

@Async
Future<User> findByFirstname(String firstname);               // (1)

@Async
CompletableFuture<User> findOneByFirstname(String firstname); // (2)

@Async
ListenableFuture<User> findOneByLastname(String lastname);    // (3)

(1) 반환 유형으로 java.util.concurrent.Future를 사용한다.
(2) 반환 유형으로 Java 8 java.util.concurrent.CompletableFuture을 사용한다.
(3) 반환 유형으로 org.springframework.util.concurrent.ListenableFuture을 사용한다.

11.5. 리포지토리 인스턴스 만들기

이 섹션에서는 정의된 리포지토리 인터페이스의 인스턴스와 Bean 정의를 작성하는 방법에 대해 설명한다. 이를 수행하는 한가지 방법은 리포지토리 메커니즘을 지원하는 각 Spring Data 모듈과 함께 제공되는 Spring 네임 스페이스를 사용하는 것이다. 일반적으로 Java 구성(JavaConfig)을 사용하는 것이 좋다.

11.5.1. XML 구성

각 Spring Data 모듈에는 다음 예제와 같이 Spring이 스캔하는 기본 패키지를 정의할 수 있는 repositories 요소가 포함되어 있다.

예 25: XML을 통한 Spring Data 리포지토리 사용

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns:beans="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns="http://www.springframework.org/schema/data/jpa"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
    https://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/data/jpa
    https://www.springframework.org/schema/data/jpa/spring-jpa.xsd">

  <repositories base-package="com.acme.repositories" />

</beans:beans>

위의 예에서 Spring은 com.acme.repositories 모든 하위 패키지를 스캔하고 Repository 상속하는 인터페이스 또는 하위 인터페이스 중 하나를 찾도록 지시 받는다. 발견된 각 인터페이스에 대해 인프라는 영속화 기술 고유의 FactoryBean을 등록하여, 쿼리 메소드 호출을 처리하는 적절한 프록시를 만든다. 각 Bean은 인터페이스 이름에서 파생된 Bean 이름으로 등록되므로, UserRepository 인터페이스는 userRepository로 등록된다. 중첩된 리포지토리 인터페이스의 Bean 이름에는 둘러싸는 형식 이름이 접두사로 추가된다. base-package 속성은 와일드카드를 사용할 수 있으므로 스캔한 패키지의 패턴을 정의할 수 있다.

필터 사용

기본적으로 인프라스트럭쳐는 설정된 기본 패키지에 있는 영속화 기술 고유의 Repository 서브 인터페이스를 상속하는 모든 인터페이스를 받아와 이에 대한 Bean 인스턴스를 생성한다. 다만, Bean 인스턴스가 작성되고 있는 인터페이스를 보다 세밀하게 제어해야 하는 경우가 있는데, 그렇게 하려면 <repositories /> 요소 내에서 <include-filter /> 요소와 <exclude-filter /> 요소를 사용한다. 시멘틱스는 Spring의 컨텍스트 네임스페이스의 요소와 완전히 동일하다. 자세한 내용은 이러한 요소의 Spring 참조 문서를 참조하여라.

예를 들어, 특정 인터페이스를 리포지토리 bean으로 인스턴스화에서 제외하려면 다음과 같은 구성을 사용할 수 있다.

예 26: exclude-filter 요소 사용

<repositories base-package="com.acme.repositories">
  <context:exclude-filter type="regex" expression=".*SomeRepository" />
</repositories>

위의 예는 SomeRepository 끝나는 모든 인터페이스를 인스턴스화에서 제외한다.

11.5.2. Java 구성

Java 구성 클래스에서 저장소별 @Enable${store}Repositories 어노테이션을 사용하여 리포지토리 인프라를 트리거 할 수도 있다. Spring 컨테이너의 Java 베이스의 구성의 개요에 대해서는 Spring 참조 문서의 JavaConfig를 참조하여라.

Spring Data 리포지토리를 활성화하는 샘플 구성은 다음과 같다.

예 27: 어노테이션 기반 저장소 구성 샘플

@Configuration
@EnableJpaRepositories("com.acme.repositories")
class ApplicationConfiguration {

  @Bean
  EntityManagerFactory entityManagerFactory() {
    // ...
  }
}

11.5.3. 독립 실행형 사용

Spring 컨테이너 외부(예: CDI 환경)에서 리포지토리 인프라를 사용할 수도 있다. 클래스 패스에는 아직 몇개의 Spring 라이브러리가 필요하지만, 일반적으로 프로그램으로 리포지터리를 설정할 수도 있다. 리포지토리 지원을 제공하는 Spring Data 모듈에는 다음과 같이 사용할 수 있는 영속성 기술 고유의 기능인 RepositoryFactory가 포함되어 있다.

예 28 : 저장소 팩토리의 독립 실행형 사용

RepositoryFactorySupport factory = ... // Instantiate factory here
UserRepository repository = factory.getRepository(UserRepository.class);

11.6. Spring Data 저장소의 커스텀 구현

Spring Data는 코딩을 거의 실시하지 않고 쿼리 메소드를 작성하기 위한 다양한 옵션을 제공한다. 다만, 이러한 옵션이 요구에 맞지 않는 경우는 리포지터리 메소드의 독자적인 커스텀 구현을 제공할 수도 있다. 이 섹션에서는 그 방법을 설명한다.

11.6.1. 개별 리포지토리 사용자 정의

사용자 지정 기능으로 리포지토리를 강화하려면 먼저 다음과 같이 프래그먼트(segment:부분) 인터페이스와 사용자 지정 기능의 구현을 정의해야 한다.

예 29: 사용자 지정 리포지토리 기능 인터페이스

interface CustomizedUserRepository {
  void someCustomMethod(User user);
}

예 30: 사용자 지정 리포지토리 기능 구현

class CustomizedUserRepositoryImpl implements CustomizedUserRepository {

  public void someCustomMethod(User user) {
    // Your custom implementation
  }
}

구현 자체는 Spring Data에 의존하지 않고, 일반 Spring Bean이 될 수 있다. 표준 종속성 주입 동작을 사용하여 다른 Bean(JdbcTemplate 등)에 대한 참조를 주입하고, 종횡비(aspect)에 참여하는 등의 작업을 수행할 수 있다.

그런 다음 다음과 같이 리포지토리 인터페이스가 프래그먼트 인터페이스를 상속받을 수 있다.

예 31: 리포지토리 인터페이스 변경

interface UserRepository extends CrudRepository<User, Long>, CustomizedUserRepository {

  // Declare query methods here
}

리포지토리 인터페이스에서 프래그먼트 인터페이스를 확장하면 CRUD와 사용자 정의 기능이 결합되어 클라이언트에서 사용할 수 있다.

Spring Data 리포지토리는 리포지토리 구성을 형성하는 프래그먼트을 사용하여 구현된다. 프래그먼트는 기본 리포지토리, 기능적 측면(예: QueryDsl) 및 사용자 정의 인터페이스와 구현이다. 리포지토리 인터페이스에 인터페이스를 추가할 때마다, 프래그먼트을 추가하여 구성을 강화한다. 베이스 리포지터리와 리포지터리 aspect의 구현은, 각 Spring Data 모듈에 의해 제공된다.

다음 예제는 사용자 정의 인터페이스와 구현을 보여 준다.

예 32: 구현 조각

interface HumanRepository {
  void someHumanMethod(User user);
}

class HumanRepositoryImpl implements HumanRepository {

  public void someHumanMethod(User user) {
    // Your custom implementation
  }
}

interface ContactRepository {

  void someContactMethod(User user);

  User anotherContactMethod(User user);
}

class ContactRepositoryImpl implements ContactRepository {

  public void someContactMethod(User user) {
    // Your custom implementation
  }

  public User anotherContactMethod(User user) {
    // Your custom implementation
  }
}

다음 예에서는 CrudRepository을 상속하는 사용자 지정 저장소의 인터페이스를 보여 준다.

예 33: 리포지토리 인터페이스 변경

interface UserRepository extends CrudRepository<User, Long>, HumanRepository, ContactRepository {

  // Declare query methods here
}

리포지토리는 선언의 순서대로 임포트 되는 여러 사용자 정의 구현으로 구성될 수 있다. 사용자 지정 구현은 기본 구현 및 리포지토리 측면보다 우선 순위가 높다. 이 순서을 사용하면 베이스 리포지터리 및 aspect 메소드를 재정의(override) 하여 2개의 세그먼트이 동일한 메소드 시그니쳐를 제공하는 경우의 모호함을 해결할 수 있다. 리포지토리 세그먼트는 단일 리포지토리 인터페이스에서 사용하도록 제한되지 않는다. 여러 리포지토리에서 세그먼트 인터페이스를 사용할 수 있으므로 다른 리포지토리에서 사용자 지정을 재사용할 수 있다.

다음 예제는 저장소 조각과 그 구현을 보여 준다.

예 34: save(…)을 재정의하는 조각

interface CustomizedSave<T> {
  <S extends T> S save(S entity);
}

class CustomizedSaveImpl<T> implements CustomizedSave<T> {

  public <S extends T> S save(S entity) {
    // Your custom implementation
  }
}

다음 예에서는 앞에서 설명한 리포지토리 조각을 사용하는 리포지토리를 보여 준다.

예 35: 사용자 정의된 리포지토리 인터페이스

interface UserRepository extends CrudRepository<User, Long>, CustomizedSave<User> {
}

interface PersonRepository extends CrudRepository<Person, Long>, CustomizedSave<Person> {
}
구성(Configuration)

네임스페이스 구성을 사용하는 경우 리포지토리 인프라는 리포지토리를 찾은 패키지의 클래스를 검색하여 사용자 지정 구현 세그먼트를 자동으로 검색하려고 시도한다. 이러한 클래스는 네임스페이스 요소의 repository-impl-postfix 속성을 조각 인터페이스 이름에 추가하는 명명 규칙을 따라야 한다. 이 접미어의 기본값은 Impl이다. 다음 예는 기본 접미어를 사용하는 리포지토리와 접미어의 사용자 지정 값을 설정하는 리포지토리를 보여 준다.

예 36: 구성 예

<repositories base-package="com.acme.repository" />

<repositories base-package="com.acme.repository" repository-impl-postfix="MyPostfix" />

이전 예제의 첫 번째 구성은 사용자 지정 리포지토리 구현 역할을 하는 com.acme.repository.CustomizedUserRepositoryImpl 클래스를 찾는다. 두 번째 예에서는 com.acme.repository.CustomizedUserRepositoryMyPostfix검색을 시도한다.

모호성 해결

일치하는 클래스명을 가지는 복수의 구현이 다른 패키지로 발견되었을 경우에는 Spring Data는 사용할 빈 이름을 식별하기 위해 Bean 이름을 사용한다.

앞에서 설명한 CustomizedUserRepository 2개의 사용자 정의 구현이 주어지면 최초의 구현이 사용된다. 구현된 Bean 이름은 customizedUserRepositoryImpl이며, 이는 프래그먼트 인터페이스( CustomizedUserRepository)의 이름과 접미사 Impl와 일치한다.

예 37: 모호한 구현의 해결

package com.acme.impl.one;

class CustomizedUserRepositoryImpl implements CustomizedUserRepository {

  // Your custom implementation
}
package com.acme.impl.two;

@Component("specialCustomImpl")
class CustomizedUserRepositoryImpl implements CustomizedUserRepository {

  // Your custom implementation
}

UserRepository 인터페이스에 @Component("specialCustom") 어노테이션을 추가하면, Bean 이름에 Impl 붙은 com.acme.impl.two의 리포지터리 구현용으로 정의된 것과 일치하여 첫 번째을 대신하여 사용된다.

수동 연결

커스텀 구현에서 어노테이션 기반의 구성과 Autowired만을 사용하는 경우에는 이전 접근 방식은 다른 Spring Bean와 같이 처리되기 때문에 잘 동작한다. 구현 프래그먼트 bean에 특별한 접속이 필요한 경우, bean을 선언해, 이전 섹션(모호성 해결)에서 설명한 규칙에 따라 이름을 붙일 수가 있다. 인프라는 Bean 정의를 수동으로 작성하는 대신에 이름으로 수동 정의한 것을 참조한다. 다음 예제에서는 사용자 정의 구현을 수동으로 연결하는 방법을 보여 준다.

예 38: 사용자 정의 구현 수동 연결

<repositories base-package="com.acme.repository" />

<beans:bean id="userRepositoryImpl" class="...">
  <!-- further configuration -->
</beans:bean>

11.6.2. 기본 리포지토리 사용자 정의

이전 섹션(모호성 해결)에서 설명한 접근 방식에서는 모든 리포지토리가 영향을 받는 기본 리포지토리의 동작을 사용자 지정하는 경우 각 리포지토리 인터페이스를 사용자 지정해야 한다. 대신에, 모든 리포지터리의 동작을 변경하기 위해서, 영속화 기술 고유의 리포지터리(repository) 베이스 클래스를 상속하는 구현을 작성할 수 있다. 이 클래스는 다음 예제와 같이 리포지토리 프록시의 사용자 지정 기본 클래스 역할을 한다.

예 39: 사용자 지정 리포지토리 기반 클래스

class MyRepositoryImpl<T, ID>
  extends SimpleJpaRepository<T, ID> {

  private final EntityManager entityManager;

  MyRepositoryImpl(JpaEntityInformation entityInformation,
                          EntityManager entityManager) {
    super(entityInformation, entityManager);

    // Keep the EntityManager around to used from the newly introduced methods.
    this.entityManager = entityManager;
  }

  @Transactional
  public <S extends T> S save(S entity) {
    // implementation goes here
  }
}

마지막 단계는 Spring Data 인프라가 사용자 정의된 리포지토리 기본 클래스를 인식하게 하는 것이다. Java 구성에서는, 다음의 예와 같이 @Enable${store}Repositories 어노테이션의 repositoryBaseClass 속성을 사용해 이것을 실행할 수 있다.

예 40 : JavaConfig를 사용하여 사용자 정의 리포지토리 베이스 클래스 구성

@Configuration
@EnableJpaRepositories(repositoryBaseClass = MyRepositoryImpl.class)
class ApplicationConfiguration { ... }

다음 예제와 같이 XML 네임스페이스에서 해당 속성을 사용할 수 있다.

예 41: XML을 사용하여 사용자 지정 리포지토리 기반 클래스 구성

<repositories base-package="com.acme.repository"
     base-class="....MyRepositoryImpl" />

11.7. 집계 루트에서 이벤트 게시

리포지토리에서 관리하는 엔티티는 집계 루트이다. 도메인 주도 설계 애플리케이션에서 이러한 집계 루트는 일반적으로 도메인 이벤트를 게시한다. Spring Data는 @DomainEvents라는 어노테이션을 제공한다. 이는 다음의 예와 같이 해당 발행을 가능한 한 쉽게 만들기 위해 집계 루트의 메소드에 사용할 수 있도록 해준다.

예 42: 집계 루트에서 도메인 이벤트 게시

class AnAggregateRoot {

    @DomainEvents // (1)
    Collection<Object> domainEvents() {
        // ... return events you want to get published here
    }

    @AfterDomainEventPublication  // (2)
    void callbackMethod() {
       // ... potentially clean up domain events list
    }
}

(1) @DomainEvents를 사용하는 메소드는 단일 이벤트 인스턴스 또는 이벤트 컬렉션 중 하나를 반환할 수 있다. 인수가 있으면 안된다.
(2) 모든 이벤트가 게시된 후에는 @AfterDomainEventPublication 어노테이션이 선언된 메소드가 있다. 이를 사용하여 게시할 이벤트 목록을 (다른 용도에서도) 잠재적으로 정리(clean)할 수 있다.

메소드는 Spring Data 리포지토리의 save(...), saveAll(...), delete(...) 그리고 deleteAll(...) 메소드 중에 하나가 호출 될 때마다 호출된다.

11.8. Spring Data 확장

이 섹션에서는 다양한 컨텍스트에서 Spring Data를 사용할 수 있도록하는 일련의 Spring Data 확장에 대해 설명한다. 현재 대부분의 통합은 Spring MVC 를 대상으로 한다.

11.8.1. Querydsl 확장

Querydsl (영어) 는 흐르는 API 를 사용해, 정적으로 형태 지정된 SQL 와 같은 쿼리의 구축을 가능하게 하는 프레임워크이다.

다음 예제와 같이 일부 Spring Data 모듈은 QuerydslPredicateExecutorQuerydsl과의 통합을 통해 제공된다.

예 43: QuerydslPredicateExecutor 인터페이스

public interface QuerydslPredicateExecutor<T> {

  Optional<T> findById(Predicate predicate);  // (1)

  Iterable<T> findAll(Predicate predicate);   // (2)

  long count(Predicate predicate);            // (3)

  boolean exists(Predicate predicate);        // (4)

  // ... more functionality omitted.
}

(1) Predicate와 일치하는 단일 엔티티를 검색하고 반환한다.
(2) Predicate와 일치하는 모든 엔티티를 검색하고 반환한다.
(3) Predicate에 일치하는 엔티티의 수를 돌려준다.
(4) Predicate에 일치하는 엔티티가 존재할지 어떨지를 돌려준다.

Querydsl 지원을 사용하려면 다음 예와 같이 리포지토리 인터페이스에서 QuerydslPredicateExecutor을 상속한다.

예 44: 리포지토리에서 Querydsl 통합

interface UserRepository extends CrudRepository<User, Long>, QuerydslPredicateExecutor<User> {
}

이전 예제에서는 다음 예제와 같이 Querydsl Predicate 인스턴스를 사용하여 유형 안전 쿼리를 작성할 수 있다.

Predicate predicate = user.firstname.equalsIgnoreCase("dave")
  .and(user.lastname.startsWithIgnoreCase("mathews"));

userRepository.findAll(predicate);

11.8.2. 웹 지원

리포지토리 프로그래밍 모델을 지원하는 Spring Data 모듈에는 다양한 웹 지원이 포함되어 있다. Web 관련 컴퍼넌트에서는 Spring MVC JAR가 클래스 경로에 있어야 한다. 여기에 일부는 심지어 Spring HATEOAS와 통합도 제공한다. 일반적으로 통합 지원 다음의 예와 같이 JavaConfig 구성 클래스로 @EnableSpringDataWebSupport 어노테이션을 사용하는 것으로 유효하게 된다.

예 45: Spring Data 웹 지원 사용

@Configuration
@EnableWebMvc
@EnableSpringDataWebSupport
class WebConfiguration {}

@EnableSpringDataWebSupport 어노테이션은 여러 구성 요소를 등록한다. 이에 대해서는 이 섹션의 뒷부분에서 설명한다. 또한 클래스 패스로 Spring HATEOAS를 감지하고 이에 대한 통합 구성 요소(존재하는 경우)도 등록한다.

또는, XML 구성을 사용하는 경우에 다음의 예와 같이 SpringDataWebConfiguration 또는 HateoasAwareSpringDataWebConfiguration을 Spring Bean으로 등록한다(SpringDataWebConfiguration의 경우).

예 46: XML에서 Spring Data 웹 지원 사용

<bean class="org.springframework.data.web.config.SpringDataWebConfiguration" />

<!-- If you use Spring HATEOAS, register this one *instead* of the former -->
<bean class="org.springframework.data.web.config.HateoasAwareSpringDataWebConfiguration" />
기본 웹 지원

이전 섹션(11.8.2. 웹 지원)에 표시된 구성은 몇 가지 기본 구성 요소를 등록한다.

  • Spring MVC가 요청 매개 변수 또는 경로 변수에서 저장소 관리 도메인 클래스의 인스턴스를 확인할 수 있도록 하는 DomainClassConverter 클래스 사용.
  • Spring MVC가 요구 파라미터로부터 PageableSort 인스턴스를 해결할 수 있게 하는 HandlerMethodArgumentResolver 구현.
  • Jackson 모듈은 사용 중인 Spring Data 모듈에 따라 PointDistance 같은 유형을 반전/직렬화 하거나 특정 유형을 저장한다.
DomainClassConverter 클래스 사용

DomainClassConverter 클래스를 사용하면 Spring MVC 컨트롤러 메소드 시그니처에서 도메인 유형을 직접 사용할 수 있으므로 다음 예제와 같이 리포지토리에서 인스턴스를 수동으로 검색할 필요가 없다.

예 47: 메소드 시그니처에서 도메인 유형을 사용하는 Spring MVC 컨트롤러

@Controller
@RequestMapping("/users")
class UserController {

  @RequestMapping("/{id}")
  String showUserForm(@PathVariable("id") User user, Model model) {

    model.addAttribute("user", user);
    return "userForm";
  }
}

이 메소드는 User 인스턴스를 직접 받아 그 이상의 조회할 필요가 없다. 인스턴스는 Spring MVC가 먼저 경로 변수를 도메인 클래스의 id 유형으로 변환하고, 최종적으로 도메인 유형에 등록된 리포지토리 인스턴스에서 findById(...)을 호출하여 인스턴스에 액세스하여 해결할 수 있다.

Pageable 및 Sort을 위한 HandlerMethodArgumentResolvers

바로 이전 섹션에 표시된 구성 스니펫는 PageableHandlerMethodArgumentResolverSortHandlerMethodArgumentResolver의 인스턴스를 등록한다. 다음 예와 같이 등록은 PageableSort를 유효한 컨트롤러 메소드 인수로 사용한다.

예 48 : 컨트롤러 메소드의 인수로 Pageable을 사용

@Controller
@RequestMapping("/users")
class UserController {

  private final UserRepository repository;

  UserController(UserRepository repository) {
    this.repository = repository;
  }

  @RequestMapping
  String showUsers(Model model, Pageable pageable) {

    model.addAttribute("users", repository.findAll(pageable));
    return "users";
  }
}

위의 메소드 시그니처를 사용하여 Spring MVC는 다음 기본 구성을 사용하여 요청 매개 변수에서 Pageable 인스턴스를 파생시킨다.

표 1: Pageable 인스턴스에 대해 평가된 요청 매개 변수

page 가져올 페이지. 0부터 인덱싱되며 기본값은 0이다.
size 검색할 페이지의 크기. 기본값은 20이다.
sort property,property(,ASC|DESC)(,IgnoreCase)형식으로 정렬해야 하는 속성이다. 기본 정렬 방향은 대소문자를 구분한다. 정렬 방향이나 대문자와 소문자를 구분하려면 여러 sort 매개 변수를 사용한다.
(예: ?sort=firstname&sort=lastname,asc&sort=city,ignorecase).

이 동작을 사용자 정의하려면 PageableHandlerMethodArgumentResolverCustomizer 인터페이스 또는 SortHandlerMethodArgumentResolverCustomizer 인터페이스를 각각 구현하는 Bean을 등록한다. 다음 예제와 같이 customize() 메소드가 호출되어 설정을 변경할 수 있다.

@Bean SortHandlerMethodArgumentResolverCustomizer sortCustomizer() {
    return s -> s.setPropertyDelimiter("<-->");
}

기존의MethodArgumentResolver 속성을 설정하는 것만으로 목적에 맞지 않는 경우는 SpringDataWebConfiguration 또는 HATEOAS 대응의 확장 기능을 상속하여 pageableResolver() 또는 sortResolver() 메소드를 재정의(override)한다. @Enable 어노테이션을 사용하는 대신에 사용자 정의한 구성 파일을 임포트 한다.

요청에서 여러 Pageable 또는 Sort 인스턴스를 확인해야 하는 경우(예: 여러 테이블의 경우)는 Spring @Qualifier 어노테이션을 사용하여 서로 구별할 수 있다. 그런 다음에 요청 매개변수 앞에 ${qualifier}_를 추가해야 한다. 다음 예제는 결과의 메소드 시그니처를 보여 준다.

String showUsers(Model model,
      @Qualifier("thing1") Pageable first,
      @Qualifier("thing2") Pageable second) { ... }

thing1_page, thing2_page등을 설정해야 한다.

디폴트로 메소드에 전달되고 PageablePageRequest.of(0, 20)와 동일하지만, Pageable 파라미터의 @PageableDefault 어노테이션을 사용하여 정의 될 수 있다.

Pageable의 하이퍼 미디어 지원

Spring HATEOAS에는 표현 모델 클래스(PagedResources)가 포함되어 있다. 이를 통해 Page 인스턴스의 콘텐츠를 원하는 Page 메타데이터와 클라이언트가 페이지를 쉽게 탐색할 수 있는 링크를 통해 향상시킬 수 있다. Page에서 PagedResources 변환은 PagedResourcesAssembler라고 불리는 Spring HATEOAS ResourceAssembler 인터페이스를 구현하여 수행된다. 다음 예제는 PagedResourcesAssembler 컨트롤러 메소드의 인수로 사용하는 방법을 보여 준다.

예 49: 컨트롤러 메소드의 인수로 PagedResourcesAssembler 사용

@Controller
class PersonController {

  @Autowired PersonRepository repository;

  @RequestMapping(value = "/persons", method = RequestMethod.GET)
  HttpEntity<PagedResources<Person>> persons(Pageable pageable,
    PagedResourcesAssembler assembler) {

    Page<Person> persons = repository.findAll(pageable);
    return new ResponseEntity<>(assembler.toResources(persons), HttpStatus.OK);
  }
}

이전 예와 같이 구성을 사용하면 PagedResourcesAssembler을 컨트롤러 메소드의 인수로 사용할 수 있다. 그 위에 toResources(...) 호출하면 다음과 같은 효과가 있다.

  • Page 콘텐츠는 PagedResources 인스턴스의 콘텐츠가 된다.
  • PagedResources 객체는 연결된 PageMetadata 인스턴스를 가져오고, Page 및 기본이 되는 PageRequest 정보를 채워진다.
  • PagedResources에는 페이지의 상태에 따라, prevnext 링크가 첨부되어 있는 경우가 있다. 링크는 메소드가 맵핑하는 URI를 가르킨다. 메소드에 추가된 페이지네이션 매개 변수는 PageableHandlerMethodArgumentResolver 설정과 일치하여 링크를 나중에 확인할 수 있다.

데이터베이스에 30개의 Person 인스턴스가 있다고 가정한다. 이것으로 요청(GET http://localhost:8080/persons)을 트리거하여, 다음과 유사한 출력을 확인할 수 있다.

{ "links" : [ { "rel" : "next",
                "href" : "http://localhost:8080/persons?page=1&size=20" }
  ],
  "content" : [
     ... // 20 Person instances rendered here
  ],
  "pageMetadata" : {
    "size" : 20,
    "totalElements" : 30,
    "totalPages" : 2,
    "number" : 0
  }
}

어셈블러는 올바른 URI를 생성하고, 기본 구성을 선택하여, 매개 변수를 다음 요청인 Pageable으로 해결한다. 즉, 그 구성을 변경하면 링크가 자동으로 변경을 준수한다. 디폴트로는 어셈블러는 그것이 불려 가는 컨트롤러 메소드를 가르키지만, 페이지네이션을 링크를 구축하기 위한 베이스로서 사용되는 커스텀 Link을 건네주는 것으로, 그것을 커스터마이즈 할 수 있다. 이렇게 되면 PagedResourcesAssembler.toResource(...) 메소드가 오버로드된다.

Spring Data Jackson 모듈

코어 모듈 및 일부 저장소 특정 모듈에는 Spring Data 도메인에서 사용되는 org.springframework.data.geo.Distanceorg.springframework.data.geo.Point와 같은 유형의 Jackson 모듈 세트가 포함하고 있다. 이 모듈은 웹 지원이 활성화되고, com.fasterxml.jackson.databind.ObjectMapper가 사용 가능할 때 가져온다.

초기화하는 동안에 SpringDataJacksonConfiguration와 같이 SpringDataJacksonModules가 인프라에 의해 검색되기 때문에, 선언된 com.fasterxml.jackson.databind.Module이 Jackson ObjectMapper로 사용할 수 있게 된다.

다음 도메인 유형의 데이터 바인딩 믹스인은 공통 인프라에 의해 등록된다.

org.springframework.data.geo.Distance
org.springframework.data.geo.Point
org.springframework.data.geo.Box
org.springframework.data.geo.Circle
org.springframework.data.geo.Polygon
웹 데이터 바인딩 지원

다음 예제와 같이 Spring Data 프로젝션을 사용하여 JSONPath식(Jayway JsonPath 또는 XPath식(XmlBeam 필요) 중 하나를 사용하여 수신 요청 페이로드를 바인딩할 수 있다.

예 50: JSONPath 또는 XPath 식을 사용한 HTTP 페이로드 바인딩

@ProjectedPayload
public interface UserPayload {

  @XBRead("//firstname")
  @JsonPath("$..firstname")
  String getFirstname();

  @XBRead("/lastname")
  @JsonPath({ "$.lastname", "$.user.lastname" })
  String getLastname();
}

이전에 예에 표시된 형식은 Spring MVC 바인드 메서드의 인수로 사용하거나, ParameterizedTypeReference의 메소드 중에 하나를 사용할 수 있는 RestTemplate 있다. 이전 메소드 선언은 지정된 도큐먼트 내의 임의의 곳에서 firstname를 찾아내려고 한다. lastname XML 조회(Look up)는 수신 도큐먼트의 최상위 레벨에서 실행된다. 그 JSON 변형은 최상위 레벨의 lastname을 먼저 시도하지만, 그 전에 값을 반환하지 않으면 user 하위 문서에 중첩 된 lastname도 시도한다. 이렇게 하는 것으로 클라이언트가 공개 메소드를 호출하지 않아도, 소스 도큐먼트의 구조 변경을 간단히 경감할 수 있다 (일반적인 클래스 베이스의 페이로드 바인딩의 단점이다).

중첩된 프로젝션은 프로젝션에 따라 지원된다. 메소드가 인터페이스 이외의 복잡한 유형을 반환하는 경우 Jackson ObjectMapper이 닫힌 매핑에 사용된다.

Spring MVC의 경우, @EnableSpringDataWebSupport가 액티브하게 되어, 필요한 종속성이 클래스 패스로 사용 가능하게 되는 즉시, 필요한 컨버터가 자동적으로 등록된다. RestTemplate에서 사용하는 경우 수동으로 ProjectingJackson2HttpMessageConverter(JSON) 또는 XmlBeamHttpMessageConverter를 등록한다.

자세한 내용은 표준 Spring Data 샘플 리포지토리 웹 프로젝션의 예를 참조한다.

Querydsl 웹 지원

QueryDSL가 통합된 저장소의 경우는 Request 쿼리 문자열에 포함된 속성에서 쿼리를 파생시킬 수 있다.

다음 쿼리 문자열을 고려해야 한다.

?firstname=Dave&lastname=Matthews

이전 예제의 User 객체가 주어지면 다음과 같이 QuerydslPredicateArgumentResolver쿼리 문자열을 다음 값으로 확인할 수 있다.

QUser.user.firstname.eq("Dave").and(QUser.user.lastname.eq("Matthews"))

메소드 시그니쳐에 @QuerydslPredicate을 추가하면, 즉시 사용할 수 있는 Predicate가 제공된다. 이는 QuerydslPredicateExecutor를 사용하여 수행할 수 있다.

다음 예제에서는 메소드 시그니처에서 @QuerydslPredicate를 사용하는 방법을 보여 준다.

@Controller
class UserController {

  @Autowired UserRepository repository;

  @RequestMapping(value = "/", method = RequestMethod.GET)
  String index(Model model, @QuerydslPredicate(root = User.class) Predicate predicate,    // (1)
          Pageable pageable, @RequestParam MultiValueMap<String, String> parameters) {

    model.addAttribute("users", repository.findAll(predicate, pageable));

    return "index";
  }
}

(1) User에 일치하는 Predicate 쿼리 문자열 인수를 확인한다.

기본 바인딩은 다음과 같다.

  • eq로 간단한 속성 Object.
  • contains와 같은 속성 같은 컬렉션 Object.
  • in로 간단한 속성 Collection.

이러한 바인딩은 @QuerydslPredicate 의 bindings 속성을 사용하는지, Java 8 default methods 를 사용하여 다음과 같이 리포지터리 인터페이스에 QuerydslBinderCustomizer 메소드를 추가하는 것으로 커스터마이즈 할 수 있다.

interface UserRepository extends CrudRepository<User, String>,
                                 QuerydslPredicateExecutor<User>,                // (1)
                                 QuerydslBinderCustomizer<QUser> {               // (2)

  @Override
  default void customize(QuerydslBindings bindings, QUser user) {

    bindings.bind(user.username).first((path, value) -> path.contains(value))    // (3)
    bindings.bind(String.class)
      .first((StringPath path, String value) -> path.containsIgnoreCase(value)); // (4)
    bindings.excluding(user.password);                                           // (5)
  }
}

(1) QuerydslPredicateExecutorPredicate의 특정 파인더 메소드에 대한 액세스를 제공한다.
(2) 리포지토리 인터페이스에 정의되었지만 QuerydslBinderCustomizer 자동으로 선택되고, 바로 가기 @QuerydslPredicate(bindings=...)가 선택된다.
(3) username 속성의 바인딩을 간단한 contains 바인딩으로 정의한다. (4) String 프로퍼티의 디폴트의 바인딩을, 대문자와 소문자를 구별하지 않는 contains 일치가 되도록 정의한다. (4) password 속성을 Predicate 확인에서 제외한다.

11.8.3. 저장소 채우기(Repository Populators)

Spring JDBC 모듈을 사용하는 경우는 아마도 DataSource에 SQL 스크립트를 캡처하는 지원에 익숙할 것이다. 유사한 추상화를 저장소 레벨에서 사용할 수 있지만 저장소에 의존하지 않아야 하기 때문에 데이터 정의 언어로 SQL을 사용하지 않는다. 대리인은 XML(Spring의 OXM 추상화에 의한)과 JSON(Jackson에 의한)을 지원하여 리포지토리에 데이터를 캡처하는 데이터를 정의한다.

다음 내용의 data.json 파일이 있다고 가정한다.

예 51: JSON에 정의된 데이터

[ { "_class" : "com.acme.Person",
 "firstname" : "Dave",
  "lastname" : "Matthews" },
  { "_class" : "com.acme.Person",
 "firstname" : "Carter",
  "lastname" : "Beauford" } ]

Spring Data Commons에서 제공되는 리포지토리 이름 공간의 populator 요소를 사용하여 리포지토리에 데이터를 가져올 수 있다. 위의 데이터를 PersonRepository에 입력하려면 다음과 같은 포퓰레이터(populator)를 선언한다.

예 52: Jackson 저장소 트리포퓰레이터 선언

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:repository="http://www.springframework.org/schema/data/repository"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
    https://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/data/repository
    https://www.springframework.org/schema/data/repository/spring-repository.xsd">

  <repository:jackson2-populator locations="classpath:data.json" />

</beans>

위의 선언은 data.json 파일을 Jackson ObjectMapper의해 읽어서 역 직렬화를 한다.

JSON 객체가 정렬되지 않는 유형은 JSON 문서의 _class 속성을 검사하여 결정된다. 인프라는 궁극적으로 적절한 리포지토리를 선택하여 deserialize된 객체를 처리한다.

대신, XML을 사용하여 리포지토리로 데이터를 캡처해야 하는 데이터를 정의하려면, unmarshaller-populator 요소를 사용할 수 있다. Spring OXM에서 사용 가능한 XML 마샬러 옵션 중 하나를 사용하도록 구성한다. 자세한 내용은 Spring 참조 문서를 참조하여라. 다음 예는 JAXB를 사용하여 저장소 리포퓰레이터를 언마샬링(unmarshall)하는 방법을 보여 준다.

예 53 : 정렬되지 않은 저장소 리포퓰레이터 선언 (JAXB 사용)

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:repository="http://www.springframework.org/schema/data/repository"
  xmlns:oxm="http://www.springframework.org/schema/oxm"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
    https://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/data/repository
    https://www.springframework.org/schema/data/repository/spring-repository.xsd
    http://www.springframework.org/schema/oxm
    https://www.springframework.org/schema/oxm/spring-oxm.xsd">

  <repository:unmarshaller-populator locations="classpath:data.json"
    unmarshaller-ref="unmarshaller" />

  <oxm:jaxb2-marshaller contextPath="com.acme" />

</beans>



최종 수정 : 2021-12-28