스프링 데이터 R2DBC 공식 레퍼런스를 한글로 번역한 문서입니다.
전체 목차는 여기에 있습니다.
목차
- 16.1. Object Mapping Fundamentals
- 16.2. Convention-based Mapping
- 16.3. Mapping Configuration
- 16.4. Metadata-based Mapping
MappingR2dbcConverter
는 다양한 엔티티 매핑을 제공한다. MappingR2dbcConverter
는 풍부한 메타데이터 모델을 사용해서 도메인 객체를 데이터 row에 매핑한다. 매핑 메타데이터 모델은 도메인 객체에 선언한 어노테이션을 통해 채워진다. 하지만 메타데이터 정보를 어노테이션으로만 확인하는 건 아니다. MappingR2dbcConverter
를 사용하면 컨벤션 셋에 따라, 별도 메타데이터를 제공하지 않고도 객체를 row에 매핑할 수 있다.
이번 섹션에선 MappingR2dbcConverter
기능을 설명하며, 객체를 row에 매핑하기 위한 컨벤션 사용법과,어노테이션 기반 매핑 메타데이터 컨벤션을 재정의하는 방법을 알아본다.
16.1. Object Mapping Fundamentals
이번 섹션은 스프링 데이터의 객체 매핑, 생성, 필드와 프로퍼티 접근, mutability와 immutability의 기초를 다지는 섹션이다. 단, 여기서 설명하는 내용은 데이터 저장소의 객체 매핑을 사용하지 않는 스프링 데이터 모듈에만 해당하는 내용이다 (JPA 등). 인덱스, 컬럼/필드명 커스텀 등과 같은 저장소에 특화된 객체 매핑은 저장소 전용 섹션을 참고해라.
스프링 데이터가 객체를 매핑할 때 담당하는 핵심 역할은 도메인 객체 인스턴스를 생성하고, 여기에 저장소 네이티브 데이터 구조를 매핑하는 일이다. 따라서 두 핵심 단계를 거친다:
- 노출한 생성자 중 하나로 인스턴스 생성하기.
- 인스턴스를 채워 노출된 모든 프로퍼티 구체화하기.
16.1.1. Object creation
스프링 데이터는 persistent 객체 타입을 실체화할 엔티티 생성자를 자동으로 감지한다. 리졸브 알고리즘은 다음과 같이 동작한다:
- 인자가 없는 생성자가 있으면 우선 사용한다. 다른 생성자는 무시한다.
- 인자를 받는 생성자가 하나 있으면 이 생성자를 사용한다.
- 인자를 받는 생성자가 여러 개 있다면, 스프링 데이터가 사용할 생성자에
@PersistenceConstructor
를 선언해야 한다.
생성자가 받는 인자 이름은 엔티티 프로퍼티명과 일치한다고 가정하고 값을 리졸브한다. 즉, 프로퍼티를 채우는 듯이 보인다. 커스텀 매핑(데이터 저장소 컬럼과 필드명이 다른 경우 등)도 마찬가지다. 단, 이때는 클래스 파일에 파라미터 이름 정보가 있거나, 생성자에 @ConstructorProperties
어노테이션이 있어야 한다.
리졸브 방식은 스프링 프레임워크의 @Value
어노테이션을 선언해 저장소 전용 SpEL 표현식으로 커스텀할 수 있다. 자세한 내용은 저장소 전용 매핑 섹션을 참고해라.
Object creation internals
스프링 데이터는 객체를 생성할 때 리플렉션 오버헤드를 피하기 위해 런타임에 디폴트로 생성되는 팩토리 클래스를 사용한다. 팩토리 클래스에선 도메인 클래스의 생성자를 직접 호출한다. 다음 타입으로 예를 들면:
class Person { Person(String firstname, String lastname) { … } }
스프링 데이터는 런타임에 이 생성자와 의미가 동일한 팩토리 클래스를 하나 만든다:
class PersonObjectInstantiator implements ObjectInstantiator { Object newInstance(Object... args) { return new Person((String) args[0], (String) args[1]); } }
이 팩토리 클래스는 리플렉션에 비해 성능이 10% 정도 뛰어나다. 단, 이렇게 도메인 클래스를 최적화하려면 몇 가지 제약 조건을 준수해야 한다:
- private 클래스가 아닐 것
- non-static inner 클래스가 아닐 것
- CGLib 프록시 클래스가 아닐 것
- 스프링 데이터가 사용하는 생성자는 private이 아닐 것
이 기준에 해당되는 게 하나라도 있으면 스프링 데이터는 다시 리플렉션으로 엔티티 인스턴스를 만든다.
16.1.2. Property population
엔티티 인스턴스를 만들고나면 스프링 데이터는 클래스에 남은 모든 persistent 프로퍼티에 값을 채운다. 엔티티 생성자로 값을 채우지 않았다면 (생성자 인자 리스트로 컨슘하는 등), 순환 객체 참조를 리졸브할 수 있도록 식별자 프로퍼티를 먼저 채운다. 그 후에 아직 생성자로 채워지지 않은 모든 non-transient 프로퍼티를 엔티티 인스턴스에 설정한다. 이 땐 아래와 같은 알고리즘을 사용한다:
- 프로퍼티가 불변(immutable)이지만
with…
메소드를 노출하고 있으면 (아래 참고),with…
메소드를 사용해 프로퍼티를 채운 새 엔티티 인스턴스를 만든다. - 프로퍼티 접근을 정의했다면(i.e. getter와 setter 접근), setter 메소드를 실행한다.
- 프로퍼티를 바꿀 수 있으면(mutable) 직접 필드를 설정한다.
- 프로퍼티가 불변(immutable)이면 persistence 연산(객체 생성 참조)에 사용하는 생성자로 인스턴스 복사본을 만든다.
- 기본 동작은 필드 값을 직접 설정하는 거다.
Property population internals
객체 생성 최적화와 유사하게, 스프링 데이터는 런타임에 접근자 클래스를 생성한다. 엔티티와의 상호 작용은 이 접근자 클래스를 통해 이루어진다.
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; } }
Example 84. A generated Property Accessor
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
는 변경할 수 있는(mutable) 엔티티 객체 인스턴스를 하나 가지고 있다. 덕분에 불변(immutable) 프로퍼티를 수정할 수 있다.
(2) 스프링 데이터는 기본적으로 필드 접근을 통해 프로퍼티 값을 읽고 수정한다.private
필드는 직접 접근할 수 없기 때문에MethodHandles
로 필드와 상호 작용한다.
(3) 이 클래스는 식별자 설정을 위한withId(…)
메소드를 노출하고 있다. 예를 들어 데이터 저장소에 인스턴스를 삽입해 식별자를 생성했을 때 식별자를 설정한다.withId(…)
를 호출하면 새Person
객체를 만든다. 이후에 일어나는 모든 수정 작업은 새 인스턴스에서 일어나며, 이전 인스턴스는 손대지 않는다.
(4) 프로퍼티 접근을 사용하기 때문에MethodHandles
를 이용하지 않고 바로 메소드를 실행한다.접근자 클래스를 활용하면 리플렉션에 비해 성능이 25% 정도 좋아진다. 단, 이렇게 도메인 클래스를 최적화려면 몇 가지 제약 조건을 준수해야 한다:
- 타입이 디폴트 패키지나
java
패키지에 있으면 안 된다.- 타입과 생성자는
public
이어야 한다.- inner 클래스라면
static
이어야 한다.- 기존
ClassLoader
에서 클래스를 선언할 수 있는 자바 런타임을 사용해야 한다. 자바 9 이상엔 제약이 있다.스프링 데이터는 디폴트로 프로퍼티 접근자를 만들어 사용하며, 제약 사항을 하나라도 발견하면 리플렉션으로 폴백한다.
아래 엔티티를 살펴보자:
Example 85. A sample entity
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
인스턴스는 변경하지 않는다. 저장소가 관리하는 다른 프로퍼티에도 같은 패턴을 적용하지만, persistence 연산에 따라 바뀔 수 있다. wither 메소드는 필수는 아닌데, persistence 생성자(6번 참고)를 사실상 복사를 위해 사용하게 되며, 프로퍼티를 세팅하면 새 식별자를 적용한 인스턴스를 새로 만든다.
(2) firstname
, lastname
프로퍼티는 getter를 통해 노출할 수 있는 평범한 불변(immutable) 프로퍼티다.
(3) age
프로퍼티는 불변(immutable)이지만 birthday
프로퍼티에서 파생된다. 스프링 데이터는 선언한 생성자만 사용하기 때문에 이 모델에선 데이터베이스에 있는 값이 디폴트보다 우선시된다. 직접 계산하는걸 의도했더라도, 생성자가 age
파라미터도 받아야 (나중에 무시하더라도) 값을 채울 때 실패하지 않는다. 그러지 않으면 따로 age 필드를 설정해야 하는데, age 필드는 불변(immutable)이면서 with…
메소드도 없기 때문이다.
(4) comment
프로퍼티는 변경할 수 있기 때문에(mutable), 필드를 직접 설정해서 값을 채운다.
(5) remarks
프로퍼티는 변경할 수 있기 때문에(mutable), 필드를 직접 설정하거나 setter 메소드를 실행한다.
(6) 이 클래스는 객체 생성을 위한 팩토리 메소드와 생성자를 하나씩 노출하고 있다. 여기서 핵심 아이디어는, 생성자를 추가하면 @PersistenceConstructor
를 선언해야 하므로, 대신에 팩토리 메소드를 사용하는 거다. 프로퍼티 디폴트 값 세팅은 대신 팩토리 메소드 내에서 처리한다.
16.1.3. General recommendations
- 되도록이면 불면(immutable) 객체를 사용해라 — 불변 객체는 생성자만 호출하면 객체를 구체화할 수 있어서, 객체 생성이 직관적이다. 게다가 클라이언트 코드가 객체 상태를 조작 할 수 있는 setter 메소드로 도메인 객체를 어지르는 일을 방지할 수 있다. setter 메소드가 필요하다면 동일한 패키지 내에서만 제한적으로 호출할 수 있도록 보호하는게 좋다. 생성자로만 구체화하면 프로퍼티 population 보다 최대 30% 빠르다.
- 모든 인자를 다 받는 생성자를 만들어라 — 엔티티를 불변으로 모델링할 수 없거나 원치 않는 경우라고 해도, mutable까지 포함한 엔티티의 모든 프로퍼티를 받는 생성자를 만드는 게 좋다. 객체를 매핑할 때 프로퍼티 population을 건너뛰어 성능을 최적화할 수 있기 때문이다.
- 생성자를 오버로드하고
@PersistenceConstructor
를 선언하는 대신에 팩토리 메소드를 사용해라 — 최적의 성능을 위해 필요한 all-arguments 생성자와는 별도로, 어플리케이션 유스 케이스에 따라 자동 생성 식별자 등 일부를 생략한 전용 생성자를 노출하기도 한다. 하지만 별도 생성자를 노출하는 것보단 확실히 자리잡은 스태틱 팩토리 메소드 패턴을 사용하는 게 좋다. - 팩토리 메소드(instantiator)와 프로퍼티 접근자 클래스를 사용할 수 있게 제약 조건을 준수해라
- 식별자를 생성하려면 final을 유지하고, all-arguments persistence 생성자나(더 선호)
with…
메소드를 함께 사용해라 - 보일러플레이트 코드가 싫다면 롬복을 사용해라 — persistence 연산은 보통 모든 인자를 받는 생성자가 필요하므로, 보일러 플레이트 파라미터를 반복해서 일일이 필드를 할당하지 않으려면 롬복의
@AllArgsConstructor
가 제격이다.
16.1.4. Kotlin support
스프링 데이터는 코틀린 자체 문법도 수용해서 객체를 만들고 수정한다.
Kotlin object creation
코틀린 클래스로도 인스턴스를 만들 수 있다. 모든 클래스는 기본적으로 변경이 불가능하며(immutable), 변경 가능한 프로퍼티를 정의하려면 프로퍼티 선언에 명시해야 한다. 아래 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")
}
코틀린에선 원한다면 파라미터를 제공하지 않았을 때 사용할 기본 값을 지정할 수 있다. 스프링 데이터는 생성자에서 파라미터 디폴트 값을 발견하면, 데이터 저장소가 제공하지 않은 값은(단순히 null
을 반환했거나) 코틀린이 파라미터 기본값을 적용하도록 파라미터 값이 없는 채로 나둔다. name
에 파라미터 기본값을 적용하는 아래 클래스를 살펴보자:
data class Person(var id: String, val name: String = "unknown")
name
파라미터가 결과에 없거나 값 자체가 null
이었다면, name
은 무조건 unknown
으로 맞춰진다.
Property population of Kotlin data classes
코틀린에선 모든 클래스가 기본적으로 변경이 불가능하며(immutable), 변경 가능한 프로퍼티를 정의하려면 프로퍼티 선언에 명시해야 한다. 아래 data
클래스 Person
을 살펴보자:
data class Person(val id: String, val name: String)
이 클래스는 사실상 불변이다. 하지만 코틀린이 copy(…)
메소드를 생성하기 때문에 새 인스턴스를 만들 수 있다. copy(…)
메소드는 기존 객체의 모든 프로퍼티 값을 복사해 메소드 파라미터로 넘기고, 객체 인스턴스를 새로 만든다.
16.2. Convention-based Mapping
별도 매핑 메타데이터가 없을 때 MappingR2dbcConverter
는 몇 가지 컨벤션에 따라 객체를 row에 매핑한다. 이 컨벤션은 다음과 같다:
- 테이블 명은 자바 클래스명을 축약해 매핑한다. 예를 들어
com.bigbank.SavingsAccount
클래스는savings_account
라는 테이블명에 매핑한다. - 중첩 객체는 지원하지 않는다.
- 스프링 컨버터를 등록하면 객체 프로퍼티를 row 컬럼과 값에 매핑하는 기본 방식을 재정의할 수 있다.
- 객체의 필드를 사용해서 row의 컬럼 값을 상호 변환한다. Public
JavaBean
프로퍼티는 사용하지 않는다. - 생성자 인자명이 row의 최상위 컬럼명과 일치하는 단일 생성자가 있으면 이 생성자를 사용한다. 없으면 인자가 없는 생성자를 사용한다. 인자를 받는 생성자가 둘 이상이면 예외를 던진다.
16.3. Mapping Configuration
기본적으로 (직접 설정을 명시하지 않으면) DatabaseClient
를 생성할 때 MappingR2dbcConverter
인스턴스도 만들어진다. 원한다면 자체 MappingR2dbcConverter
인스턴스도 생성할 수 있다. 자체 인스턴스를 만들면 데이터베이스와 특정 클래스 간 상호 매핑에 활용할 스프링 컨버터를 등록할 수 있다.
MappingR2dbcConverter
도 DatabaseClient
나 ConnectionFactory
처럼 자바 기반 메타데이터로 설정할 수 있다. 다음 예시는 스프링의 자바 설정을 사용한다:
Example 86. @Configuration class to configure R2DBC mapping support
@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
메소드를 재정의해서 MappingR2dbcConverter
에 별도 컨버터를 더 추가할 수 있다.
AbstractR2dbcConfiguration
은DatabaseClient
인스턴스를 만들어databaseClient
라는 이름으로 컨테이너에 등록한다.
16.4. Metadata-based Mapping
스프링 데이터 R2DBC 내부에서 지원하는 객체 매핑 기능을 최대한 활용하려면, 매핑할 객체에 @Table
어노테이션을 달아야 한다. 매핑 프레임워크에서 이 어노테이션이 필요한 건 아니지만 (어노테이션이 없어도 POJO를 매핑할 수 있음), 어노테이션 있으면 클래스패스 스캐너가 도메인 객체를 찾아 필요한 메타데이터를 미리 추출할 수 있다. 어노테이션을 선언하지 않으면 도메인 객체를 처음 저장할 때 어플리케이션 성능이 약간 저하된다. 매핑 프레임워크가 도메인 객체 프로퍼티를 알아내고 저장 방법을 결정하기 위해 내부 메타데이터 모델을 만들기 때문이다. 다음은 도메인 객체 예시다:
Example 87. Example domain object
package com.mycompany.domain;
@Table
public class Person {
@Id
private Long id;
private Integer ssn;
private String firstName;
private String lastName;
}
매퍼는
@Id
어노테이션을 보고 기본 키로 사용할 프로퍼티를 알 수 있다.
16.4.1. Default Type Mapping
아래 테이블은 엔티티의 프로퍼티 타입에 따른 매핑 정보를 담고 있다:
Source Type | Target Type | Remarks |
---|---|---|
원시 타입과 래퍼 타입 | 그대로 | Explicit Converters로 커스텀할 수 있음. |
JSR-310 Date/Time 타입 | 그대로 | Explicit Converters로 커스텀할 수 있음. |
String , BigInteger , BigDecimal , UUID |
그대로 | Explicit Converters로 커스텀할 수 있음. |
Enum |
String | Explicit Converters를 등록해 커스텀할 수 있음. |
Blob , Clob |
그대로 | Explicit Converters로 커스텀할 수 있음. |
byte[] , ByteBuffer |
그대로 | 바이너리 페이로드로 간주함. |
Collection<T> |
T 타입의 배열 |
설정한 드라이버가 지원하는 타입이면 배열로 변환. 그 외는 지원하지 않음. |
원시타입, 래퍼타입, String 의 배열 |
래퍼 타입의 배열 (e.g. int[] → Integer[] ) |
설정한 드라이버가 지원하는 타입이면 배열로 변환. 그 외는 지원하지 않음. |
드라이버 전용 타입 | 그대로 | 사용하는 R2dbcDialect 가 심플 타입으로 지원. |
복잡한 객체 | 등록한 Converter 에 따라 다름. |
Explicit Converters가 없으면 지원하지 않음 |
컬럼의 네티이브 데이터 타입은 R2DBC 드라이버 타입 매핑에 따라 다르다. 드라이버는 Geometry 타입같은 별도 심플 타입을 지원할 수 있다.
16.4.2. Mapping Annotation Overview
MappingR2dbcConverter
는 메타데이터를 사용해서 객체를 row에 매핑한다. 다음과 같은 어노테이션을 지원한다:
@Id
: 기본 키로 마킹할 필드 레벨에 사용.@Table
: 클래스 레벨에 사용. 데이터베이스에 매핑할 후보 클래스를 알려준다. 데이터베이스에 저장할 테이블 이름을 지정할 수 있다.@Transient
: 기본적으로 모든 필드를 row에 매핑하는데, 이 어노테이션을 적용한 필드는 제외하고 저장한다. 컨버터는 transient 프로퍼티 값을 결정할 수 없기 때문에, persistence 생성자 인자엔 transient 프로퍼티를 사용하면 안 된다.@PersistenceConstructor
: 데이터베이스에서 가져온 객체를 초기화할 때 사용할 생성자를 마킹한다. 패키지 보호를 받는 생성자도 마찬가지다. 생성자 인자 이름에 따라 조회한 row 값을 매핑한다.@Value
: 스프링 프레임워크의 어노테이션으로, 매핑 프레임워크에선 생성자 인자에 적용할 수 있다. 스프링 표현식 언어를 사용해서 도메인 객체를 만들기 전에 데이터베이스에서 조회한 키 값을 변환할 수 있다. 조회한 row의 컬럼을 참조하려면 다음과 같은 표현식을 사용해야 한다:@Value("#root.myProperty")
. 여기서 root는 가져온Row
의 루트를 나타낸다.@Column
: 컬럼명을 지정할 필드 레벨에 사용. 덕분에 컬럼명과 클래스의 필드 이름이 달라도 된다.@Version
: 필드 레벨에 사용. 낙관적 잠금에서 저장 연산을 실행할 때 수정 여부를 확인하는 데 사용한다. 값이null
(원시 타입에선zero
)이라는 것은 새 엔티티라는 마커로 간주한다. 처음 저장하는 값은zero
다(원시 타입은one
). 이 버전은 업데이트할 때마다 자동으로 증가한다. 자세한 내용은 낙관적 잠금을 참고해라.
매핑 메타데이터 관련 기반 코드는 기술에 구애받지 않는 별도 spring-data-commons
프로젝트에 정의돼 있다. R2DBC에선 전용 하위 클래스를 통해 어노테이션 기반 메타데이터를 지원한다. 물론 다른 전략을 사용할 수도 있다 (필요하면).
16.4.3. Customized Object Construction
매핑 시스템은 생성자에 @PersistenceConstructor
어노테이션을 달아 객체 생성을 커스텀할 수 있도록 지원한다. 생성자 파라미터 값은 다음과 같은 방식으로 리졸브한다:
- 파라미터에
@Value
어노테이션이 있으면, 주어신 표현식을 평가해서 그 결과를 파라미터 값으로 사용한다. - 자바 타입에 입력 row의 필드와 이름이 일치하는 프로퍼티가 있으면, 이 프로퍼티 정보를 사용해서 적절한 생성자 파라미터에 입력 필드 값을 전달한다. 이 방식은 자바
.class
파일에 파라미터 이름 정보가 있는 경우에만 동작한다. 클래스 파일에 파라미터 이름 정보를 추가하려면, 컴파일할 때 디버그 정보도 포함시키거나, 자바 8의javac
커맨드라인 플래그-parameters
를 사용하면 된다. - 그 외에는 주어진 생성자 파라미터를 바인딩할 수 없음을 뜻하는
MappingException
을 던진다.
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. Overriding Mapping with Explicit Converters
객체를 저장하고 질의할 땐, 모든 자바 타입을 R2dbcConverter
인스턴스가 OutboundRow
인스턴스로 매핑하도록 두는 게 간편하다. 하지만 R2dbcConverter
인스턴스로 대부분을 처리하더라도, 특정 타입은 직접 변환해야 할 때도 있다. 보통 성능을 최적화해야 할 때가 그렇다.
필요할 때만 직접 변환하려면 R2dbcConverter
에 org.springframework.core.convert.converter.Converter를 한 개 이상 등록해라.
컨버터를 설정할 땐 AbstractR2dbcConfiguration
의 r2dbcCustomConversions
메소드를 사용하면 된다. 자바 설정은 이 챕터 시작 부분에 있는 예제를 참고해라.
최상위 엔티티 변환을 커스텀하려면 양방향 변환을 둘 다 제공해야 한다. 인바운드 데이터는 R2DBC의
Row
에서 추출한다. 아웃바운드 데이터(INSERT
/UPDATE
문에서 사용하는)는OutboundRow
로 표현한 뒤에 SQL 문으로 합쳐진다.
다음 예제는 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;
}
}
컨버터는 단일 프로퍼티에 적용된다는 점에 주목해라. 컬렉션 프로퍼티(e.g. Collection<Person>
)는 순회해서 요소별로 각각 변환한다. 컬렉션 컨버터(e.g. Converter<List<Person>>, OutboundRow
)는 지원하지 않는다.
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;
}
}
Overriding Enum Mapping with Explicit Converters
Postgres 등 일부 데이터베이스는 자체 열거형 컬럼 타입을 활용해 enum 값을 저장할 수 있다. 스프링 데이터는 범용성을 위해 기본적으로 Enum
값을 String
으로 변환한다. 실제 enum 값을 유지하려면, 소스와 타겟 타입에 실제 enum을 사용하는 @Writing
컨버터를 등록하고 Enum.name()
변환을 쓰지 않으면 된다. 추가로, 드라이버 레벨에도 enum 타입을 설정해야 드라이버가 enum 타입을 표현하는 방법을 알 수 있다.
다음 예제는 네티이브로 Color
enum 값을 읽고 쓰는 관련 컴포넌트 보여준다:
enum Color {
Grey, Blue
}
class ColorConverter extends EnumWriteSupport<Color> {
}
class Product {
@Id long id;
Color color;
// …
}
Next :Kotlin Support
스프링 데이터 R2DBC에서 지원하는 코틀린 전용 API(Null-safety, 익스텐션, 코루틴) 소개
전체 목차는 여기에 있습니다.