본문 바로가기

스터디/스프링

Spring Data JDBC - 엔티티 생성편

개요

우아한 테크코스 Lv2를 진행하며 Spring Data JDBC를 사용하게 되었습니다.

Spring Data JDBC는 처음 사용해 보았기 때문에 많은 시행착오를 거쳤습니다.

이 글은 공식문서를 참고하여 작성되었습니다.
문서의 양이 적지는 않아 시리즈로 작성하려 합니다.

한글 문서가 별로 없어서 고생했는데 Spring Data JDBC를 필자처럼 처음 사용하는 분들에게 도움이 되길 바라는 마음으로 작성하였습니다.

만약 더 자세한 내용을 알고 싶으시다면 공식문서를 참고하시면 좋을 것 같습니다.

테스트 환경

데이터 베이스는 메모리 DB인 H2를 사용하여 테스트 하였습니다.
스프링 부트 버전은 2.2.6 RELEASE 입니다.
Spring Data Jdbc 버전은 1.1.7 RELEASE 입니다.

게시글에 사용된 예제코드와 테스트코드는 GitHub에서 확인하실 수 있습니다.

1. 엔티티(Entity) 생성

Spring Data JDBC에서 엔티티 객체를 생성하는 알고리즘은 3가지입니다.

  1. 기본 생성자가 있는 경우 기본생성자를 사용합니다.
    다른 생성자가 존재해도 무시하고 최우선 기본생성자를 사용합니다.
  2. 매개변수가 존재하는 생성자가 하나만 존재한다면 해당 생성자를 사용합니다.
  3. 매개변수가 존재하는 생성자가 여러개 있다면 @PersistenceConstructor 어노테이션이 적용된 생성자를 사용합니다.
    @PersistenceConstructor가 존재하지 않고, 기본 생성자가 없다면 org.springframework.data.mapping.model.MappingInstantiationException이 발생합니다.

여기서 기본 생성자를 private접근 제어자로 선언해도 정상적으로 잘 작동합니다

엔티티 내부 값 주입 과정

Spring Data JDBC는 생성자로 인해 채워지지 않은 필드들에 대해 자동으로 생성된 프로퍼티 접근자(Property Accessor)가 다음과 같은 순서로 멤버변수의 값을 채워넣습니다.

  1. 엔티티의 식별자를 주입합니다.
  2. 엔티티의 식별자를 이용해 참조중인 객체에 대한 값을 주입합니다.
  3. transient 로 선언된 필드가 아닌 멤버변수에 대한 값을 주입합니다.

엔티티 맴버변수는 다음과 같은 방식으로 주입됩니다.

1.멤버변수에 final 예약어가 있다면(즉, 불변이라면) wither 메서드를 이용하여 값을 주입합니다.

   // wither method
   public Sample withId(Long id) {
       //내부적으로 기존 생성자를 이용하며 imuttable한 값을 매개변수로 가진다.
       return new Sample(id, this.sampleName);
   }

해당 wither메서드가 존재하지 않다면 java.lang.UnsupportedOperationException: Cannot set immutable property ... 를 발생시킵니다.

2.해당 멤버변수의 @AccessTypePROPERTY라면 setter를 사용하여 주입합니다.

  • setter가 존재하지 않으면 java.lang.IllegalArgumentException: No setter available for persistent property이 발생합니다.

3.기본적으로 직접 멤버변수에 주입합니다.(Field 주입)

witherMethod

withMethod를 정의하는 경우는 final예약어가 있는 immutable한 멤버변수가 존재 할 때 입니다.
이때 주의해야할 점이 있습니다.

만약 immutable한 멤버변수가 n개라면 witherMethod또한 n개 작성해 주어야 합니다.
단순히 witherMethod 한개의 매개변수에 여러개의 immutable 필드를 주입한다면 java.lang.UnsupportedOperationException: Cannot set immutable property를 보게 됩니다.

또한 witherMethod의 이름을 잘못 작성해서는 안됩니다.
멤버변수의 이름이 createdAt 이라면 witherMethod의 이름은 멤버변수의 이름을 뒤에 붙힌 withCreatedAt으로 작성해야 합니다.
테이블 컬럼 이름이 아닌 멤버 변수명을 따라서 작성해야합니다.

엔티티 생성 가이드 라인

Spring Data JDBC 문서를 보면 JDBC를 이용해서 만드는 객체들에 대한 몇가지 가이드라인이 있습니다.

가이드 라인은 다음과 같습니다.

  • 불변객체를 만드려고 노력하라.
  • 모든 매개변수를 가진 생성자(All Arguments Contructor)를 제공하라
    • 모든 매개변수를 가진 생성자를 제공하면 object mapping 과정중 Property 주입 방식을 건너뜀으로써 최대 30% 빠른 생성이 가능해진다.
  • 생성자 오버로딩으로 인해 @PersistenceConstructor을 사용하는 것을 피하기 위해 팩토리 매서드 패턴을 사용하라
  • 생성자와 자동 생성된 프로퍼티 접근자(Property Accessor)가 객체를 생성할 수 있도록 규약을 지켜라
  • 자동 생성될 식별자 필드(ex. id)를 final과 함께 사용하기 위해 wither메서드를 사용하라
  • 보일러 플레이트 코드를 피하기 위해 Lombok을 사용하라
    • 예를 들어 모든 매개변수를 가진 생성자의 경우 Lombok@AllArgsConstructor가 좋은 방법이 될 수 있다.

여기까지 복잡하다면 복잡하고 단순하다면 단순한 객체의 생성에 대한 설명입니다.
그렇다면 어떻게 사용하는 것이 단순하고 사용하기 편한방법 일까요?

다음과 같은 Table이 있다고 했을때

CREATE TABLE CHESSGAME
(
    id     BIGINT auto_increment,
    name   varchar(255),
    active bit default 1,
    primary key (id)
);

롬복을 사용하지 않는다면 개인적으로 아래와 같은 방법이 가장 단순하고 사용하기 편한 방법인것 같습니다.

@Table("CHESSGAME")
public class ChessGame {
    @Id
    private Long id;
    private String name;
    private boolean active;

    private ChessGame() {
    }

}

객체 생성을 위한 기본생성자를 선언하고 접근 제어자는 private로 선언해 두고 사용하는 것입니다.

이후 필요에 따라 아래와 같이 생성자를 추가하거나 정적 팩터리 메서드를 생성하여 사용하면 좋을 것 같습니다.

@Table("CHESSGAME")
public class ChessGame {
    @Id
    private Long id;
    private String name;
    private boolean active;

    private ChessGame() {
    }

      //실제 사용될 생성자
    public ChessGame(final String name, final boolean active) {
        this.name = name;
        this.active = active;
    }
}

다만 롬복을 사용한다면 아래와 같이 전체 맴버변수를 가지는 생성자를 항상 최신화 하며 @Builder를 사용하고 @PersistenceConstructor 를 같이 사용하는것이 성능 면에서 좋을 것 같습니다.

@Table("CHESSGAME")
public class ChessGame {
    @Id
    private Long id;
    private String name;
    private boolean active;

      @Builder
      @PersistenceConstructor
    public ChessGame(final Long id, final String name, final boolean active) {
                this.id = id
          this.name = name;
        this.active = active;
    }

    public ChessGame(final String name, final boolean active) {
        this.name = name;
        this.active = active;
    }
}