이번 포스팅에서는 Spring Data의 객체 매핑, 객체 생성, 필드 및 속성 접근, 불변성 등에 대해 다룰 예정이다.
소개되는 내용들은 JPA처럼 관계형 DB 매핑을 사용하지 않는 Redis나 MongoDB 같은 모듈에만 해당된다.
즉, 테이블처럼 정해진 데이터구조와 일대일 매칭되지 않는, NoSQL이나 캐시시스템에 해당되는 내용이다.
Spring Data 객체 매핑의 핵심은 인스턴스를 생성하고, 이를 저장구조에 매핑하는 것이다.
Spring Data는 엔티티의 생성자를 자동으로 감지하려고 아래의 과정을 순서대로 시도한다.
@PersistenceCreator
애노테이션이 설정된 정적팩토리 메서드가 있다면 사용한다.- 단일 생성자가 있다면 사용한다.
- 여러 생성자 중에
@PersistenceCreator
가 설정된 생성자가 있다면 사용한다.- Java의 Record인 경우, 표준 생성자가 사용된다.
- 기본생성자가 있다면 해당 생성자가 사용되고, 다른 생성자는 무시된다.
생성자나 팩토리메서드의 매개변수 이름은 엔티티의 속성이름과 일치한다고 가정되며,
사용자 정의가 사용되는 경우엔 생성자에 @ConstructorProperties
애노테이션이 설정되거나 클래스 파일을 통한 추론이 가능해야한다.
class Person {
Person(String firstname, String lastname) { … }
}
// 런타임 시, 생성되는 팩토리 클래스
class PersonObjectInstantiator implements ObjectInstantiator {
Object newInstance(Object... args) {
return new Person((String) args[0], (String) args[1]);
}
}
Spring Data는 리플렉션으로 인한 오버헤드를 피하기 위해 런타임 시 팩토리 클래스를 생성한다.
생성된 팩토리 클래스는 실제 클래스의 생성자를 직접적으로 호출하기 때문에 클래스 작성에 제약사항이 필요하다.
private
클래스여선 안된다static
이 아닌 내부클래스가 있어서는 안된다- CGLib 프록시 클래스여서는 안된다
- Spring Data가 사용할 생성자는
private
여선 안된다
만약, 위 제약사항 중 하나라도 어긋난다면 리플렉션이 사용되어 성능이 저하된다.
생성자를 통해 인스턴스가 생성되는 과정에서 모든 속성값이 채워지지 않을 수 있다.
이러한 경우, Spring Data에서 해당 값들을 채우려고 시도하며, 아래의 과정이 이뤄진다.
- 불변 속성에 대해
with..
메서드가 있는 경우, 이를 이용하여 새로운 인스턴스를 생성한다.- getter와 setter 접근자가 존재하는 경우, setter 를 실행하여 값을 채운다.
- 가변 속성이라면 직접적으로 값을 채운다.
- 불변 속성인 경우 영속성 작업에 사용되는 생성자를 이용하여 복사본을 생성한다.
- 직접적으로 필드값을 세팅한다.
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;
}
}
class PersonPropertyAccessor implements PersistentPropertyAccessor {
private static final MethodHandle firstname;
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
}
}
}
Spring Data는 런타임 시에 accessor 클래스를 생성하여 엔티티 인스턴스와 상호작용한다.
- Accessor 클래스는 원본 인스턴스를 보유하고 있으며, 기본적으로 필드 접근을 이용해 속성값을 채운다.
- 만약
private
속성인 경우,MethodHandle
( 리플렉션 )을 사용해 필드와 상호작용한다.- 원본 클래스가 식별자 설정에
withId(..)
를 노출하고 있으므로, 이를 사용하여 새로운 인스턴스를 생성할 수 있다.
새로운 인스턴스가 생성된 경우, 이후의 작업에서 새 인스턴스가 사용된다.- setter와 같은 직접적인 접근자가 존재한다면 리플렉션이 아닌, 해당 접근자를 사용한다.
이를통해, 리플렉션에 비해 25% 정도의 성능향상을 기대할 수 있다.
값-속성 매칭에 위 과정을 수행하고 싶다면 아래의 제약사항을 지켜야한다.
- 클래스는 프리미티브 타입이거나 자바 패키지에 포함되지 않아야한다
- 생성자는
public
이어야 한다- 내부 클래스는
static
이어야 한다- 클래스로더에서 클래스를 선언할 수 있어야하므로, Java 9 이상 버전에서는 제한이 있을 수 있다