[Spring Data Commons] 1. 개요

2024. 12. 16. 14:48Spring/Spring Docs

반응형

Spring Data Commons 프로젝트는 다양한 관계형 & 비관계형 데이터 저장소에 사용되는 솔루션 개발의 핵심 Spring 개념을 제공합니다.


1. Dependencies

각각의 Spring Data 모듈들의 시작일이 모두 다르기 때문에 대부분 major, minor 버전이 다릅니다.

호환되는 버전을 찾는 가장 쉬운 방법은 Spring Data Release Train을 사용하는 것입니다.

Spring Data Release Train내에는 각 모듈이 통합적으로 호환되기 위한 버전이 정의되어있습니다.


1.1. Spring Data release train BOM

릴리즈 트레인 버전은 calver 패턴 양식인 YYYY.MINOR.MICRO 을 따르며, 현재 날짜 기준으로 2024.1.0 입니다.

※ spring-data-commons의 Release Notes 참고

GA 릴리즈, 서비스 릴리즈는 ${calver} 포맷으로 버전이 정의되고
그 외의 릴리즈들의 경우 suffix로 아래와 같은 modifier를 붙여 ${calver}-${modifier} 포맷을 사용합니다.

  • SNAPSHOT: 현재스냅샷
  • M1, M2, ...: 마일스톤
  • RC1, RC2, ...: 릴리즈 후보

Spring Data examples repository에서 예제코드를 찾아볼 수 있습니다.

현 문서에서 설명하고 있는 Spring Data Common 모듈의 버전은 Spring Framework 6.2.0 이상을 요구합니다.


1.1.1. maven

<properties>
  <spring-data-bom.version>2024.1.0</spring-data-bom.version>
</properties>

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

1.1.2. gradle

ext {
	springDataBomVersion = '2024.1.0'
}

dependencyManagement {
	imports {
		mavenBom "org.springframework.data:spring-data-bom:${springDataBomVersion}"
	}
}

1.2. Spring Boot 종속성 관리

Spring Boot는 최신버전의 Spring Data 모듈을 선택해 줍니다.
만약 새버전으로 업그레이드 하기를 원한다면, spring-data-bom.version 버전을 최신화하면 됩니다.

Spring Docs의 Version Properties 문서를 보면, Spring Boot에 의해 관리되고 있는 라이브러리들의 버전을 override하기위한 버전과 관련된 프로퍼티명을 제공하고 있습니다.
관련 사항이이 궁금하다면 해당 문서를 참고하면 됩니다.


2. Spring Data 업그레이드

Spring Data 버전을 업그레이드하기 위한 방법은 프로젝트의 Wiki 페이지를 참고하면 됩니다.

Wiki의 릴리즈 노트에는 각 버전별로 업그레이드를 위한 지침을 포함하고 있습니다.
만일 릴리즈 노트 업데이트가 여러개가 있다면 과거 릴리즈 노트도 검토한 후 업그레이드 하는 것이 좋습니다.


3. Object Mapping

Spring Data 객체 매핑/생성 과 각 필드 및 프로퍼티에 접근하는 것, 그리고 가변성/불변성에 대한 기본사항을 알아봅시다.

이 포스팅에서는 JPA와 같은 데이터 저장소의 객체 매핑을 사용하지 않는 Spring Data 모듈에 대해 설명합니다.

Spring Data 객체 매핑의 핵심 책임은 도메인 객체의 인스턴스를 생성하고, store-native 데이터 구조를 그 인스턴스에 매핑하는 것으로, 아래 2가지 기본 단계를 필요로 합니다.

  1. 노출 되어있는 생성자를 이용한 객체 생성
  2. 노출된 모든 프로퍼티를 구체화하기 위한 객체 요소

3.1. 객체 생성

Spring Data는 해당 타입의 객체를 구체화하기 위해 사용할 퍼시스턴스 엔티티의 생성자를 감지하기 위해 시도하며, 아래와 같은 조건에 의해 객체를 생성합니다.

  1. 만일 @PersistenceCreator 애너테이션이 설정된 단일 정적 팩토리 메서드가 존재할 경우, 그것을 사용함
  2. 생성자가 단 하나일 경우, 그것을 사용함
  3. 여러개의 생성자가 있으나 @PersistenceCreator 애너테이션이 설정된 생성자가 단 하나일 경우, 그것을 사용함
  4. record class 타입일 경우, 그것을 사용함
  5. NoArgsConstructor(인수 없는 생성자)가 존재할 경우, (다른 생성자는 모두 무시하고) 그것을 사용함

값은 기본적으로 엔티티의 프로퍼티명과 생성자(또는 팩토리 메서드) 인수 이름이 동일하다고 가정하여 해당 속성들을 채워져서 표현됩니다.

생성자(또는 팩토리 메서드)에 설정된 인수값과 엔티티의 프로퍼티명을 달리하면서 값을 표현하고 싶다면 @ConstructorProperties 에 설정하면 되고,
@Value 애너테이션으로 SpEL 표현식을 사용하여 사용자 정의 방식으로 값을 표현할 수도 있습니다.


객체 생성 시 최적화

Spring Data 객체 생성은 reflection 오버헤드를 방지하기 위해 기본적으로 런타임시 생성된 팩토리 클래스를 사용하여 객체 생성을 최적화 합니다.

팩토리 클래스는 도메인 클래스 생성자를 직접 호출합니다.

예를들어, 아래와 같은 클래스가 있다고 할때

class Person {
  Person(String firstname, String lastname) {}
}

런타임 중에 아래와 같은 팩토리 클래스를 생성하여 사용합니다.

class PersonObjectInstantiator implements ObjectInstantiator {

  Object newInstance(Object... args) {
    return new Person((String) args[0], (String) args[1]);
  }
}

팩토리 생성자를 런타임시 생성하여 활용하는 경우, reflection을 이용하는 것보다 10%의 성능 향상이 제공됩니다.

단, 아래의 조건을 충족하는 경우에만 내부적으로 팩토리 메서드를 생성하니, 관련 제약조건에 유의해야 합니다.
(만일, 아래의 제약조건을 만족하지 않을 경우에는 reflection을 이용하여 엔티티를 객체화 합니다.)

  1. private class 가 아니어야 합니다.
  2. non-static inner class 가 아니여야 합니다.
  3. CGLib 프록시 class가 아니어야 합니다.
  4. Spring Data에서 사용할 생성자는 private이면 안됩니다.

3.2. 프로퍼티(속성) 채우기

엔티티 객체가 생성될 때, Spring Data는 클래스의 나머지 모든 영구(persistent) 속성들을 채웁니다.
만일 엔티티의 생성자에 의해 채워지지 않는 경우에는 식별자 속성은 순환 객체 참조를 해결할 수 있도록 먼저 채워집니다.
그 다음 비일시적(non-transient) 속성들이 엔티티 객체에 설정됩니다.

속성을 채우는 과정은 아래의 단계를 따릅니다.

  1. immutable property(변경할수 없는 속성)이면서 공개되어있는 with... 메서드가 있는 경우, with... 메서드를 사용하여 새 속성값을 새 엔티티 인스턴스를 만듭니다.
  2. setter를 이용한 속성 접근이 정의되어있는 경우 setter를 호출합니다.
  3. 변경가능한 속성일 경우 직접 설정
  4. 속성의 변경이 불가한 경우, 영속적 연산에 사용되는 생성자를 사용하여 객체의 복제본을 생성합니다. (3.1. 객체 생성 참고)
  5. 기본적으로 값은 직접 설정 합니다.

내부

객체 생성 최적화와 유사하게, 속성 체우기에서도 Spring Data 런타임시 생성된 접근자 클래스를 사용하여 엔티티 객체와 상호작용을 합니다.

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;                                    

  public void setProperty(PersistentProperty property, Object value) {

    String name = property.getName();

    if ("firstname".equals(name)) {
      firstname.invoke(person, (String) value);             
    } else if ("id".equals(name)) {
      this.person = person.withId((Long) value);            
    } else if ("lastname".equals(name)) {
      this.person.setLastname((String) value);              
    }
  }
}

PropertyAccessor는 (객체의 변경할 수 없는 속성의 변형을 가능하게 하기 위해) 객체의 변경가능한 인스턴스를 보유합니다.

기본적으로 FieldAccess 방식으로 프로퍼티 값을 읽고 쓰며, (= 리플렉션 활용)
명시적으로 @AccessType(Type.PROPETY) 와 같이 접근방식을 설정할 경우 PropertyAccess방식으로 메서드를 호출할 수 있습니다.(= setter)

setter나 with... 메서드를 이용한 방식은 리플렉션을 이용한 프로퍼티 설정보다 25% 의 성능향상이 있습니다.

++ 작동되는 예제 추후 보강될 예정...


3.3. 예시

person:
  id: 123
  first-name: jini
  last-name: yoo
@Getter
@ToString
@ConfigurationProperties(prefix = "person")
public class Person {
    private final Long id;
    private final String firstName;
    private final String lastName;

    public Person(Long id, String firstName, String lastName) {
        this.id = id;
        this.firstName = firstName;
        this.lastName = lastName;
    }

}
728x90
반응형