Spring Data R2DBC | 참조 문서
12. 도입
12.1. 문서 구조
참조 문서의 이 부분에서는 Spring Data R2DBC에서 제공하는 핵심 기능에 대해 설명한다.
“R2DBC 지원“은 R2DBC 모듈의 기능 세트를 소개한다.
“R2DBC Repository“는 R2DBC 저장소 지원을 소개한다.
13. R2DBC 지원
R2DBC에는 다양한 기능이 포함되어 있다.
- R2DBC 드라이버 인스턴스의 Java 기반
@Configuration
클래스에 의한 Spring 구성 지원. - 행과 POJO 간의 통합 객체 매핑을 사용하여 일반적인 R2DBC 작업을 수행할 때 생산성을 향상시키는 엔터티 바인딩 작업의 주요 클래스인
R2dbcEntityTemplate
. - Spring의 변환 서비스(Conversion Service)와 통합된 기능이 풍부한 객체 매핑.
- 다른 메타데이터 형식을 지원하기 위해 확장 가능한 어노테이션 기반 매핑 메타데이터.
- 사용자 지정 쿼리 메소드 지원을 포함한 리포지토리 인터페이스의 자동 구현.
대부분의 작업에서는 R2dbcEntityTemplate
또는 리포지토리 지원을 사용해야 하며, 모두 많은 매핑(rich mapping) 기능을 사용한다. R2dbcEntityTemplate
는 Ad ad-hoc CRUD 작업과 같은 액세스 기능을 찾는다.
13.1. 입문
작업 환경을 설정하는 쉬운 방법은 start.spring.io을 통해 Spring 기반 프로젝트를 만드는 것이다. 이렇게 하려면 :
- 다음을 pom.xml 파일의
dependencies
요소에 추가한다.<dependencyManagement> <dependencies> <dependency> <groupId>io.r2dbc</groupId> <artifactId>r2dbc-bom</artifactId> <version>${r2dbc-releasetrain.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <!-- other dependency elements omitted --> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-r2dbc</artifactId> <version>1.4.0</version> </dependency> <!-- a R2DBC driver --> <dependency> <groupId>io.r2dbc</groupId> <artifactId>r2dbc-h2</artifactId> <version>Arabba-SR10</version> </dependency> </dependencies>
- pom.xml의 Spring 버전을 다음과 같이 변경한다.
<spring-framework.version>5.3.13</spring-framework.version>
- Maven의 Spring 마일스톤 리포지토리의 다음 위치를
<dependencies/>
요소와 동일한 레벨이되도록pom.xml
에 추가한다.<repositories> <repository> <id>spring-milestone</id> <name>Spring Maven MILESTONE Repository</name> <url>https://repo.spring.io/libs-milestone</url> </repository> </repositories>
리포지토리는 여기에서 검색할 수도 있다.
로깅 레벨을 DEBUG
로 설정하여, 추가 정보를 표시할 수도 있다. 그렇게 하려면 application.properties
파일에 다음와 같은 내용이 포함되도록 편집한다.
logging.level.org.springframework.r2dbc=DEBUG
그런 다음에는 예를 들어, Person
클래스를 만들어 다음과 같이 영속화할 수 있다.
public class Person {
private final String id;
private final String name;
private final int age;
public Person(String id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
public String getId() {
return id;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
@Override
public String toString() {
return "Person [id=" + id + ", name=" + name + ", age=" + age + "]";
}
}
그 다음으로 다음과 같이 데이터베이스에 테이블 구조를 생성해야 한다.
CREATE TABLE person
(id VARCHAR(255) PRIMARY KEY,
name VARCHAR(255),
age INT);
또한, 다음과 같이 실행할 메인 애플리케이션도 필요하다.
import io.r2dbc.spi.ConnectionFactories;
import io.r2dbc.spi.ConnectionFactory;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.test.StepVerifier;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
public class R2dbcApp {
private static final Log log = LogFactory.getLog(R2dbcApp.class);
public static void main(String[] args) {
ConnectionFactory connectionFactory = ConnectionFactories.get("r2dbc:h2:mem:///test?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE");
R2dbcEntityTemplate template = new R2dbcEntityTemplate(connectionFactory);
template.getDatabaseClient().sql("CREATE TABLE person" +
"(id VARCHAR(255) PRIMARY KEY," +
"name VARCHAR(255)," +
"age INT)")
.fetch()
.rowsUpdated()
.as(StepVerifier::create)
.expectNextCount(1)
.verifyComplete();
template.insert(Person.class)
.using(new Person("joe", "Joe", 34))
.as(StepVerifier::create)
.expectNextCount(1)
.verifyComplete();
template.select(Person.class)
.first()
.doOnNext(it -> log.info(it))
.as(StepVerifier::create)
.expectNextCount(1)
.verifyComplete();
}
}
메인 프로그램을 실행하면, 앞의 예에서는 다음과 같이 출력된다.
2018-11-28 10:47:03,893 DEBUG amework.core.r2dbc.DefaultDatabaseClient: 310 - Executing SQL statement [CREATE TABLE person
(id VARCHAR(255) PRIMARY KEY,
name VARCHAR(255),
age INT)]
2018-11-28 10:47:04,074 DEBUG amework.core.r2dbc.DefaultDatabaseClient: 908 - Executing SQL statement [INSERT INTO person (id, name, age) VALUES($1, $2, $3)]
2018-11-28 10:47:04,092 DEBUG amework.core.r2dbc.DefaultDatabaseClient: 575 - Executing SQL statement [SELECT id, name, age FROM person]
2018-11-28 10:47:04,436 INFO org.spring.r2dbc.example.R2dbcApp: 43 - Person [id='joe', name='Joe', age=34]
이 간단한 예에서도 몇 가지 주의해야 할 사항이 있다
- 표준의
io.r2dbc.spi.ConnectionFactory
객체를 사용하여, Spring Data R2DBC(R2dbcEntityTemplate
)에서 주요 도우미 클래스의 인스턴스를 생성할 수 있다. - 매퍼는 추가 메타데이터 없이, 표준 POJO 객체에서 작동한다(다만, 선택적으로 해당 정보를 제공할 수 있다. 여기를 참조하길 바란다.).
- 매핑 규칙에서는 필드 액세스를 사용할 수 있다.
Person
클래스에는 getter 밖에 없다. - 생성자의 인수 이름이 저장된 행의 열 이름과 일치하면, 그걸로 객체를 인스턴스화하는데 사용된다.
13.2. 예제 리포지토리
GitHub 리포지토리 및 몇 가지 예를 다운로드하여 사용하여 라이브러리의 동작을 확인한다.
13.3. Spring을 사용하여 관계형 데이터베이스에 연결
관계형 데이터베이스와 Spring을 사용하는 경우에 첫번째 작업 중 하나는 IoC 컨테이너를 사용하여 io.r2dbc.spi.ConnectionFactory
객체를 만드는 것이다. 지원되는 데이터베이스와 드라이버를 사용한다.
13.3.1. Java 기반 메타데이터를 사용하여 ConnectionFactory
인스턴스 등록
다음 예는 Java 기반 Bean 메타데이터를 사용하여 io.r2dbc.spi.ConnectionFactory
인스턴스를 등록하는 예를 보여준다.
예 54: Java 기반 Bean 메타데이터를 사용하여 io.r2dbc.spi.ConnectionFactory
오브젝트 등록
@Configuration
public class ApplicationConfiguration extends AbstractR2dbcConfiguration {
@Override
@Bean
public ConnectionFactory connectionFactory() {
return ...
}
}
이 접근 방법(approach)에서는 표준 io.r2dbc.spi.ConnectionFactory
인스턴스를 사용할 수 있으며, 컨테이너는 Spring의 AbstractR2dbcConfiguration
을 사용한다. ConnectionFactory
인스턴스를 직접 등록하는 것과 비교하여 구성 지원에는 @Repository
어노테이션으로 선언된 데이터 액세스 클래스에 대해 R2DBC 예외를 Spring의 이식 가능한 DataAccessException 계층의 예외로 변환하는 ExceptionTranslator
구현을 컨테이너에 제공하는 추가 이점이 있다. 이 계층 구조와 @Repository의 사용은 Spring의 DAO 지원 기능에 설명되어 있다.
AbstractR2dbcConfiguration
는 DatabaseClient
도 등록한다. 이는 데이터베이스 상호 작용 및 리포지토리 구현에 필요하다.
13.3.2. R2DBC 드라이버
Spring Data R2DBC는 R2DBC의 플러그 가능한 SPI 메커니즘을 통해 드라이버를 지원한다. Spring Data R2DBC으로 R2DBC 사양을 구현하는 임의의 드라이버를 사용할 수 있다. Spring Data R2DBC는 각 데이터베이스의 특정 기능에 반응하기 때문에 Dialect
구현이 필요한다. 그렇지 않으면, 응용 프로그램이 시작되지 않는다. Spring Data R2DBC에는, 다음의 드라이버용의 다이어렉트 구현이 함께 제공된다.
- H2 (
io.r2dbc:r2dbc-h2
) - MariaDB (
org.mariadb:r2dbc-mariadb
) - Microsoft SQL Server (
io.r2dbc:r2dbc-mssql
) - MySQL (
dev.miku:r2dbc-mysql
) - jasync-sql MySQL (
com.github.jasync-sql:jasync-r2dbc-mysql
) - Postgres (
io.r2dbc:r2dbc-postgresql
) - Oracle (
com.oracle.database.r2dbc:oracle-r2dbc
)
Spring Data R2DBC는 ConnectionFactory
를 검사하는 것에 의해서 데이터베이스 세부 사항에 반응하고, 그에 따라 적절한 데이터베이스 다이렉트를 선택한다. 사용하는 드라이버가 Spring Data R2DBC 에 아직 인식되고 있지 않는 경우는, 독자적인 R2dbcDialect
를 구성해야 한다.
Tip
다이얼렉트는ConnectionFactory
로부터 DialectResolver
에 의해 해결된다. 일반적으로 ConnectionFactoryMetadata
를 검사한다. + org.springframework.data.r2dbc.dialect.DialectResolver$R2dbcDialectProvider
로부터 META-INF/spring.factories
를 구현하는 클래스를 등록하는 것으로, Spring에 R2dbcDialect
를 자동 검출시킬 수가 있다. DialectResolver
는 Spring의 SpringFactoriesLoader
를 사용하여 클래스 패스로부터 다이얼렉트 프로바이더의 구현을 검출한다.
13.4. R2dbcEntityOperations 데이터 액세스 API
R2dbcEntityTemplate
는 Spring Data R2DBC의 핵심 진입점이다. 데이터를 쿼리, 삽입, 업데이트, 삭제와 같은 일반적인 임시(ad-hoc) 사용 사용를 위한 직접적인 엔티티 지향 방법과 보다 좁게 플루언트 인터페이스(fluent interface)를 제공한다.
플루언트 인터페이스(fluent interface)는 메소드 체이닝에 상당 부분 기반한 객체 지향 API 설계 메소드이며, 소스 코드의 가독성을 산문과 유사하게 만드는 것이 목적이다. - 출처: 위키백과
진입점(insert()
, select()
, update()
등)은 수행할 작업을 기반으로 하는 자연스러운 명명 스키마를 따른다. 진입점에 계속해서 API는 SQL문를 작성하고 실행하는 종료 메소드에 연결되는 문맥 의존의 메소드만을 제공하도록 설계되고 있다. Spring Data R2DBC는, R2dbcDialect
추상화를 하여 바인드 마커, 페이지네이션 지원 및 기본 드라이버에 의해 네이티브에 서포트되는 데이터형을 결정한다.
Info
모든 터미널 메소드는 항상 원하는 조작을 하는Publisher
유형을 반환한다. 실제 명령문은 구독 시 데이터베이스로 전송된다.
13.4.1. 엔티티를 삽입하고 업데이트하는 방법
R2dbcEntityTemplate
에는 객체를 저장하고 삽입하는 편리한 방법이 있다. 변환 프로세스를 보다 세밀하게 제어하기 위해 Spring 컨버터를 R2dbcCustomConversions
에 등록할 수 있다(예를 들면, Converter<Person, OutboundRow>
, Converter<Row, Person>
).
저장 작업을 사용하는 간단한 경우는 POJO를 저장하는 것이다. 이 경우에 테이블 이름은 클래스의 이름(전체 규정이 아님)에 의해 결정된다. 특정 컬렉션 이름을 사용하여 저장 작업을 호출할 수도 있다. 매핑 메타데이터를 사용하여 객체를 저장하는 컬렉션을 재정의할 수 있다.
삽입 또는 저장할 때에 Id
프로퍼티이 설정되지 않은 경우, 해당 값은 데이터베이스에서 자동으로 생성된다고 가정한다. 자동 생성의 경우에는 클래스내의 Id
프로퍼티 또는 필드의 타입은 Long
또는 Integer
가 아니면 안된다.
다음 예제는 행을 삽입하고 그 내용을 얻는 방법을 보여준다.
예 55: R2dbcEntityTemplate를 사용하여 엔티티 삽입 및 검색
Person person = new Person("John", "Doe");
Mono<Person> saved = template.insert(person);
Mono<Person> loaded = template.selectOne(query(where("firstname").is("John")),
Person.class);
다음 삽입 및 갱신 작업을 사용할 수 있다.
유사한 삽입 작업 세트도 사용할 수 있다.
-
Mono<T> insert (T objectToSave)
: 객체를 기본 테이블에 삽입한다. -
Mono<T> update (T objectToSave)
: 객체를 기본 테이블에 삽입한다.
흐르는 API를 사용하여, 테이블 이름을 사용자 지정할 수 있다.
13.4.2. 데이터 선택
R2dbcEntityTemplate
의 select(...)
와 selectOne(...)
메소드는 테이블로부터 데이터를 선택하는데 사용된다. 두 메소드 모두 Query
필드 프로젝션(field projection), WHERE
절, ORDER BY
절, 제한/오프셋 페이징을 정의하는 객체를 사용한다. 제한/오프셋 기능은 기본 데이터베이스에 관계없이 응용 프로그램에 투명한다. 이 기능은 각각의 SQL 플레이버간의 차이에 대응하기 위한 R2dbcDialect
추상화에 의해 지원되고 있다.
예 56: R2dbcEntityTemplate
을 사용하여 엔티티 선택
Flux<Person> loaded = template.select(query(where("firstname").is("John")),
Person.class);
13.4.3. Fluent API
이 섹션에서는 플루언트 API의 사용 부분에 대해 설명한다. 다음 간단한 쿼리를 생각해 보자.
Flux<Person> people = template.select(Person.class) // (1)
.all(); // (2)
(1) select(...)
메소드에서 Person
를 사용하면, 테이블 형식의 결과가 Person
결과 객체에 매핑된다.
(2) all()
행을 패칭(Fetching)하면 결과를 제한하지 않는 Flux<Person>
가 반환된다.
다음 예제에서는 이름, WHERE
조건 및 ORDER BY
절로 테이블 이름을 지정하는 보다 복잡한 쿼리를 선언한다.
Mono<Person> first = template.select(Person.class) // (1)
.from("other_person")
.matching(query(where("firstname").is("John") // (2)
.and("lastname").in("Doe", "White"))
.sort(by(desc("id")))) // (3)
.one(); // (4)
(1) 테이블에서 이름으로 선택하면, 지정된 도메인 유형을 사용하는 행의 결과가 반환된다.
(2) 발행된 쿼리는 결과를 필터링으로 firstname
와 lastname
열에 WHERE
조건을 선언한다.
(3) 결과는 각각의 컬럼 이름으로 재정렬될 수 있으며, 결과로 ORDER BY
절이 생성된다.
(4) 하나의 결과를 선택하면 한 행만 페치된다. 행을 소비하는 이 방법에서는 쿼리가 정확히 단일 결과를 반환할 것으로 예상된다. 쿼리의 결과가 둘 이상인 경우에 Mono
라면 IncorrectResultSizeDataAccessException
이 발생된다.
Tip
select(Class<?>)
를 통해 대상 유형을 지정하면, “프로젝션"을 결과에 직접 적용할 수 있다.
다음 종료 방법을 사용하여 단일 엔티티 검색과 여러 엔터티 검색을 전환할 수 있다.
first()
: 첫 번째 행만 사용하여Mono
를 반환한다. 쿼리가 결과를 반환하지 않으면, 반환된Mono
객체를 게시하지 않고 완료된다.one()
:Mono
를 반환하는 한 줄만 사용한다. 쿼리가 결과를 반환하지 않으면 반환된Mono
객체를 게시하지 않고 완료된다. 쿼리가 여러 행을 반환하면Mono
는IncorrectResultSizeDataAccessException
예외가 발생한다.all()
:Flux
을 반환하는 모든 반환된 행을 사용한다.count()
:Mono<Long>
을 반환하는 카운트 프로젝션을 적용한다.exists()
:Mono<Boolean>
를 반환하여, 쿼리가 행을 생성하는지 여부를 반환한다.
select()
진입점을 사용하여 SELECT
쿼리를 표현할 수 있다. 결과의 SELECT
쿼리는 자주 사용되는 절(WHERE
및 ORDER BY
)을 지원하고 페이지네이션을 지원한다. 플루언트 API 스타일에 의해, 체인은 복수의 메소드를 함께 이해하면서, 코드를 이해하기 쉬워진다. 가독성을 높이기 위해 Criteria
인스턴스를 만드는 데 ‘새로운’ 키워드를 사용하지 않는 정적 가져오기를 사용할 수 있다.
Criteria 클래스의 메소드
Criteria
클래스는 다음의 메소드를 제공한다. 이러한 메소드는 모두 SQL 연산자에 대응하고 있다.
Criteria
and(String column)
: 지정된property
을 가진 연결된Criteria
를 현재의Criteria
에 추가하고 새로 작성된Criteria
을 반환한다.Criteria
or(String column)
: 지정된property
을 가진 연결된Criteria
를 현재의Criteria
에 추가하고 새로 작성된Criteria
을 반환한다.Criteria
greaterThan(Object o)
:>
연산자를 사용하여 기준을 만든다.Criteria
greaterThanOrEquals(Object o)
:>=
연산자를 사용하여 기준을 만든다.Criteria
in(Object... o)
: varargs 인수에IN
연산자를 사용하여 기준을 만든다.Criteria
in(Collection<?> collection)
: 컬렉션을 사용하여IN
연산자를 사용하여 기준을 만든다.Criteria
is(Object o)
: 열 매칭(property = value
)를 사용하여 기준을 만든다.Criteria
isNull()
:IS NULL
연산자를 사용하여 기준을 만든다.Criteria
isNotNull()
:IS NOT NULL
연산자를 사용하여 기준을 만든다.Criteria
lessThan(Object o)
:<
연산자를 사용하여 기준을 만든다.Criteria
lessThanOrEquals(Object o)
:<=
연산자를 사용하여 기준을 만든다.Criteria
like(Object o)
: 이스케이프 문자 처리 없이LIKE
연산자를 사용하여 기준을 만든다.Criteria
not(Object o)
:!=
연산자를 사용하여 기준을 만든다.Criteria
notIn(Object... o)
: varargs 인수에NOT IN
연산자를 사용하여 기준을 만든다.Criteria
notIn(Collection<?> collection)
: 컬렉션을 사용하여NOT IN
연산자를 사용하여 기준을 만든다.
Criteria
은 SELECT
, UPDATE
, DELETE
쿼리를 사용할 수 있다.
13.4.4. 데이터 삽입
insert()
진입점을 사용하여 데이터를 삽입할 수 있다.
다음 간단한 형식화된 삽입 작업을 보도록 하자.
Mono<Person> insert = template.insert(Person.class) // (1)
.using(new Person("John", "Doe")); // (2)
(1) into(...)
메소드로 Person
를 사용하면, 매핑 메타데이터를 기반으로 INTO
테이블이 설정된다. 또한 삽입할 Person
객체를 허용하는 insert
문을 준비한다.
(2) 스칼라 Person
객체를 제공한다. 또는 Publisher
를 지정하여 INSERT
명령문 스트림을 실행할 수 있다. 이 메소드는 모든 null
이 아닌 값을 추출하고 삽입한다.
13.4.5. 데이터 업데이트
update()
진입점을 사용하여 행을 업데이트할 수 있다. 데이터 업데이트는 할당을 지정하는 Update
을 수락하고 업데이트할 테이블을 지정하는 것으로 시작된다. 또한 Query
을 수락하여 WHERE
절을 만든다.
다음 간단한 유형이 지정된 업데이트 작업을 보도록 하자.
Person modified = ...
Mono<Integer> update = template.update(Person.class) // (1)
.inTable("other_table") // (2)
.matching(query(where("firstname").is("John"))) // (3)
.apply(update("age", 42)); // (4)
(1) Person
객체를 업데이트하고, 매핑 메타데이터를 기반으로 매핑을 적용한다.
(2) inTable(...)
메소드를 호출하여, 다른 테이블 이름을 설정한다.
(3) WHERE
절로 변환되는 쿼리를 지정한다.
(4) Update
객체를 적용한다. 이 경우는 age
를 42
로 설정하고 영향을 받는 행 수를 반환한다.
13.4.6. 데이터 삭제
delete()
진입점을 사용하여 행을 삭제할 수 있다. 데이터 삭제는 삭제할 테이블 지정으로 시작하고 선택적으로 Criteria
를 수락하여 WHERE
절을 만든다.
다음 간단한 삭제 작업을 보도록 하자.
Mono<Integer> delete = template.delete(Person.class) // (1)
.from("other_table") // (2)
.matching(query(where("firstname").is("John"))) // (3)
.all(); // (4)
(1) Person
객체를 삭제하고, 매핑 메타데이터를 기반으로 매핑을 적용한다.
(2) from(...)
메소드를 호출하여, 다른 테이블 이름을 설정한다.
(3) WHERE
절로 변환되는 쿼리를 지정한다.
(4) 삭제 조작을 적용하여 영향을 받는 행 수를 리턴한다.
14. R2DBC Repository
이 장에서는 R2DBC의 리포지토리 지원 전문 분야에 대해 설명한다. 이 섹션에서는 Spring Data Repository 작업에서 설명한 핵심 저장소 지원을 기반으로 한다. 이 섹션을 읽기 전에 여기에 설명된 기본 개념을 확실히 이해해야 한다.
14.1. 사용방법
관계형 데이터베이스에 저장된 도메인 엔티티에 액세스하려면, 고급 리포지토리 지원을 사용하여 구현을 크게 용이하게 한다. 이렇게 하려면 리포지토리 인터페이스를 만든다. 다음의 Person
클래스를 보도록 하자.
예 57: 샘플 Person 엔티티
public class Person {
@Id
private Long id;
private String firstname;
private String lastname;
// ... getters and setters omitted
}
다음 예는 이전의 Person
클래스의 저장소 인터페이스를 보여준다.
예 58: Person 엔티티를 영속화하는 기본 리포지토리 인터페이스
public interface PersonRepository extends ReactiveCrudRepository<Person, Long> {
// additional custom query methods go here
}
R2DBC 리포지토리를 구성하려면 @EnableR2dbcRepositories
어노테이션을 사용할 수 있다. 기본 패키지가 구성되지 않은 경우 인프라스트럭처는 어노테이션이 달린 구성 클래스의 패키지를 스캔한다. 다음 예는 리포지토리에 Java 구성을 사용하는 방법을 보여준다.
예 59: 리포지토리의 Java 구성
@Configuration
@EnableR2dbcRepositories
class ApplicationConfig extends AbstractR2dbcConfiguration {
@Override
public ConnectionFactory connectionFactory() {
return ...
}
}
도메인 리포지토리는 ReactiveCrudRepository
상속되므로 엔터티에 액세스하기 위한 리액티브 CRUD 작업을 제공한다. ReactiveCrudRepository
뿐만 아니라 PagingAndSortingRepository
와 비슷한 정렬 기능을 추가하는 ReactiveSortingRepository
도 있다. 리포지토리 인스턴스 작업은 클라이언트에 삽입하는 종속성 문제일 뿐이다. 다음 코드로 모든 Person
객체를 얻을 수 있다.
예 60: 개인 엔티티에 대한 페이징 액세스
@ExtendWith(SpringExtension.class)
@ContextConfiguration
class PersonRepositoryTests {
@Autowired
PersonRepository repository;
@Test
void readsAllEntitiesCorrectly() {
repository.findAll()
.as(StepVerifier::create)
.expectNextCount(1)
.verifyComplete();
}
@Test
void readsEntitiesByNameCorrectly() {
repository.findByFirstname("Hello World")
.as(StepVerifier::create)
.expectNextCount(1)
.verifyComplete();
}
}
앞의 예에서는 Spring 단위 테스트 지원을 사용하여 응용 프로그램 컨텍스트를 만든다. 이렇게 하면 테스트 케이스에 어노테이션 기반 의존 주입이 수행된다. 테스트 메소드 내에서는 리포지토리를 사용하여 데이터베이스에 쿼리를 실행한다. 결과에 대한 기대를 검증하기 위해 테스트 지원으로 StepVerifier
사용한다.
14.2. 쿼리 메소드
일반적으로 리포지토리에서 트리거하는 데이터 액세스의 대부분은 데이터베이스에 대해 실행되는 쿼리이다. 이러한 쿼리의 정의는 다음 예제와 같이 리포지토리 인터페이스에서 메소드를 선언하는 것이다.
예 61: PersonRepository 및 쿼리 메소드
interface ReactivePersonRepository extends ReactiveSortingRepository<Person, Long> {
Flux<Person> findByFirstname(String firstname); // (1)
Flux<Person> findByFirstname(Publisher<String> firstname); // (2)
Flux<Person> findByFirstnameOrderByLastname(String firstname, Pageable pageable); // (3)
Mono<Person> findByFirstnameAndLastname(String firstname, String lastname); // (4)
Mono<Person> findFirstByLastname(String lastname); // (5)
@Query("SELECT * FROM person WHERE lastname = :lastname")
Flux<Person> findByLastname(String lastname); // (6)
@Query("SELECT firstname, lastname FROM person WHERE lastname = $1")
Mono<Person> findFirstByLastname(String lastname); // (7)
}
(1) 이 메소드는 지정한 firstname
을 가진 모든 사람들을 조회하는 쿼리이다. 쿼리는 And
및 Or
를 연결할 수 있는 제약의 메소드 이름을 구문 분석하여 파생된다. 메소드 이름은 SELECT ... FROM person WHERE firstname = :firstname
와 같은 쿼리 식이 된다.
(2) 이 메소드는 지정된 Publisher
에 의해 firstname
발급되면, 지정된 firstname
을 가진 모든 사람의 쿼리를 나타낸다.
(3) Pageable
를 사용하여 오프셋 및 정렬 매개 변수를 데이터베이스에 전달한다.
(4) 지정된 조건으로 단일 엔티티를 검색한다. 고유하지 않은 결과에 대해서는 IncorrectResultSizeDataAccessException
으로 완료된다.
(5) <4>
가 아니면 쿼리에서 더 많은 결과 행을 생성하더라도 첫 번째 엔터티만 항상 보내진다.
(6) findByLastname
메소드는 지정된 성을 가진 모든 사람들의 쿼리를 표시한다.
(7) firstname
및 lastname
열만 보여주는 단일 Person
엔티티를 조회한다. 어노테이션이 있는 쿼리는 이 예에서 Postgres
바인딩 마커인 네이티브 바인드 마커를 사용한다.
@Query
어노테이션에서 사용되는 select
문의 열은 각각의 속성에 대해 NamingStrategy
에 의해 생성된 이름과 일치해야 한다. select
문과 일치하는 열이 포함되어 있지 않으면, 해당 속성이 설정되지 않는다. 해당 속성이 영속성 생성자에 필요한 경우는 null 또는 (프리미티브형의 경우) 디폴트치가 제공된다.
다음 표는 쿼리 메소드에서 지원되는 키워드를 보여준다.
표 2: 쿼리 메소드에서 지원되는 키워드
키워드 | 샘플 | 논리적 결과 |
---|---|---|
After |
findByBirthdateAfter(Date date) |
birthdate > date |
GreaterThan |
findByAgeGreaterThan(int age) |
age > age |
GreaterThanEqual |
findByAgeGreaterThanEqual(int age) |
age >= age |
Before |
findByBirthdateBefore(Date date) |
birthdate < date |
LessThan |
findByAgeLessThan(int age) |
age < age |
LessThanEqual |
findByAgeLessThanEqual(int age) |
age <= age |
Between |
findByAgeBetween(int from, int to) |
age BETWEEN from AND to |
NotBetween |
findByAgeNotBetween(int from, int to) |
age NOT BETWEEN from AND to |
In |
findByAgeIn(Collection<Integer> ages) |
age IN (age1, age2, ageN) |
NotIn |
findByAgeNotIn(Collection ages) |
age NOT IN (age1, age2, ageN) |
IsNotNull, NotNull |
findByFirstnameNotNull() |
firstname IS NOT NULL |
IsNull, Null |
findByFirstnameNull() |
firstname IS NULL |
Like, StartingWith, EndingWith |
findByFirstnameLike(String name) |
firstname LIKE name |
NotLike, IsNotLike |
findByFirstnameNotLike(String name) |
firstname NOT LIKE name |
문자열의 Containing |
findByFirstnameContaining(String name) |
firstname LIKE '%' + name +'%' |
문자열의 NotContaining |
findByFirstnameNotContaining(String name) |
firstname NOT LIKE '%' + name + '%' |
(No keyword) |
findByFirstname(String name) |
firstname = name |
Not |
findByFirstnameNot(String name) |
firstname != name |
IsTrue, True |
findByActiveIsTrue() |
active IS TRUE |
IsFalse, False |
findByActiveIsFalse() |
active IS FALSE |
14.2.1. 쿼리 변경
이전 섹션에서는 특정 엔터티 또는 엔터티 컬렉션에 액세스하기 위한 쿼리를 선언하는 방법에 대해 설명하였다. 이전 표의 키워드를 사용하면 delete...By
또는 remove...By
를 조합하여 일치하는 행을 삭제하는 파생 쿼리를 만들 수 있다.
예 62: Delete...By
쿼리
interface ReactivePersonRepository extends ReactiveSortingRepository<Person, String> {
Mono<Integer> deleteByLastname(String lastname); // (1)
Mono<Void> deletePersonByLastname(String lastname); // (2)
Mono<Boolean> deletePersonByLastname(String lastname); // (3)
}
(1) Mono<Integer>
반환 유형을 사용하면, 영향을 받는 행 수를 반환한다.
(2) Void
를 사용하면 결과 값을 출력하지 않고 행이 성공적으로 삭제되었는지 여부를 보고한다.
(3) Boolean
를 사용하면 적어도 하나의 행이 삭제되었는지 여부를 보고한다.
이 접근 방식은 포괄적인 사용자 지정 기능에 적합하므로, 다음 예제와 같이 쿼리 메소드에 @Modifying
어노테이션을 붙여 매개 변수 바인딩만 필요한 쿼리를 변경할 수 있다.
@Modifying
@Query("UPDATE person SET firstname = :firstname where lastname = :lastname")
Mono<Integer> setFixedFirstnameFor(String firstname, String lastname);
변경 쿼리의 결과는 다음과 같다.
Void
(또는 KotlinUnit
)은 업데이트 카운트를 버리고 완료를 기다린다.- 영향을 받는 행 수를 출력하는
Integer
또는 다른 숫자 유형. - 적어도 1개의 행이 갱신되었는지 어떤지를 출력하는
Boolean
.
@Modifying
어노테이션은 @Query
어노테이션과 결합한 경우에만 관련이 있다. 파생된 사용자 지정 메소드에서는 이 어노테이션이 필요하지 않는다.
또는 Spring Data 저장소의 커스텀 구현에서 설명하고 있는 있는 기능을 사용하여 커스텀 변경 동작을 추가할 수 있다.
14.2.2. SpEL 표현식을 사용한 쿼리
쿼리 문자열 정의를 SpEL 식과 함께 사용하여 런타임에 동적 쿼리를 만들 수 있다. SpEL 식은 쿼리를 실행하기 직전에 평가되는 서술 값(predicate values)을 제공할 수 있다.
표현식은 모든 인수를 포함하는 배열을 통해 메소드 인수를 공개한다. 다음 쿼리는 [0]
를 사용하여, lastname
의 서술 값을 선언한다(이는 :lastname
매개 변수 바인딩과 동일).
@Query("SELECT * FROM person WHERE lastname = :#{[0]}")
Flux<Person> findByQueryWithExpression(String lastname);
쿼리 문자열 SpEL은 쿼리를 향상시키는 강력한 방법이다. 그러나 이런 것들은 또한 불필요한 인수의 넓은 범위를 받아들일 수 있다. 쿼리에 불필요한 변경이 발생하지 않도록 쿼리에 전달하기 전에 문자열을 제거해야 한다.
표현식 지원은 Query SPI:org.springframework.data.spel.spi.EvaluationContextExtension
를 통해 확장 가능하다. Query SPI는 속성과 기능을 제공하고, 루트 객체를 사용자 지정할 수 있다. 확장 기능은 쿼리를 만들 때 SpEL 평가할 시에 응용 프로그램 컨텍스트에서 가져온다.
Info
SpEL 표현식을 일반 매개 변수와 함께 사용하는 경우에는 네이티브 바인드 마커 대신 명명된 매개변수 표기법을 사용하여 적절한 바인드 순서를 보장한다.14.2.3. 예시에 의한 문의 (Query By Example)
Spring Data R2DBC에서는 Query By Example을 사용하여 쿼리를 만들 수도 있다. 이 기술을 사용하면 “프로브(probe)” 객체를 사용할 수 있다. 기본적으로 비어 있거나 null
이 아닌 모든 필드가 일치하는데 사용된다.
예를 들면 다음과 같다.
Employee employee = new Employee(); // (1)
employee.setName("Frodo");
Example<Employee> example = Example.of(employee); // (2)
Flux<Employee> employees = repository.findAll(example); // (3)
// do whatever with the flux
(1) 조건을 사용하여 도메인 객체를 만든다(null
필드는 무시된다).
(2) 도메인 객체를 사용하여, Example
을 만든다.
(3) R2dbcRepository
을 통해, 쿼리를 실행한다(Mono
에는 findOne
을 사용한다).
이는 도메인 객체를 사용하여 간단한 프로브를 만드는 방법을 보여준다. 이 경우는 Frodo
와 같은 Employee
객체의 name
필드를 기반으로 쿼리를 실행한다. null
필드는 무시된다.
Employee employee = new Employee();
employee.setName("Baggins");
employee.setRole("ring bearer");
ExampleMatcher matcher = matching() // (1)
.withMatcher("name", endsWith()) // (2)
.withIncludeNullValues() // (3)
.withIgnorePaths("role"); // (4)
Example<Employee> example = Example.of(employee, matcher); // (5)
Flux<Employee> employees = repository.findAll(example);
// do whatever with the flux
(1) 모든 필드와 일치하는 사용자 정의 ExampleMatcher
를 만든다(matchingAny()
를 사용하여 ANY 필드 일치)
(2) name
필드는 필드의 뒷부분과 일치하는 와일드카드를 사용한다.
(3) 열을 null
과 일치시킨다(관계형 데이터베이스의 경우는 NULL
이 NULL
과 같지 않음을 잊지 말자).
(4) 쿼리를 만들 때에 role
필드를 무시한다.
(5) 사용자 정의 ExampleMatcher
를 프로브에 연결한다.
모든 속성에 withTransform()
적용하여 쿼리를 만들기 전에 속성을 변환할 수도 있다. 예: 쿼리가 생성되기 전에 toUpperCase()
을 String
-based(기반) 속성에 적용할 수 있다.
예시적인 쿼리는 쿼리에 필요한 모든 필드를 미리 모르는 경우에 매우 유용하다. 사용자가 필드를 선택할 수 있는 웹 페이지에서 필터를 만드는 경우에 Query By Example은 이를 효율적인 쿼리에 유연하게 통합하는 좋은 방법이다.
14.2.4. 엔티티 상태 검출 전략
다음 표는 엔터티가 새로운지에 대한 여부를 감지하기 위해 Spring Data가 제공하는 전략을 설명한다.
표 3 : Spring Data에서 엔티티가 새로운지 여부를 감지하는 옵션
@Id - 속성 검사(기본값) |
기본적으로 Spring Data는 지정된 엔티티의 식별자 속성을 검사한다. 기본 유형인 경우에는 식별자 속성이 null 또는 0 인 엔티티는 새로운 것으로 간주된다. 그렇지 않으면 새로운 것이 아닌 것으로 간주된다. |
---|---|
@Version - 속성 검사 |
@Version 으로 어노테이션이 달린 속성이 존재하는 null 인 경우 또는 기본 유형인 0 의 버전 속성의 경우에 엔터티는 새로운 것으로 간주된다. 버전 속성은 존재하는 값이 다른 경우, 엔티티는 신규가 아닌 것으로 간주된다. 버전 속성이 존재하지 않으면 Spring Data는 식별자 속성의 검사로 되돌아 간다. |
Persistable 구현 |
엔티티가 Persistable 를 구현하고 있는 경우, Spring Data 는 엔티티의 isNew(...) 메소드에 새로운 검출을 위양한다. 자세한 내용은 Javadoc 을 참조하라참고: AccessType.PROPERTY 를 사용하면 Persistable 의 속성이 검색되고 유지된다. 이를 방지하려면 @Transient 을 사용한다. |
사용자 정의 EntityInformation 구현 제공 |
모듈 고유의 리포지토리 팩토리의 서브 클래스를 작성하여 getEntityInformation(...) 메소드를 오버라이드(override) 하는 것으로, 리포지터리 베이스의 구현으로 사용되는 EntityInformation 추상화를 커스터마이즈 할 수 있다. 다음에 모듈 고유의 리포지토리 팩토리의 커스텀 구현을 Spring Bean 로서 등록할 필요가 있다. 이것이 필요한 것은 거의 없다. |
14.2.5. ID 생성(Generation)
Spring Data R2DBC는 ID를 사용하여 엔티티를 식별한다. 엔티티의 ID는 Spring Data의 @Id
어노테이션이 붙어야 한다.
데이터베이스에 ID 열의 자동 증가(auto-increment) 열이 있는 경우는 생성된 값은 데이터베이스에 삽입된 후 엔터티로 설정된다.
Spring Data R2DBC는 엔티티가 새롭고 식별자의 값이 디폴트로 초기값이 되어 있는 경우, 식별자의 열의 값을 삽입하려고 하지 않는다. 이는 일반 유형의 경우는 0
이 되고, 식별자 프로퍼티가 Long
등의 수치 래퍼 유형을 사용하고 있는 경우는 null
이다.
중요한 제약 중 하나는 엔티티를 저장한 후에 엔티티가 새로운 것이 아니어야 한다는 것이다. 그 엔티티가 새로운지 여부는 엔티티 상태의 일부이다. 자동 증가 열은 ID 열의 값을 사용하여 Spring Data에 의해 ID를 설정하기 때문에 자동으로 수행된다.
14.2.6. 낙관적 잠금 (Optimistic Locking)
@Version
어노테이션은 R2DBC 컨텍스트에서 JPA와 유사한 구문을 제공하여 업데이트가 일치하는 버전의 행에만 적용되도록 한다. 버전 속성의 실제 값은 다른 작업이 그 사이에 행을 변경할 때 업데이트가 영향을 주지 않도록 업데이트 쿼리에 추가된다. 이 경우는 OptimisticLockingFailureException
가 throw 된다. 다음 예제는 이러한 기능을 보여준다.
@Table
class Person {
@Id Long id;
String firstname;
String lastname;
@Version Long version;
}
R2dbcEntityTemplate template = ...;
Mono<Person> daenerys = template.insert(new Person("Daenerys")); // (1)
Person other = template.select(Person.class)
.matching(query(where("id").is(daenerys.getId())))
.first().block(); // (2)
daenerys.setLastname("Targaryen");
template.update(daenerys); // (3)
template.update(other).subscribe(); // emits OptimisticLockingFailureException // (4)
(1) 먼저 행을 삽입한다. version
로 0
으로 설정된다.
(2) 방금 삽입한 행을 로드한다. version
그것은 0
으로 남아있다.
(3) 행을 version = 0
으로 업데이트한다. lastname
을 설정하고 version
을 1
로 범프(bump)한다.
(4) version = 0
이 아직 남아 있는 이전에 로드된 행을 업데이트해 보자. 현재 version
는 1
이므로 작업이 OptimisticLockingFailureException
으로 실패한다.
14.2.7. 프로젝션 (Projections)
Spring Data 쿼리 메소드는 일반적으로 리포지토리에 의해 관리되는 집계 루트의 하나 이상의 인스턴스를 반환한다. 다만, 이러한 유형의 특정 속성을 기반으로 프로젝션을 만드는 것이 바람직할 수 있다. Spring Data 에서는 전용의 반환값형을 모델화하여 관리 대상 집합체의 부분 뷰를 보다 선택적으로 얻을 수 있다.
다음 예제와 같은 저장소 및 집계 루트 유형을 보도록 하자.
예 63: 샘플 집계 및 리포지토리
class Person {
@Id UUID id;
String firstname, lastname;
Address address;
static class Address {
String zipCode, city, street;
}
}
interface PersonRepository extends Repository<Person, UUID> {
Flux<Person> findByLastname(String lastname);
}
여기에서 사람의 이름 속성만 검색한다고 해보자. Spring Data는 이를 달성하기 위해 어떤 의미를 가지고 있을까? 이 섹션의 나머지는 그 질문에 답한다.
인터페이스 기반 프로젝션
쿼리 결과를 이름 속성에만 제한하는 가장 쉬운 방법은 다음 예제와 같이 읽을 속성의 접근자 메소드를 공개하는 인터페이스를 선언하는 것이다.
예 64 : 속성의 서브 세트를 취득하는 프로젝션 인터페이스
interface NamesOnly {
String getFirstname();
String getLastname();
}
여기서 중요한 것은 여기에 정의된 속성이 집계 경로의 속성과 정확히 일치한다는 것이다. 이렇게 하면 쿼리 메소드를 다음과 같이 추가할 수 있다.
예 65: 쿼리 메소드에서 인터페이스 기반 프로젝션을 사용하는 리포지토리
interface PersonRepository extends Repository<Person, UUID> {
Flux<NamesOnly> findByLastname(String lastname);
}
쿼리 실행 엔진은 반환된 각 요소에 대해 런타임에 해당 인터페이스의 프록시 인스턴스를 만들고 게시된 메소드에 대한 호출을 대상 객체로 전달한다.
Info
기본 메소드 (예 :CrudRepository
저장소 특정 리포지토리 인터페이스 또는 Simple...Repository
선언됨)를 재정의(override)하는 메소드를 Repository
선언하면 선언된 리턴 유형에 관계없이 기본 메소드가 호출된다. 기본 메소드는 프로젝션에 사용할 수 없으므로 호환 가능한 리턴 유형을 사용해야 한다. 일부 저장소 모듈은 @Query
어노테이션을 지원하여 재정의된 기본 메소드를 쿼리 메소드로 변환한다. 이 쿼리 메소드를 사용하여 프로젝션을 반환할 수 있다.
프로젝션은 재귀적으로 사용할 수 있다. Address
정보의 일부를 포함하고 싶다면, 다음의 예와 같이 그렇게 하기 위해 투용 인터페이스를 작성하여 getAddress()
의 선언으로부터 그 인터페이스를 반환한다.
예 66 : 속성의 서브 세트를 취득하는 프로젝션 인터페이스
interface PersonSummary {
String getFirstname();
String getLastname();
AddressSummary getAddress();
interface AddressSummary {
String getCity();
}
}
메소드의 호출시에, 타겟 인스턴스의 address
프로퍼티를 받아와서 순서대로 프로젝션 프록시에 감싸지게 된다.
닫힌 프로젝션 (Closed Projections)
접근자 메소드가 모든 대상 집합의 속성과 일치하는 프로젝션 인터페이스는 닫힌 프로젝션으로 간주된다. 다음 예제(이 섹션의 전반부에서도 사용했다)는 닫힌 프로젝션이다.
예 67: 닫힌 프로젝션
interface NamesOnly {
String getFirstname();
String getLastname();
}
닫힌 프로젝션을 사용하는 경우는 Spring Data는 쿼리 실행을 최적화할 수 있다. 이는 프로젝션 프록시 백업에 필요한 모든 속성을 알고 있기 때문이다. 자세한 내용은 참조 문서의 모듈별 부분을 참조하여라.
열린 프로젝션 (Open Projections)
다음 예제와 같이 @Value
어노테이션을 사용하여 프로젝션 인터페이스의 접근자 메소드를 사용하여 새 값을 계산할 수도 있다.
예 68: 열린 프로젝션
interface NamesOnly {
@Value("#{target.firstname + ' ' + target.lastname}")
String getFullName();
...
}
프로젝션을 지원하는 집계 루트는 target
변수에서 사용할 수 있다. @Value
를 사용한 프로젝션 인터페이스는 개방 프로젝션이다. 이 경우 Spring Data는 쿼리 실행 최적화를 적용할 수 없다. 이는 SpEL 표현식이 집계 루트의 모든 속성을 사용할 수 있기 때문이다.
@Value
에서 사용되는 표현식은 너무 복잡해서는 안된다. String 변수에서 프로그래밍을 피하고 싶을 것이다. 매우 간단한 식의 경우에 첫번째 옵션은 다음의 예제와 같이 기본 메소드(Java 8에서 도입)를 사용하는 것이다.
예 69: 사용자 정의 로직에 기본 메소드를 사용하는 프로젝션 인터페이스
interface NamesOnly {
String getFirstname();
String getLastname();
default String getFullName() {
return getFirstname().concat(" ").concat(getLastname());
}
}
이 접근 방식은 프로젝션 인터페이스에서 공개되는 다른 접근자 메소드에 순수한 기반으로 논리를 구현할 수 있어야 합니다. 더 유연한 두번째 옵션은 Spring Bean에 사용자 정의 로직을 구현하고 다음 예제와 같이 SpEL 표현식에서 호출하는 것이다.
예 70: 간단한 Person 객체
@Component
class MyBean {
String getFullName(Person person) {
...
}
}
interface NamesOnly {
@Value("#{@myBean.getFullName(target)}")
String getFullName();
...
}
SpEL 표현식이 myBean
을 참조하여 getFullName(...)
메소드를 호출하고, 프로젝션 대상을 메소드 매개 변수로 전송하는 방법에 주목하자. SpEL식의 평가를 뒷받침하는 메소드는 메소드 매개 변수를 사용할 수도 있다. 이 매개 변수는 표현식에서 참조할 수 있다. 메소드의 매개 변수는 args
라는 Object
배열을 통해 사용할 수 있다. 다음 예제에서는 args
배열에서 메소드 매개 변수를 검색하는 방법을 보여 준다.
예 71: 샘플 Person 객체
interface NamesOnly {
@Value("#{args[0] + ' ' + target.firstname + '!'}")
String getSalutation(String prefix);
}
다시 말하지만, 더 복잡한 표현식의 경우 이전에 설명한 대로 Spring Bean을 사용하고 표현식이 메소드를 호출하도록 해야 한다.
null 허용 래퍼 (Nullable Wrappers)
프로젝션 인터페이스의 Getter는 null 허용 래퍼를 사용하여 null의 안전성을 향상시킬 수 있다. 현재 지원되는 래퍼 유형은 다음과 같다.
java.util.Optional
com.google.common.base.Optional
scala.Option
io.vavr.control.Option
예 72: null 허용 래퍼를 사용한 프로젝션 인터페이스
interface NamesOnly {
Optional<String> getFirstname();
}
기본 프로젝션 값이 null
이 아닌 경우, 값은 래퍼 유형의 현재 표현을 사용하여 반환된다. 백킹값(backing value)이 null
의 경우, getter 메소드는 사용된 래퍼 타입의 빈 상태로 반환한다.
클래스 기반 프로젝션(DTO)
프로젝션을 정의하는 또 다른 방법은 검색할 필드의 속성을 보유하는 값 유형 DTO(데이터 전송 객체)를 사용하는 것이다. 이러한 DTO 유형은 프록시가 발생하지 않고 중첩된 프로젝션을 적용할 수 없다는 점을 제외하면 프로젝션 인터페이스와 정확히 동일한 방식으로 사용할 수 있다.
저장소가 로드하는 필드를 제한하는 것으로 쿼리 실행을 최적화하는 경우, 로드되는 필드는 공개된 생성자의 매개변수 이름에서 결정된다.
다음 예는 프로젝션 DTO를 보여준다.
예 73: 프로젝션 DTO
class NamesOnly {
private final String firstname, lastname;
NamesOnly(String firstname, String lastname) {
this.firstname = firstname;
this.lastname = lastname;
}
String getFirstname() {
return this.firstname;
}
String getLastname() {
return this.lastname;
}
// equals(...) and hashCode() implementations
}
Tip
프로젝션 DTO의 정형 코드 피하기
@Value
어노테이션을 제공하는 프로젝트 Lombok을 사용하면, DTO 코드를 극적으로 단순화 할 수 있다(이전 인터페이스 예제에서 설명한 Spring의 @Value
어노테이션과 혼동하지 말자). Project Lombok의 @Value
어노테이션을 사용하는 경우는 앞의 샘플 DTO는 다음과 같다.
@Value
class NamesOnly {
String firstname, lastname;
}
Field는 기본적으로 private final
이고, 클래스가 모든 필드를 가져와서 equals(...)
및 hashCode()
메소드를 자동으로 구현하는 생성자를 공개한다.
동적 프로젝션
지금까지 컬렉션의 반환 유형 또는 요소 유형으로 프로젝션 유형을 사용했다. 단, 호출할 때에 사용할 유형을 선택할 수도 있다(이를 통해 동적으로 된다). 동적 프로젝션을 적용하려면 다음 예제와 같은 쿼리 메소드를 사용한다.
예 74: 동적 프로젝션 매개변수를 사용하는 리포지토리
interface PersonRepository extends Repository<Person, UUID> {
<T> Flux<T> findByLastname(String lastname, Class<T> type);
}
이 방법을 사용하면 다음 예제와 같이 메소드를 사용하여 그대로 두거나 프로젝션을 적용하여 집계를 얻을 수 있다.
예 75: 동적 프로젝션에 리포지토리 사용
void someMethod(PersonRepository people) {
Flux<Person> aggregates =
people.findByLastname("Matthews", Person.class);
Flux<NamesOnly> aggregates =
people.findByLastname("Matthews", NamesOnly.class);
}
결과 매핑
인터페이스 또는 DTO 프로젝션을 반환하는 쿼리 메소드는 실제 쿼리에서 생성된 결과를 기반으로 한다. 인터페이스 프로젝션은 일반적으로 잠재적인 @Column
유형 매핑을 고려하기 위해 먼저 도메인 유형에 대한 매핑 결과에 의존 하고, 실제 프로젝션 프록시는 잠재적으로 부분적으로 구체화된 엔터티를 사용하여 프로젝션 데이터를 공개한다.
DTO 프로젝션의 결과 매핑은 실제 쿼리 유형에 따라 다르다. 파생 쿼리는 도메인 유형을 사용하여 결과를 매핑하고 Spring Data는 도메인 유형에서 사용 가능한 속성에서만 DTO 인스턴스를 만든다. 도메인 유형에서 사용할 수 없는 DTO 속성 선언은 지원되지 않는다.
문자열 기반 쿼리는 실제 쿼리, 특히 필드 프로젝션과 결과 유형 선언이 밀접하게 관련되어 있기 때문에 서로 다른 접근 방식을 사용한다. @Query
으로 어노테이션이 달린 쿼리 메소드에서 사용되는 DTO 프로젝션은 쿼리 결과를 DTO 유형에 직접 매핑한다. 도메인 유형의 필드 매핑은 고려되지 않는다. DTO 유형을 직접 사용하면 쿼리 메소드는 도메인 모델로 제한되지 않는 보다 동적인 프로젝션의 이점을 얻을 수 있다.
14.3. 엔티티 콜백
Spring Data 인프라는, 특정의 메소드가 불려 가기 전후에 엔티티를 변경하기 위한 훅을 제공한다. 이른바 EntityCallback
인스턴스는 콜백 형식의 엔티티를 확인하고, 잠재적으로 변경하는 편리한 방법을 제공한다.
EntityCallback
는 특화된 ApplicationListener
와 매우 비슷하다. 일부의 Spring Data 모듈은, 특정 엔티티의 변경을 허가하는 저장소 고유의 이벤트(BeforeSaveEvent
등)를 공개한다. 불변의 형태를 조작하는 경우 등, 이러한 이벤트는 문제를 일으킬 가능성이 있다. 또한 이벤트 발행은 ApplicationEventMulticaster
에 따라 다르다. 비동기 TaskExecutor
로 구성하면, 이벤트 처리를 스레드로 분기할 수 있으므로 예측할 수 없는 결과를 초래할 수 있다.
엔티티 콜백은 동기화 포인트와 리액티브 API를 모두 통합 포인트에 제공하여, 처리 체인의 명확하게 정의된 체크 포인트에서 순서 실행을 보장하며 잠재적으로 변경된 엔티티 또는 리액티브 래퍼 유형을 반환한다.
엔티티 콜백은 일반적으로 API 유형으로 분리된다. 이 분리는 동기 API가 동기 엔티티 콜백만을 고려하여 리액티브 구현이 리액티브 엔티티 콜백만을 고려하는 것을 의미한다.
Info
Entity Callback API는 Spring Data Commons 2.2에서 도입되었다. 엔티티 변경을 적용하는 권장 방법이다. 기존의 저장소 고유의ApplicationEvents
는, 등록되어 있을 가능성이 있는 EntityCallback
인스턴스를 호출하기 전에 공개된다.
14.3.1. 엔티티 콜백 구현
EntityCallback
는 제네릭스 유형 인수를 통해 도메인 유형과 직접 연결된다. 각 Spring Data 모듈에는 일반적으로 EntityCallback
엔티티 라이프사이클을 다루는 사전에 정의된 인터페이스 세트와 함께 제공된다.
예 76: EntityCallback 구조
@FunctionalInterface
public interface BeforeSaveCallback<T> extends EntityCallback<T> {
/**
* Entity callback method invoked before a domain object is saved.
* Can return either the same or a modified instance.
*
* @return the domain object to be persisted.
*/
T onBeforeSave(T entity <2>, String collection <3>); // (1)
}
(1) 엔티티가 저장되기 전에 호출되는 BeforeSaveCallback
고유한 메소드. 잠재적으로 변경된 인스턴스를 반환한다.
(2) 영속화하기 직전의 엔티티이다.
(3) 엔터티가 유지되는 컬렉션과 같은 여러 저장소 특정 인수이다.
예 77: 리액티브 EntityCallback 구조
@FunctionalInterface
public interface ReactiveBeforeSaveCallback<T> extends EntityCallback<T> {
/**
* Entity callback method invoked on subscription, before a domain object is saved.
* The returned Publisher can emit either the same or a modified instance.
*
* @return Publisher emitting the domain object to be persisted.
*/
Publisher<T> onBeforeSave(T entity <2>, String collection <3>); // (1)
}
(1) 엔티티가 저장되기 전에 구독 시에 호출되는 BeforeSaveCallback
고유 메소드. 잠재적으로 변경된 인스턴스를 발행한다.
(2) 영속화하기 직전의 엔티티이다.
(3) 엔티티가 영속화되는 컬렉션과 같은 여러 저장소 특정 인수이다.
Info
옵션의 엔티티 콜백 파라미터는 구현 Spring Data 모듈에 의해 정의되어EntityCallback.callback()
의 호출 사이트로부터 추측된다.
다음 예제와 같이 애플리케이션 요구에 맞는 인터페이스를 구현한다.
예 78: 예 BeforeSaveCallback
class DefaultingEntityCallback implements BeforeSaveCallback<Person>, Ordered { // (2)
@Override
public Object onBeforeSave(Person entity, String collection) { // (1)
if(collection == "user") {
return // ...
}
return // ...
}
@Override
public int getOrder() {
return 100; // (2)
}
}
(1) 요구 사항에 따라 콜백 구현. (2) 동일한 도메인 유형의 엔티티 콜백이 여러 개인 경우에 엔티티 콜백을 순서를 정할 수 있다. 순서는 가장 낮은 우선 순위를 따른다.
14.3.2. 엔티티 콜백 등록
EntityCallback
Bean은 ApplicationContext
에 등록되어 있는 경우에 저장소 고유의 구현에 의해 선택된다. 대부분의 템플릿 API는 이미 ApplicationContextAware
가 구현되어 있으므로 ApplicationContext
에 액세스할 수 있다.
다음 예는 유효한 엔티티 콜백 등록 모음을 설명한다.
예 79: EntityCallback Bean 등록 예
@Order(1) // (1)
@Component
class First implements BeforeSaveCallback<Person> {
@Override
public Person onBeforeSave(Person person) {
return // ...
}
}
@Component
class DefaultingEntityCallback implements BeforeSaveCallback<Person>,
Ordered { // (2)
@Override
public Object onBeforeSave(Person entity, String collection) {
// ...
}
@Override
public int getOrder() {
return 100; // (2)
}
}
@Configuration
public class EntityCallbackConfiguration {
@Bean
BeforeSaveCallback<Person> unorderedLambdaReceiverCallback() { // (3)
return (BeforeSaveCallback<Person>) it -> // ...
}
}
@Component
class UserCallbacks implements BeforeConvertCallback<User>,
BeforeSaveCallback<User> { // (4)
@Override
public Person onBeforeConvert(User user) {
return // ...
}
@Override
public Person onBeforeSave(User user) {
return // ...
}
}
(1) BeforeSaveCallback
은 @Order
어노테이션에 선언된 순서대로 수신한다.
(2) BeforeSaveCallback
은 Ordered
인터페이스 구현을 통해 순서대로 수신한다.
(3) 람다 식을 사용한 BeforeSaveCallback
. 기본적으로 순서 지정되지 않고 마지막에 호출된다. 람다 식으로 구현된 콜백은 입력 정보를 공개하지 않으므로 할당할 수 없는 엔터티로 호출하면 콜백 처리량에 영향을 준다. class
또는 enum
를 사용하여, 콜백 Bean 유형 필터링을 사용할 수 았다.
(4) 단일 구현 클래스에 여러 엔티티 콜백 인터페이스를 결합한다.
14.3.3. 특정 EntityCallbacks 저장
Spring Data R2DBC는 검사 지원에 EntityCallback
API를 사용하여 다음 콜백에 응답한다.
표 4: 지원되는 엔티티 콜백
콜백 | 방법 | 설명 | 순서 |
---|---|---|---|
BeforeConvertCallback | onBeforeConvert(T entity, SqlIdentifier table) |
도메인 객체가 OutboundRow 로 변환되기 전에 호출된다. |
Ordered.LOWEST_PRECEDENCE |
AfterConvertCallback | onAfterConvert(T entity, SqlIdentifier table) |
도메인 객체가 로드된 후에 호출된다. 행에서 읽은 후에 도메인 객체를 변경할 수 있다. |
Ordered.LOWEST_PRECEDENCE |
AuditingEntityCallback | onBeforeConvert(T entity, SqlIdentifier table) |
생성 또는 수정은 검사 가능한 엔터티를 표시하는 것이다. | 100 |
BeforeSaveCallback | onBeforeSave(T entity, OutboundRow row, SqlIdentifier table) |
도메인 객체가 저장되기 전에 호출된다. 맵핑된 모든 엔티티 정보를 포함하는 OutboundRow 영속성을 위해 대상을 변경할 수 있다. |
Ordered.LOWEST_PRECEDENCE |
AfterSaveCallback | onAfterSave(T entity, OutboundRow row, SqlIdentifier table) |
도메인 객체가 저장된 후에 호출된다. 도메인 객체를 변경하여 저장 후 반환되도록 할 수 있다. OutboundRow 에는 맵핑된 모든 엔티티 정보가 포함된다. |
Ordered.LOWEST_PRECEDENCE |
14.4. 여러 데이터베이스에서 작업
여러 개의 잠재적으로 다른 데이터베이스로 작업하는 경우에는 응용 프로그램은 구성에 다른 접근 방식을 필요하다. 제공되는 AbstractR2dbcConfiguration
지원 클래스는 Dialect
이 파생된 하나의 ConnectionFactory
을 가정한다. 즉, 여러 데이타베이스에서 동작하도록 Spring Data R2DBC를 구성하려면 몇개의 Bean 를 스스로 정의해야 한다.
R2DBC 리포지토리는 리포지토리를 구현하기 위해 R2dbcEntityOperations
를 필요로 한다. AbstractR2dbcConfiguration
를 사용하지 않고 리포지토리를 스캔하는 간단한 구성은 다음과 같다.
@Configuration
@EnableR2dbcRepositories(basePackages = "com.acme.mysql", entityOperationsRef = "mysqlR2dbcEntityOperations")
static class MySQLConfiguration {
@Bean
@Qualifier("mysql")
public ConnectionFactory mysqlConnectionFactory() {
return ...
}
@Bean
public R2dbcEntityOperations mysqlR2dbcEntityOperations(@Qualifier("mysql") ConnectionFactory connectionFactory) {
DatabaseClient databaseClient = DatabaseClient.create(connectionFactory);
return new R2dbcEntityTemplate(databaseClient, MySqlDialect.INSTANCE);
}
}
@EnableR2dbcRepositories
에서는 databaseClientRef
또는 entityOperationsRef
중에 하나를 통해 구성할 수 있다. 다양한 DatabaseClient
Bean을 사용하면 동일한 유형의 여러 데이터베이스에 연결할 때 유용하다. 서로 다른 다이렉트 데이터베이스 시스템을 사용하는 경우에는 @EnableR2dbcRepositories
(entityOperationsRef = …)`를 대신에 사용하도록 한다.
15. 감사
15.1. 기본
Spring Data는 엔티티를 만들거나 변경한 사용자와 변경이 발생한 시기를 투명하게 추적하기 위한 고급 지원을 제공한다. 이 기능을 활용하려면 어노테이션을 사용하거나 인터페이스를 구현하여 정의할 수 있는 감사 메타데이터를 엔터티 클래스에 설치해야 한다. 또한 필요한 인프라 구성 요소를 등록하려면 어노테이션 구성 또는 XML 구성을 통해 감사를 활성화해야 한다. 구성 샘플에 대해서는 저장소별 섹션을 참조하여라.
Info
작성일과 변경일만 추적하는 애플리케이션에서는AuditorAware
지정할 필요는 없다.
15.1.1. 어노테이션 기반 감사 메타데이터
엔티티를 만들거나 수정한 사용자 기록하려면 @CreatedBy
와 @LastModifiedBy
, 변경이 발생하였을 때 기록하려면 @CreatedDate
와 @LastModifiedDate
를 제공한다.
예 80: 감사 대상 엔티티
class Customer {
@CreatedBy
private User user;
@CreatedDate
private Instant createdDate;
// ... further properties omitted
}
보시다시피, 어떤 정보를 기록하는지에 따라 어노테이션을 선택적으로 적용할 수 있다. 변경이 수행되었을 때에 기록하는 어노테이션은 타입 Joda-Time, DateTime
, 레거시 Java Date
및 Calendar
, JDK8의 일자와 시각 타입와 long
또는 Long
의 프로퍼티로 사용할 수 있다.
감사 메타데이터는 반드시 루트 레벨 엔터티에 존재할 필요는 없지만, 아래에 표시된 대로 포함된 엔터티에 추가할 수 있다(실제로 사용되는 저장소에 따라 다름).
예 81: 포함된 엔터티의 메타데이터 감사
class Customer {
private AuditMetadata auditingMetadata;
// ... further properties omitted
}
class AuditMetadata {
@CreatedBy
private User user;
@CreatedDate
private Instant createdDate;
}
15.1.2. 인터페이스 기반 감사 메타데이터
어노테이션을 사용하여 감사 메타데이터를 정의하지 않으려면 도메인 클래스에 Auditable
인터페이스를 구현할 수 있다. 모든 감사 속성의 setter 메소드를 공개한다.
15.1.3. AuditorAware
@CreatedBy
또는 @LastModifiedBy
를 사용하는 경우는 감사 인프라는 어떤 식으로든 현재 보안 주체를 인식해야 한다. 이를 위해 현재 사용자 또는 애플리케이션과 상호 작용하는 시스템이 누구인지를 인프라에 알리기 위해 구현해야 하는 AuditorAware<T>
SPI 인터페이스를 제공한다. 제네릭 클래스 T
는 @CreatedBy
또는 @LastModifiedBy
어노테이션이 붙은 속성 유형을 정의한다.
다음 예제는 스프링 시큐리티의 Authentication
객체를 사용하여 인터페이스를 구현을 보여준다.
예 82: Spring Security 기반 AuditorAware
구현
class SpringSecurityAuditorAware implements AuditorAware<User> {
@Override
public Optional<User> getCurrentAuditor() {
return Optional.ofNullable(SecurityContextHolder.getContext())
.map(SecurityContext::getAuthentication)
.filter(Authentication::isAuthenticated)
.map(Authentication::getPrincipal)
.map(User.class::cast);
}
}
구현은 Spring Security에서 제공하는 Authentication
객체에 액세스하여 UserDetailsService
구현으로 작성한 커스텀 UserDetails
인스턴스를 검색한다. 여기에서는 UserDetails
구현을 통해 도메인 사용자를 공개하고 있지만, 발견된 Authentication
에 따라 어디서나 검색할 수 있다고 가정한다.
15.1.4. ReactiveAuditorAware
리액티브 인프라를 사용하는 경우는 컨텍스트 정보를 활용하여 @CreatedBy
또는 @LastModifiedBy
정보를 제공할 수 있다. 애플리케이션과 상호 작용하는 현재 사용자 또는 시스템이 누구인지를 인프라에 알리기 위해 구현해야 하는 ReactiveAuditorAware<T>
SPI 인터페이스를 제공한다. 제네릭 형식 T
은 @CreatedBy
또는 @LastModifiedBy
어노테이션이 달린 속성이 어떤 형식이어야 하는지를 정의한다.
다음의 예는 리액티브 Spring Security의 Authentication
객체를 사용하는 인터페이스의 구현을 나타내고 있다.
예 83: Spring Security 기반 ReactiveAuditorAware
구현
class SpringSecurityAuditorAware implements ReactiveAuditorAware<User> {
@Override
public Mono<User> getCurrentAuditor() {
return ReactiveSecurityContextHolder.getContext()
.map(SecurityContext::getAuthentication)
.filter(Authentication::isAuthenticated)
.map(Authentication::getPrincipal)
.map(User.class::cast);
}
}
구현은 Spring Security가 제공하는 Authentication
객체에 액세스하여, UserDetailsService
구현으로 작성한 사용자 정의 UserDetails
인스턴스를 검색한다. 여기에서는 UserDetails
구현을 통해 도메인 사용자를 공개하고 있지만 발견된 Authentication
에 따라 어디서나 검색할 수 있다고 가정한다.
15.2. R2DBC의 일반적인 감사 구성
Spring Data R2DBC 1.2부터 다음 예제와 같이 구성 클래스에 @EnableR2dbcAuditing
어노테이션을 달아 감사를 활성화 할 수 있다.
예 84: JavaConfig를 사용하여 감사 활성화
@Configuration
@EnableR2dbcAuditing
class Config {
@Bean
public ReactiveAuditorAware<AuditableUser> myAuditorProvider() {
return new AuditorAwareImpl();
}
}
유형 ReactiveAuditorAware
의 Bean을 ApplicationContext
에 공개하면 감사 인프라는 자동으로 이를 가져오고, 이를 사용하여 도메인 유형으로 설정할 현재 사용자를 결정한다. ApplicationContext
에 복수의 구현이 등록되어 있는 경우는 @EnableR2dbcAuditing
의 auditorAwareRef
속성을 명시적으로 설정하는 것으로 사용할 구현을 선택할 수 있다.
16. 매핑
MappingR2dbcConverter
에서는 풍부한 매핑 지원을 제공한다. MappingR2dbcConverter
는 도메인 객체를 데이터 행에 매핑할 수 있는 풍부한 메타데이터 모델이 있다. 매핑 메타데이터 모델은 도메인 객체의 어노테이션을 사용하여 만들어 진다. 다만 인프라는 메타데이터 정보의 유일한 소스로 어노테이션을 사용하는 것에 제한되지 않는다. MappingR2dbcConverter
에는 다음 일련의 규칙에 따라 추가 메타데이터를 제공하지 않고 객체를 행에 매핑할 수 있다.
이 섹션에서는 객체를 행에 매핑하는 규칙을 사용하는 방법과 어노테이션 기반 매핑 메타데이터에서 이러한 규칙을 재정의하는 방법과 MappingR2dbcConverter
의 기능에 대해 설명한다.
16.1. 객체 매핑의 기초
이 섹션에서는 Spring Data 객체 매핑, 객체 생성, 필드 및 속성 액세스, 가변성 및 불변성의 기초에 대해 설명한다. 이 섹션은 기본이 되는 데이터 스토어(JPA 등)의 오브젝트 매핑을 사용하지 않는 Spring Data 모듈에게만 적용되는 것에 주의하자. 또한 인덱스, 컬럼 이름 및 필드 이름의 사용자 정의와 같이 저장소별 오브젝트 맵핑에 대해서는 저장소 특정 섹션을 참조하여라.
Spring Data 객체 매핑의 중심적인 역할은 도메인 객체의 인스턴스를 작성하여 스토어 네이티브 데이터 구조를 그것들에 매핑 하는 것이다. 즉, 두 가지 기본 단계가 필요하다.
- 공개된 생성자 중에 하나를 사용하여 인스턴스 생성.
- 공개된 모든 속성을 구체화하는 인스턴스 설정.
16.1.1. 객체 생성
Spring Data는 그 형태의 오브젝트의 구체화에 사용되는 영속 엔티티의 생성자를 자동적으로 검출하려고 한다. 해결 알고리즘은 다음과 같이 동작한다.
- 생성자가 1개 밖에 없는 경우는 그 생성자가 사용된다.
- 생성자가 여러 개 있고 그 중 하나만
@PersistenceConstructor
어노테이션이 달린 경우는 그 생성자가 사용된다. - 인수가 없는 생성자가 있는 경우는 그 생성자가 사용된다. 다른 생성자는 무시된다.
값의 결정은 생성자의 인수 이름이 엔티티의 속성 이름과 일치한다고 가정한다. 즉, 매핑의 모든 커스터마이즈(다른 데이터스토어 열 또는 필드명 등)를 포함한 속성이 설정되는 것처럼 결정된다. 또한 클래스 파일에서 사용할 수 있는 매개변수 이름 정보 또는 생성자에 존재하는 @ConstructorProperties
어노테이션이 필요하다.
값의 결정은은 스토어 고유의 SpEL 식을 사용한 Spring Framework의 @Value
값 어노테이션을 사용해 커스터마이즈 할 수 있다. 자세한 내용은 저장소별 맵핑 섹션을 참조하여라.
##### 객체 생성에 대해 자세히 알아보기 리플렉션의 오버헤드를 피하기 위해 Spring Data 객체의 생성은 기본적으로 런타임에 생성된 팩토리 클래스를 사용하며 도메인 클래스 생성자를 직접 호출한다. 즉, 이 예제의 유형: ```java class Person { Person(String firstname, String lastname) { ... } } ``` 런타임 시에 이것과 의미적으로 동등한 팩토리 클래스를 생성한다. ```java class PersonObjectInstantiator implements ObjectInstantiator { Object newInstance(Object... args) { return new Person((String) args[0], (String) args[1]); } } ``` 이렇게하면 리플렉션보다 약 10% 성능이 향상된다. 도메인 클래스가 이러한 최적화의 대상이 되기 위해서는 일련의 제약을 따를 필요가 있다. - private 클래스가 아니어야 한다. - 비정적(non-static) 내부 클래스(inner class)가 아니어야 한다. - CGLib 프록시 클래스가 아니어야 한다. - Spring Data에서 사용되는 생성자는 private이 아니어야 한다. 이러한 조건 중 하나라도 일치하게 되면 Spring Data는 리플렉션을 통해 엔터티 인스턴스화로 대체된다.
16.1.2. 속성 설정
엔티티의 인스턴스가 생성되면 Spring Data는 해당 클래스의 나머지 모든 영구 속성을 설정한다. 엔티티의 생성자에 의해 이미 입력되어 있지 않은 경우(즉, 생성자 인수 목록을 통해 사용), ID property가 최초로 입력되어 순환 오브젝트 참조의 해결이 가능하게 된다. 그런 다음 생성자에 의해 아직 설정되지 않은 모든 비일시적 속성이 엔터티 인스턴스로 설정된다. 이를 위해 다음과 같은 알고리즘을 사용한다.
- 속성이 불변인
with...
메소드를 공개하는 경우(아래 참조),with...
메소드를 사용하여 새 속성 값을 갖는 새로운 엔터티 인스턴스를 만든다. - 속성의 접근(즉, getter 및 setter를 통한 액세스)가 정의되고 있는 경우, setter 메소드를 호출하고 있다.
- 속성을 변경할 수 있는 경우는 필드를 직접 설정한다.
- 속성이 불변의 경우는 영속화 조작(객체 생성을 참조)로 사용되는 생성자를 사용하여 인스턴스의 복사본을 생성한다.
- 기본적으로 필드 값을 직접 설정한다.
##### 속성 설정에 대해 자세히 알아보기 [객체 생성 최적화](##객체-생성에-대해-자세히-알아보기)와 마찬가지로 Spring Data 런타임 생성되는 접근자 클래스(accessor classes)를 사용하여 엔터티 인스턴스와 상호 작용한다. ```java class Person { private final Long id; private String firstname; private @AccessType(Type.PROPERTY) String lastname; Person() { this.id = null; } Person(Long id, String firstname, String lastname) { // Field assignments } Person withId(Long id) { return new Person(id, this.firstname, this.lastame); } void setLastname(String lastname) { this.lastname = lastname; } } ``` **예 85: 생성된 속성 접근자** ```java class PersonPropertyAccessor implements PersistentPropertyAccessor { private static final MethodHandle firstname; // (2) private Person person; // (1) public void setProperty(PersistentProperty property, Object value) { String name = property.getName(); if ("firstname".equals(name)) { firstname.invoke(person, (String) value); // (2) } else if ("id".equals(name)) { this.person = person.withId((Long) value); // (3) } else if ("lastname".equals(name)) { this.person.setLastname((String) value); // (4) } } } ``` (1) PropertyAccessor는 기본이 되는 오브젝트의 가변 인스턴스를 보관 유지한다. 이렇게 하지 않으면 불변 속성을 변경할 수 있기 때문이다. (2) 기본적으로 Spring Data는 필드 액세스를 사용하여 속성 값을 읽거나 쓸수 있다. `private` 필드의 가시성 규칙에 따라 `MethodHandles` 필드와 상호 작용하는데 사용된다. (3) 클래스는 식별자의 설정에 사용되는 `withId(...)` 메소드를 공개한다. 인스턴스가 데이터 스토어에 삽입되어 식별자가 생성되었을 때에 `withId(...)`를 호출하면, 새로운 `Person` 객체가 생성된다. 이후에 모든 변경은 새로운 인스턴스에서 수행되며, 이전 인스턴스는 변경되지 않는다. (4) property-access를 사용하면, `MethodHandles`를 사용하지 않고 직접 메소드를 호출할 수가 있다. 이렇게 하면 리플렉션보다 약 25% 성능이 향상된다. 도메인 클래스가 이러한 최적화의 대상이 되기 위해서는 일련의 제약을 따를 필요가 있다. - 유형은 디폴트 또는 `java` 패키지에 존재해서는 안된다. - 형식과 그 생성자는 `public`이여야 한다. - 내부(inner) 클래스인 형태는 `static`이여야 한다. - 사용되는 Java 런타임은 원래 `ClassLoader` 클래스를 선언할 수 있도록 해야 한다. Java 9 이후에는 특정의 제한이 있다. 기본적으로 Spring Data는 생성된 속성 접근자를 사용하려고 하고, 제한이 감지되면 리플렉션 기반의 접근자로 폴백(falls back)한다.
다음 엔티티를 살펴보자.
예 86: 샘플 엔티티
class Person {
private final @Id Long id; // (1)
private final String firstname, lastname; // (2)
private final LocalDate birthday;
private final int age; // (3)
private String comment; // (4)
private @AccessType(Type.PROPERTY) String remarks; // (5)
static Person of(String firstname, String lastname, LocalDate birthday) { // (6)
return new Person(null, firstname, lastname, birthday,
Period.between(birthday, LocalDate.now()).getYears());
}
Person(Long id, String firstname, String lastname, LocalDate birthday, int age) { // (6)
this.id = id;
this.firstname = firstname;
this.lastname = lastname;
this.birthday = birthday;
this.age = age;
}
Person withId(Long id) { // (1)
return new Person(id, this.firstname, this.lastname, this.birthday, this.age);
}
void setRemarks(String remarks) { // (5)
this.remarks = remarks;
}
}
(1) 식별자 속성은 final
이지만, 생성자로 null
이 설정된다. 클래스는 식별자의 설정에 사용되는 withId(...)
메소드를 공개한다. 인스턴스가 데이터 스토어에 삽입되어 식별자가 생성되었을 때. 원래 Person
인스턴스는 새로운 인스턴스가 생성될 때 변경되지 않는다. 일반적으로 저장소 관리되는 다른 속성에도 동일한 패턴이 적용되지만 영속화 작업을 위해 변경해야 할 수도 있다. 영속화 생성자(6을 참조)는 사실상 본사본 생성자이며, 속성의 설정은 새로운 식별자 값이 적용된 새로운 인스턴스의 생성으로 변환되기 때문에, wither 메소드는 옵션이다.
(2) firstname
와 lastname
속성은 getter를 개입시켜 잠재적으로 공개되는 통상의 불변의 속성이다.
(3) age
속성은 불변이지만, birthday
속성에서 파생된다. 표시된 설계에서 Spring Data는 선언된 유일한 생성자를 사용하기 때문에 데이터베이스 값은 기본 설정보다 우선한다. 계산이 선호되는 경우에도 이 생성자는 매개 변수로 age
받는 것이 중요하다(무시될 수 있음). 그렇지 않으면 속성 생성 단계가 age 필드를 설정하려고 하고, 불변으로 with...
메소드가 있다.
(4) comment
속성은 가변적이며, 필드를 직접 설정하는 것으로 입력된다.
(5) remarks
속성은 가변적이며, comment
필드를 직접 설정하거나 setter 메소드를 호출하여 설정한다.
(6) 이 클래스는 오브젝트 생성 위한 팩토리 메소드와 생성자를 을 공개한다. 여기에서 핵심이 되는 아이디어는 추가 생성자 대신 팩토리 메소드를 사용하여 @PersistenceConstructor
생성자를 명확히 할 필요성을 피하는 것이다. 대신 속성의 기본 설정은 팩토리 메소드 내에서 처리된다.
16.1.3. 일반적인 권장 사항
- 불변 객체를 고집 — 불변 객체는 생성자만 호출하여 객체를 구체화하므로 쉽게 생성할 수 있다. 또, 이것에 의해, 클라이언트 오브젝트가 오브젝트의 상태를 조작할 수 있도록 하는 setter 메소드가 도메인 오브젝트에 흩어지는 것을 방지한다. 필요한 경우는 패키지가 동일한 위치에 배치된 제한된 유형에서만 호출할 수 있도록 패키지를 보호하는 것이 좋다. 생성자 전용 실체화는 속성 설정보다 최대 30% 빠르다.
- all-args 생성자 제공 — 엔터티를 불변의 값으로 모델링할 수 없거나, 원하지 않는 경우에도 객체 매핑이 속성 설정을 건너뛸 수 있으므로 엔터티의 모든 속성을 인수로 사용한다. 제공하는 것은 가치가 있다. 최적의 성능을 위해.
@PersistenceConstructor
을 회피하기 위해 오버로드 된 생성자 대신 팩토리 메소드를 사용한다. — 최적의 성능에 필요한 모든 인수 생성자는 일반적으로 자동 생성 식별자 등을 생략한 애플리케이션 사용 사례별 생성자를 공개한다. 이러한 all-args 생성자의 변형을 공개하는 정적 팩토리 메소드.- 생성된 인스턴스 생성 클래스와 프로퍼티 액세서 클래스를 사용할 수 있도록 하는 제약을 반드시 지켜야 한다.
- 생성되는 식별자에 대해서는 모든 인수의 영속화 생성자(권장) 또는
with...
메소드와 조합된final
필드를 사용한다. - Lombok을 사용하여 보일러 플레이트 코드를 피한다. — 영속화 작업은 일반적으로 모든 인수를 취하는 생성자를 필요로하므로 선언은 필드 할당에 대한 보일러 플레이트 매개 변수의 지루한 반복이지만 Lombok의
@AllArgsConstructor
을 사용하면 피할 수 있다.
속성 재정의
Java를 사용하면 도메인 클래스를 유연하게 설계할 수 있다. 이 경우에 서브 클래스는 슈퍼 클래스로 같은 이름으로 벌써 선언되고 있는 property를 정의할 수 있다. 다음 예제를 보도록 하자.
public class SuperType {
private CharSequence field;
public SuperType(CharSequence field) {
this.field = field;
}
public CharSequence getField() {
return this.field;
}
public void setField(CharSequence field) {
this.field = field;
}
}
public class SubType extends SuperType {
private String field;
public SubType(String field) {
super(field);
this.field = field;
}
@Override
public String getField() {
return this.field;
}
public void setField(String field) {
this.field = field;
// optional
super.setField(field);
}
}
두 클래스 모두 할당 가능한 유형을 사용하여 field
를 정의한다. 그러나 SubType
은 SuperType.field
을 쉐도우(shadow)한다. 클래스 설계에 따라서는 생성자를 사용하는 것은 SuperType.field
을 설정을 위한 유일한 기본 접근 방식일 수 있다. 또는 setter인 super.setField(...)
를 호출하여 SuperType
으로 field
설정할 수 있다. 속성은 동일한 이름을 공유하지만, 두 가지 다른 값을 나타낼 수 있으므로 이러한 메커니즘은 어느 정도 충돌을 일으킬 수 있다. Spring Data는 형태가 할당 가능하지 않은 경우, 수퍼 유형의 속성을 스킵 한다. 즉, 재정의된 속성의 유형은 재정의로 등록된 수퍼 유형의 속성 유형에 할당할 수 있어야 한다. 그렇지 않은 경우, 슈퍼 타입의 속성는 일시적인 것으로 간주된다. 일반적으로 개별 속성 이름을 사용하는 것이 좋다.
Spring Data 모듈은 일반적으로 다른 값을 유지하는 재정의된 속성을 지원한다. 프로그래밍 모델의 관점에서 고려해야 할 몇 가지 사항이 있다.
- 어떤 속성을 영속화해야 하나(기본적으로 선언된 모든 속성이 된다)? 이는
@Transient
어노테이션을 달면 속성을 제외할 수 있다. - 데이터 스토어의 속성을 나타내는 방법은? 다른 값에 대해 동일한 필드/열 이름을 사용하면, 일반적으로 데이터가 손상되므로 명시적 필드/열 이름을 사용하여 속성 중 하나 이상에 어노테이션을 달아야 한다.
- 슈퍼 프로퍼티는 일반적으로 setter의 구현을 한층 더 상정하지 않고 설정할 수 없기 때문에,
@AccessType(PROPERTY)
의 사용은 사용할 수 없다.
16.1.4. Kotlin 지원
Spring Data는 Kotlin 사양을 준수하여 객체를 만들고 수정할 수 있다.
Kotlin 객체 만들기
Kotlin 클래스는 인스턴스화가 지원되고 있어 모든 클래스는 디폴트가 불변이며, 가변 속성를 정의하려면 명시적인 프로퍼티 선언이 필요하다. 다음의 data
클래스 Person
를 보도록 하자.
data class Person(val id: String, val name: String)
위의 클래스는 명시적인 생성자를 가진 일반적인 클래스로 컴파일된다. 다른 생성자를 추가하여 이 클래스를 사용자 지정한 다음 @PersistenceConstructor
에 어노테이션을 달아 생성자 설정을 보여준다.
data class Person(var id: String, val name: String) {
@PersistenceConstructor
constructor(id: String) : this(id, "unknown")
}
Kotlin은 매개변수가 제공되지 않을 때 기본값을 사용할 수 있도록 하여 매개변수 옵션을 지원한다. Spring Data가 파라미터의 디폴트 설정을 가지는 생성자를 검출했을 경우, 데이터 스토어가 값을 제공하지 않는 (또는 단순히 null를 반환하는) 경우. Kotlin는 파라미터의 디폴트 설정을 적용할 수 있기 때문에, 이러한 파라미터는 존재하지 않는다. name
매개 변수의 기본 설정을 적용하는 다음 클래스를 보도록 하자.
data class Person(var id: String, val name: String = "unknown")
name
매개 변수 중 하나 결과의 일부가 아니거나 해당 값이 null
일 경우에는 name
은 "unknown"
으로 기본 설정이 된다.
Kotlin 데이터 클래스의 속성 설정
Kotlin 에서는 모든 클래스는 디폴트로 불변이며, 가변 속성를 정의하려면 명시적인 속성 선언이 필요하다. 다음의 data
클래스 Person
를 보도록 하자.
data class Person(val id: String, val name: String)
이 클래스는 사실상 불변이다. Kotlin이 기존 객체의 모든 속성 값을 복사하여 메소드에 인수로 제공된 속성 값을 적용하는 새로운 객체 인스턴스를 만드는 copy(...)
메소드를 생성할 때 새 인스턴스를 만들 수 있다.
Kotlin 재정의 속성
Kotlin에서 속성 재정의를 선언하여, 하위 클래스의 속성을 변경할 수 있다.
open class SuperType(open var field: Int)
class SubType(override var field: Int = 1) :
SuperType(field) {
}
이러한 처리 방식은 field
라는 이름의 2개 속성이 렌더링 된다. Kotlin은 각 클래스의 각 속성에 대한 속성 액세서(getter 및 setter)를 생성한다. 사실상 코드는 다음과 같다.
public class SuperType {
private int field;
public SuperType(int field) {
this.field = field;
}
public int getField() {
return this.field;
}
public void setField(int field) {
this.field = field;
}
}
public final class SubType extends SuperType {
private int field;
public SubType(int field) {
super(field);
this.field = field;
}
public int getField() {
return this.field;
}
public void setField(int field) {
this.field = field;
}
}
SubType
의 Getter와 setter는 SubType.field
만을 설정하여, SuperType.field
는 설정하지 않는다. 이러한 처리 방식에서는 생성자를 사용하는 것이 SuperType.field
를 설정하는 유일한 기본 방법이다. SubType
에 메소드를 추가하여 this.SuperType.field = ...
를 통해 SuperType.field
을 설정할 수 있지만, 지원되는 규칙의 범위를 벗어난다. 속성은 동일한 이름을 공유하지만 두 가지 다른 값을 나타낼 수 있으므로 속성 재정의로 인해 어느 정도 충돌이 발생한다. 일반적으로 개별 속성 이름을 사용하는 것이 좋다.
Spring Data 모듈은 일반적으로 다른 값을 유지하는 재정의된 속성을 지원한다. 프로그래밍 모델의 관점에서 고려해야 할 몇 가지 사항이 있다.
- 어떤 속성을 영속화해야 하나(기본적으로 선언된 모든 속성이 된다)? 이들에
@Transient
어노테이션을 달면 속성을 제외할 수 있다. - 데이터 스토어의 속성을 나타내는 방법은? 다른 값에 대해 동일한 필드/열 이름을 사용하면 일반적으로 데이터가 손상되므로 명시적 필드/열 이름을 사용하여 속성 중 하나 이상에 어노테이션을 달아야 한다.
- 슈퍼 속성을 설정할 수 없기 때문에
@AccessType(PROPERTY)
은 사용할 수 없다.
16.2. 약관 기반 매핑
MappingR2dbcConverter
에는 추가 매핑 메타데이터가 제공되지 않은 경우에 객체를 행에 매핑하는 몇 가지 규칙이 있다. 규칙은 다음과 같다.
- 짧은 Java 클래스명은, 다음의 방법으로 테이블명에 맵 된다.
com.bigbank.SavingsAccount
클래스는SAVINGS_ACCOUNT
테이블 이름에 매핑된다. 동일한 이름의 매핑이 필드를 열 이름에 매핑하는데 적용된다. 예:firstName
필드는FIRST_NAME
열에 맵핑된다. 사용자 지정NamingStrategy
을 제공하여 이 매핑을 제어할 수 있다. 자세한 내용은 “매핑 설정"을 참조한다. 속성 이름이나 클래스 이름에서 파생된 테이블 이름과 열 이름은 기본적으로 따옴표 없이 SQL 문에서 사용된다.R2dbcMappingContext.setForceQuote(true)
를 설정하여 이 동작을 제어할 수 있다. - 중첩된 객체는 지원되지 않는다.
- 컨버터는 등록되어 있는 Spring 컨버터를 사용하여, 객체 속성의 디폴트의 매핑을 행의 열과 값에 덮어쓴다.
- 오브젝트의 필드는 행의 열과의 변환에 사용된다. public
JavaBean
속성은 사용되지 않는다. - 생성자 인수 이름이 행의 최상위 컬럼 이름과 일치하는 단일 0이 아닌 인수 생성자가 있는 경우는 해당 생성자가 사용된다. 그렇지 않으면, 인수가 없는 생성자가 사용된다. 인수가 0이 아닌 생성자가 복수인 경우, 예외가 슬로우 된다.
16.3. 매핑 설정
기본적으로 (명시적으로 설정되지 않는 한) DatabaseClient
을 생성하면 MappingR2dbcConverter
인스턴스가 만들어 진다. MappingR2dbcConverter
자신의 인스턴스를 만들 수 있다. 독자적인 인스턴스를 작성하는 것으로, Spring 컨버터를 등록하여 데이타베이스와의 사이에 특정의 클래스를 매핑 할 수 있다.
Java 기반 메타데이터를 사용하여 MappingR2dbcConverter
, DatabaseClient
, ConnectionFactory
을 구성할 수 있다. 다음 예제에서는 Spring의 Java 기반 구성을 사용한다.
R2dbcMappingContext to
의 setForceQuote
를 true
로 설정하면, 클래스 및 속성으로부터 파생한 테이블명과 열명이 데이타베이스 고유의 인용부호와 함께 사용된다. 이는 이러한 이름에 예약된 SQL 단어(예: 정렬)를 사용해도 문제가 없음을 의미한다. 이렇게 하려면 AbstractR2dbcConfiguration
의 r2dbcMappingContext(Optional<NamingStrategy>)
를 재정의한다. Spring Data는 그러한 이름의 대문자 소문자를 인용부호가 사용되어 있지 않은 경우에 구성 끝난 데이타베이스에서도 사용되는 형식으로 변환한다. 이름에 키워드나 특수 문자를 사용하지 않는 한, 테이블을 만들 때 따옴표로 묶지 않은 이름을 사용할 수 있다. SQL 표준을 준수하는 데이터베이스의 경우, 이는 이름이 대문자로 변환됨을 의미한다. 인용 문자와 이름의 대문자화의 방법은 사용되는 Dialect
에 의해 제어된다. 사용자 정의 다이렉트를 구성하는 방법은 “R2DBC 드라이버"를 참조한다.
예 87: R2DBC 매핑 지원을 구성하는 @Configuration 클래스
@Configuration
public class MyAppConfig extends AbstractR2dbcConfiguration {
public ConnectionFactory connectionFactory() {
return ConnectionFactories.get("r2dbc:...");
}
// the following are optional
@Override
protected List<Object> getCustomConverters() {
List<Converter<?, ?>> converterList = new ArrayList<Converter<?, ?>>();
converterList.add(new org.springframework.data.r2dbc.test.PersonReadConverter());
converterList.add(new org.springframework.data.r2dbc.test.PersonWriteConverter());
return converterList;
}
}
AbstractR2dbcConfiguration
에는 ConnectionFactory
를 정의하는 메소드를 구현할 필요가 있다.
r2dbcCustomConversions
메소드를 재정의하는 것으로 컨버터에 컨버터를 추가할 수 있다.
사용자 정의 NamingStrategy
는 Bean으로 등록하여 구성할 수 있다. NamingStrategy
는 클래스와 속성의 이름을 테이블과 열의 이름으로 변환하는 방법을 제어한다.
Info
AbstractR2dbcConfiguration
는 DatabaseClient
인스턴스를 만들고, databaseClient
라는 이름으로 컨테이너에 등록한다.
16.4. 메타데이터 기반 매핑
Spring Data R2DBC 지원 내부의 객체 매핑 기능을 최대한으로 활용하려면 패핑된 객체에 @Table
어노테이션을 붙여야 한다. 매핑 프레임워크에 이 어노테이션을 붙일 필요는 없지만(어노테이션이 없어도 POJO는 올바르게 매핑된다), 클래스 경로 스캐너로 도메인 오브젝트를 찾아 전처리하여 필요한 메타데이터를 추출할 수 있다. 이 어노테이션을 사용하지 않는 경우에는 매핑 프레임워크는 도메인 객체의 속성과 메소드를 인식할 수 있도록 내부 메타데이터 모델을 구축해야 하므로 도메인 객체를 처음 저장할 때 응용 프로그램의 성능이 약간 떨어진다. 영속화한다. 다음 예는 도메인 객체를 보여준다.
예 88: 도메인 객체의 예
package com.mycompany.domain;
@Table
public class Person {
@Id
private Long id;
private Integer ssn;
private String firstName;
private String lastName;
}
Info
@Id
어노테이션은 어떤 속성을 기본 키(primary key)로 사용할 것인지를 매퍼에 알려준다.
16.4.1. 기본 유형 매핑
다음 표에서는 엔터티의 속성 유형이 매핑에 어떤 영향을 주는지 설명한다.
소스 | 유형 | 타겟 유형 | 댓글 |
---|---|---|---|
Primitive 유형과 wrapper 유형 | 패스스루(Passthru) | “명시적인 컨버터"를 사용하여 사용자 정의할 수 있다. | |
JSR-310 날짜/시간형 | 패스스루(Passthru) | “명시적인 컨버터"를 사용하여 사용자 정의할 수 있다. | |
String , BigInteger , BigDecimal , UUID |
패스스루(Passthru) | “명시적인 변환기"를 사용하여 사용자 정의할 수 있다. | |
Enum |
String |
“명시적인 컨버터"를 등록하여 사용자 정의할 수 있다. | |
Blob , Clob |
패스스루(Passthru) | 명시적인 변환기 를 사용하여 사용자 정의할 수 있다. | |
byte[] , ByteBuffer |
패스스루(Passthru) | 바이너리 페이로드로 간주된다. | |
Collection<T> |
T 의 배열 |
구성된 “드라이버"에서 지원되는 경우는 배열 유형으로의 변환, 그렇지 않으면 지원되지 않는다. | |
Primitive 유형, wrapper 유형, String 배열 |
래퍼형 배열(예: int[] → Integer[] ) |
구성된 드라이버 에서 지원되는 경우는 배열 유형으로의 변환, 그렇지 않으면 지원되지 않는다. | |
드라이버 특정 유형 | 패스스루(Passthru) | 사용된 R2dbcDialect 로부터 심플 타입으로서 컨트리뷰트(Contributed). |
|
복잡한 객체 | 대상 유형은 등록 Converter 에 따라 다르다. |
“명시적인 컨버터"가 필요한다. 그렇지 않으면 지원되지 않는다. |
Info
열의 네이티브 데이터 형식은 R2DBC 드라이버의 형식 매핑에 따라 다르다. 드라이버는 기하학 유형과 같은 추가적인 단순 유형을 제공할 수 있다.16.4.2. 매핑 어노테이션 개요
MappingR2dbcConverter
는 메타데이터를 사용하여 객체의 행에 매핑을 구동할 수 있다. 다음 어노테이션을 사용할 수 있다.
@Id
: 기본 키를 표시하기 위해 필드 레벨에서 적용된다.@Table
: 이 클래스가 데이터베이스에 매핑의 후보인 것을 나타내기 위해서, 클래스 레벨로 적용된다. 데이터베이스가 저장된 테이블의 이름을 지정할 수 있다.@Transient
: 기본적으로 모든 필드가 행에 매핑된다. 이 어노테이션은 적용되는 필드를 데이터베이스에 저장하는 것에서 제외된다. 컨버터는 생성자 인수의 값을 구체화할 수 없으므로 영속적 생성자 내에서 임시 속성을 사용할 수 없다.@PersistenceConstructor
: 지정된 생성자 매핑한다 — 패키지에서 보호된 것 — 데이터베이스에서 객체를 인스턴스화할 때 사용한다. 생성자의 인수는 이름에 의해 취득한 행의 값에 매핑된다.@Value
: 이 어노테이션은 Spring Framework의 일부이다. 매핑 프레임워크 내에서 생성자 인수에 적용할 수 있다. 이렇게 하면 Spring 표현식 언어 문을 사용하여 도메인 객체를 만드는데 사용되기 전에 데이터베이스에서 검색된 키 값을 변환할 수 있다. 특정 행의 열을 참조하려면 다음과 같은 표현식을 사용해야 한다. :@Value("#root.myProperty")
여기에서 root는 지정된Row
루트를 가리킨다.@Column
: 필드 레벨에서 적용되어 행에 표시되는 열의 이름을 기술하여 클래스의 필드명과는 다른 이름으로 한다.@Column
어노테이션에 지정된 이름은 SQL문에서 사용되는 경우 항상 따옴표로 묶는다. 대부분의 데이터베이스에서는 이는 이러한 이름으로 대소문자를 구별한다는 것을 의미한다. 또한 이러한 이름에 특수 문자를 사용할 수 있음을 의미한다. 그러나 다른 도구에서 문제가 발생할 수 있으므로 권장하지 않는다.@Version
: 필드 레벨에서 적용되며 낙관적 잠금에 사용되며 저장 조작 변경 사항을 확인한다. 값은null
(일반 유형의 경우는zero
)이며, 엔티티가 신규이기 위한 마커로 간주된다. 최초로 저장되는 값은zero
(일반 유형의 경우는 one)이다. 버전은 업데이트할 때마다 자동으로 증가한다. 자세한 내용은 “낙관적 잠금"을 참조한다.
매핑 메타데이터 인프라는 기술 독립적인 별도의 spring-data-commons
프로젝트에 정의되어 있다. 어노테이션 기반 메타데이터를 지원하기 위해 R2DBC 지원은 특정 서브클래스를 사용한다. 다른 전략을 도입할 수도 있다(수요가 있는 경우).
16.4.3. 커스터마이즈 된 객체 구축
맵핑 서브시스템을 사용하면 생성자에 @PersistenceConstructor
어노테이션을 추가하여 오브젝트의 구성을 사용자 정의할 수 있다. 생성자 매개 변수에 사용되는 값은 다음과 같은 방법으로 확인된다.
- 매개 변수에
@Value
어노테이션이 지정된 경우, 지정된 표현식이 평가되고 결과가 매개변수 값으로 사용된다. - Java 유형에 입력 행의 지정된 필드와 이름이 일치하는 프로퍼티가 있는 경우, 그 프로퍼티 정보를 사용하여 입력 필드 값를 전달하는 적절한 생성자 파라미터를 선택힌다. 이는 매개변수 이름 정보가 Java
.class
파일에 있는 경우에만 작동한다. 이는 디버그 정보를 사용해 소스를 컴파일 하는지, Java8의javac
의-parameters
커멘드 라인 스위치를 사용해 실현할 수 있다. - 그렇지 않은 경우,
MappingException
는 throw 되어 지정된 생성자 파라미터를 바인드 할 수 없었던 것을 나타낸다.
class OrderItem {
private @Id final String id;
private final int quantity;
private final double unitPrice;
OrderItem(String id, int quantity, double unitPrice) {
this.id = id;
this.quantity = quantity;
this.unitPrice = unitPrice;
}
// getters/setters ommitted
}
16.4.4. 명시적인 컨버터를 사용한 매핑 재정의
객체를 보관 및 조회하는 경우, R2dbcConverter
인스턴스를 사용하여 모든 Java 형태로부터 OutboundRow
인스턴스에의 매핑을 처리하면 편리한 일이 자주 있다. 다만 R2dbcConverter
인스턴스에서 대부분의 작업을 수행할 수 있지만 성능을 최적화하기 위해 특정 유형의 변환을 선택적으로 처리할 수 있다.
변환을 직접 선택적으로 처리하려면 하나 이상의 org.springframework.core.convert.converter.Converter
인스턴스를 R2dbcConverter
에 등록한다.
AbstractR2dbcConfiguration
의 r2dbcCustomConversions
메소드를 사용하여, 컨버터를 구성할 수 있다. 이 섹세의 첫번째 예는 Java를 사용하여 구성을 수행하는 방법을 보여준다.
Info
사용자 정의 최상위 엔티티 변환에는 변환에 비대칭 유형이 필요하다. 수신 데이터는 R2DBC의Row
에서 추출된다. 송신 데이터(INSERT/UPDATE 문에서 사용됨)는 OutboundRow
으로 표시되며 나중에 명령문으로 어셈블된다.
Spring 컨버터 구현의 다음의 예는, Row 로부터 Person
POJO 로 변환한다.
@ReadingConverter
public class PersonReadConverter implements Converter<Row, Person> {
public Person convert(Row source) {
Person p = new Person(source.get("id", String.class),source.get("name", String.class));
p.setAge(source.get("age", Integer.class));
return p;
}
}
컨버터는 특정 속성에 적용된다. 컬렉션 속성(예: Collection<Person>
)은 반복되며 요소별로 변환된다. 컬렉션 컨버터(예: Converter<List<Person>>
, OutboundRow
)는 지원되지 않는다.
Info
R2DBC는 박스화된 프리미티브(int.class
가 아니라 Integer.class
)를 사용하여 프리미티브 값을 반환한다.
다음 예제는 Person
에서 OutboundRow
으로 변환한다.
@WritingConverter
public class PersonWriteConverter implements Converter<Person, OutboundRow> {
public OutboundRow convert(Person source) {
OutboundRow row = new OutboundRow();
row.put("id", SettableValue.from(source.getId()));
row.put("name", SettableValue.from(source.getFirstName()));
row.put("age", SettableValue.from(source.getAge()));
return row;
}
}
명시적인 컨버터를 사용한 열거형 매핑 재정의
Postgres와 같은 일부 데이터베이스는 데이터베이스별 열거형 열 유형을 사용하여 열거형 값을 네이티브에 쓸 수 있다. Spring Data는 이식성을 극대화하기 위해 기본적으로 Enum
값을 String
값으로 변환한다. 실제 열거 값을 유지하려면 소스 유형과 대상 유형이 실제 열거 유형을 사용하여 Enum.name()
변환을 사용하지 않도록 하는 @Writing
변환기를 등록하한다. 또한 드라이버가 열거형을 나타내는 방법을 인식할 수 있도록 드라이버 레벨에서 열거형을 구성해야 한다.
다음의 예는, Color 열거치를 네이티브에 읽어내기 위한 관련 컴퍼넌트를 나타내고 있다.
enum Color {
Grey, Blue
}
class ColorConverter extends EnumWriteSupport<Color> {
}
class Product {
@Id long id;
Color color;
// ...
}
17. Kotlin 지원
Kotlin는 JVM(및 기타 플랫폼)을 타겟으로 하는 정적으로 형식화된 언어로, 간결하고 우아한 코드를 작성할 수 있는 동시에, Java로 작성된 기존의 라이브러리와의 뛰어난 상호 운용성을 제공한다.
Spring Data는 Kotlin에 대한 퍼스트 클래스 지원을 제공하며 개발자는 Spring Data가 Kotlin 네이티브 프레임워크인 것처럼 Kotlin 응용 프로그램을 만들 수 있다.
Kotlin에서 Spring 애플리케이션을 빌드하는 가장 쉬운 방법은 Spring Boot와 전용 Kotlin을 활용하는 것이다. 이 포괄적인 자습서 에서는 start.spring.io를 사용하여 Kotlin에서 Spring Boot 애플리케이션을 빌드하는 방법을 설명한다.
17.1. 요구사항
Spring Data는 1.3 Kotlin (영어)을 지원한다. kotlin-stdlib(또는 kotlin-stdlib-jdk8 다음과 같은 변형 중 하나 및 kotlin-reflect 클래스 경로에 있어야 한다. start.spring.io를 통해 Kotlin 프로젝트를 부트 스트랩하는 경우 기본적으로 제공된다.
17.2. null 안전 (nullsafe)
Kotlin의 주요 기능 중 하나는 컴파일 할시에 null
값을 깔끔히 처리하는 null
안전이다. 이는 Optional
래퍼의 비용을 들이지 않아도 null
값 선언과 ‘값 또는 값 없음’ 시멘틱을 표현하여 응용 프로그램의 안전성을 높일 수 있다. (Kotlin에서는 null 허용 값을 가진 함수 구조를 사용할 수 있다. “Kotlin null 안전에 대한 포괄적인 가이드”를 참조하여라.)
Java 에서는 유형 시스템으로 null 안전성을 표현할 수 없지만, Spring Data API에서는 org.springframework.lang
패키지로 선언된 JSR-305 도구 친화적인 어노테이션을 넣었다. 기본적으로 Kotlin에서 사용되는 Java API의 유형은 플랫폼 유형로서 인식된다. JSR-305 어노테이션 및 Spring null 허용 여부 주석에 대한 Kotlin 지원은 컴파일 시간에 null 관련 문제를 처리할 수 있는 이점과 함께 Kotlin 개발자에게 전체 Spring Data API에 대한 null 안전성을 제공한다.
Spring Data 리포지터리에 null 안전성이 어떻게 적용되는가에 대해서는 “리포지터리 메소드의 null 처리”를 참조하여라.
Tip
-Xjsr305
컴파일러 플래그에 다음 옵션을 추가하여 JSR-305 체크를 구성할 수 있다.-Xjsr305={strict|warn|ignore}
Kotlin 버전 1.1+의 경우, 기본 동작은 -Xjsr305=warn
와 동일하다. strict
값은 Spring Data API의 null의 안전성을 고려할 필요가 있다. Spring API에서 추론된 Kotlin 유형. 다만, Spring API 의 null 가능성 선언이 마이너 릴리스간에서도 진화하여 앞으로 더 많은 체크가 추가될 가능성이 있는 것을 알고 사용할 필요가 있다.
Info
제네릭스형의 인수, 가변 인수, 배열 요소의 NULL 가능성은 아직 서포트되고 있지 않습니다만, 향후의 릴리스로 지원될 예정이다.17.3. 객체 매핑
Kotlin 객체를 구체화하는 방법에 대한 자세한 내용은 Kotlin 지원을 참조한다.
17.4. 확장
Kotlin 확장는 기존 클래스를 추가 기능으로 확장하는 기능을 제공한다. Spring Data Kotlin API는 이러한 확장 기능을 사용하여 기존 Spring API에 새로운 Kotlin 고유의 유용한 기능을 추가한다.
Info
Kotlin 확장 기능을 사용하려면 가져오기를 해야 한다. 정적 가져오기와 마찬가지로 대부분의 경우 IDE는 자동으로 가져오기를 제안해야 한다.예를 들어, Kotlin 구체화된 유형 매개변수는 JVM 제네릭스 유형 삭제에 대한 해결 방법을 제공하고 Spring Data는 이 기능을 활용하기 위한 몇 가지 확장 기능을 제공한다. 이를 통해 더 나은 Kotlin API를 사용할 수 있다.
Java 로 SWCharacter
객체 목록를 검색하려면, 일반적으로 다음과 같이 작성한다.
Flux<SWCharacter> characters = client.select().from(SWCharacter.class).fetch().all();
Kotlin 및 Spring Data 확장을 사용하면 대신 다음과 같이 작성할 수 있다.
val characters = client.select().from<SWCharacter>().fetch().all()
// or (both are equivalent)
val characters : Flux<SWCharacter> = client.select().from().fetch().all()
Java와 마찬가지로 Kotlin의 characters
은 엄격하게 형식화되었지만, Kotlin의 정교한 형식 추론으로 구문을 줄일 수 있다.
Spring Data R2DBC는 다음과 같은 확장 기능을 제공한다.
DatabaseClient
그리고Criteria
의 제네릭 지원을 구체화하였다.- DatabaseClient의 코루틴 확장.
17.5. 코루틴
Kotlin 코루틴는 논블록킹 코드를 명령적으로 기술하는 것을 가능하게 하는 경량 thread이다. 언어 측면에서 suspend
함수는 비동기 작업의 추상화를 제공하며, 라이브러리 측면에서는 kotlinx.coroutines
는 async { }
와 같은 함수와 Flow와 같은 유형을 제공한다.
Spring Data 모듈은 다음 범위에서 코루틴에 대한 지원을 제공한다.
- Kotlin 확장으로 Deferred와 [Flow] (https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html) 반환 값을 지원한다.
17.5.1. 종속성
코루틴 지원, kotlinx-coroutines-core
, kotlinx-coroutines-reactive
, kotlinx-coroutines-reactor
종속성은 클래스 경로에 있을 때 활성화된다.
예 89: Maven pom.xml에 추가할 종속성
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-core</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-reactive</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-reactor</artifactId>
</dependency>
Info
지원되는 버전1.3.0
이상.
17.5.2. Reactive는 코루틴으로 어떻게 변환될까?
반환 값의 경우은 Reactive API에서 Coroutines API로의 변환은 다음과 같다.
fun handler(): Mono<Void>
가suspend fun handler()
이 된다.fun handler(): Mono<T>
는Mono
비울 수 있는지 여부에 따라suspend fun handler(): T
또는suspend fun handler(): T?
이 된다. (보다 정적으로 형식화되는 이점이 있다)fun handler(): Flux<T>
는fun handler(): Flow<T>
이 된다.
Flow는 코루틴 세계 Flux
에 해당하며 핫 스트림 또는 콜드 스트림, 유한 스트림 또는 무한 스트림에 적합하지만 주요 차이점은 다음과 같다.
Flow
는 푸시(push) 기반이고,Flux
는 푸시풀 하이브리드(push-pull hybrid)이다.- 백 프레셔는 일시 중단 기능을 통해 구현된다.
Flow
에는 단일 중단collect
메소드만 있고, 운영자는 확장 기능으로 구현된다.- 코루틴 덕분에 연산자는 쉽게 구현할 수 있다.
- 확장을 통해 사용자 정의 연산자를
Flow
에 추가할 수 있다. - 수집 작업이 기능을 중단한다.
map
연산자는 일시 중단 기능 매개 변수를 사용하므로 비동기 조작을 지원한다(flatMap필수 없음).
코루틴과 동시에 코드를 실행하는 방법과 같은 자세한 내용은 Spring, 코루틴 및 Kotlin 플로우로 반응한다. 이 블로그 게시물을 참조하여라.
코루틴과 동시에 코드를 실행하는 방법을 비롯한 자세한 내용은 Spring, Coroutines 및 Kotlin Flow로 반응하기에 대한 이 블로그 게시물을 참조한다.
17.5.3. 리포지토리
코루틴 리포지토리의 예는 다음과 같다
interface CoroutineRepository : CoroutineCrudRepository<User, String> {
suspend fun findOne(id: String): User
fun findByFirstname(firstname: String): Flow<User>
suspend fun findAllByFirstname(id: String): List<User>
}
코루틴 리포지토리는 리액티브 리포지토리에 구축되어 Kotlin의 코루틴을 통한 데이터 액세스의 비 차단성을 공개한다. 코루틴 리포지토리의 메소드는 쿼리 메소드 또는 커스텀 구현의 어느쪽이든에 의해 서포트할 수 있다. 커스텀 구현 메소드를 호출하면, 코루틴의 호출이 실제의 구현 메소드에 전파된다. 커스텀 메소드가 suspend-able
의 경우, 구현 메소드가 Mono
나 Flux
등의 리액티브형을 반환할 필요는 없다.
Info
코루틴 리포지토리는 리포지토리가CoroutineCrudRepository
인터페이스를 상속하는 경우에만 발견된다.