본문 바로가기

3.구현/Java or Kotlin

[kotlin] JPA 2: Spring Boot에서 Entity 매핑 기본

들어가기

엔티티(Entity)에서 세부적인 정의를 살펴볼려고 한다. 먼저 엔티티 자체 정의하는 방법을 살펴보자. 엔티티와 데이터베이스 테이블과 매핑하는 방법을 위주로 먼저 살펴볼려고 한다.

작성자: http://ospace.tistory.com/ (ospace114@empal.com)

엔티티 정의

엔티티 정의를 위한 기본 어노테이션으로 다음과 같다.

  • @Entity: 엔티티 선언
  • @Table: 매핑할 테이블 선택
  • @Id: 테이블 기본키 사용할 속성 지정
  • @GeneratedVaue: 키값 생성 전략
  • @Column: 매핑할 컬럼 선택

이미 몇 개는 앞에서 보았던 어노테이션도 있다.

@Entity 어노테이션

엔티티를 지정할때 사용하는 어노테이션이다. 데이터베이스 테이블에 매핑할 엔티티를 정의한다. 에티티를 지정할 때 사전 확인사항이 있다.

  • 기본 생성자가 필수이다. 가급적 기본생성자를 정의해놓는게 좋다.
  • final 클래스, enum, interface, inner 클래스에서는 사용할 수 없다.
  • final 속성 필드는 사용할 수 없다.

설정가능한 속성으로 다음과 같다.

  • name: 엔티티 이름올 테이블 이름으로 사용됨

name을 설정하지 않으면 기본으로 클래스명으로 사용된다. 이 속성은 테이블명으로 사용된다. 이 어노테이션이 정의된 클래스는 자동으로 테이블로 매핑되면서 모든 필드가 특별하게 지정하지 않는이상 테이블 컬럼으로 자동 매핑된다. 필드 이름이 그대로 컬럼 이름이 되고 필드 타입이 컬럼 데이터 유형이 된다.

추가로 @Table 어노테이션을 같이 사용할 수 있다. @Table은 데이터베이스 테이블 매핑위한 상세한 설정을 할 수 있다.

@Table 어노테이션

@Table은 엔티티와 테이블 매핑위한 속성을 추가로 설정한다. 이 어노테이션을 사용하지 않으면 @Entity 기준으로 매핑된다. 설정할 수 있는 속성은 다음과 같다.

  • name: 테이블 이름
  • catalog: 데이터베이스 catalog 이름
  • schema: 데이터베이스 schema 이름
  • uniqueConstraints(DDL): 유니크 제약 조건

@Table이 설정되면 @Entity에 name 속성을 무시되고 @Table의 name이 사용된다. 만약 @Table의 name이 비어있으면 @Entity의 name이 적용된다.

uniqueConstraints은 @UniqueConstraint 어노테이션을 사용해서 배열 형태로 표기한다. @UniqueConstraint는 다음과 같은 속성을 설정할 수 있다.

  • name: 제약조건 이름
  • columnNames: 제약조건으로 묶을 컬럼들 이름 배열

실제 사용하는 예는 다음과 같다.

@Entity
@Table(
  name="tb_member",
  uniqueConstraints = [
    UniqueConstraint(name="UNIQUE_NAME", columnNames=["name"])
  ]
)
open class Member {
  //...
}

uniqueConstraints에 의해서 DDL 안에 제약조건이 추가된다.

create table tb_member (
  (중략)
  constraint UNIQUE_NAME unique (name)
}

@Id 어노테이션

엔티티에 식별자를 데이터베이스 테이블에서 기본키에 매핑한다. 식별자를 직접 입력 받아서 사용한다. 만약 식별자 생성 전략을 설정하려면 @GeneratedValue 어노테이션을 같이 사용할수 없다. 만약 사용하지 않는다면 모든 식별자는 수작업으로 관리된다. 새로운 데이터를 추가할 경우도 식별자 값을 설정해줘야 한다.

@Entity
open class Member {
  @Id
  private var id:Long? = null
  //..
}

@GeneratedValue 어노테이션

@GeneratedValue은 식별자 생성 전략을 설정할 수 있다. 설정하는 속성으로 strategy와 generator가 있다. @Id와 같이 사용해 자동으로 식별자를 관리해준다. strategy 속성이 생성 전략으로 열거형 GenerationType으로 설정할 수 있고 설정 가능한 값은 아래와 같다.

  • AUTO: 기본, 데이터베이스마다 기본키 생성 방식이 다르기에 AUTO에 의해 자동으로 선택하게 한다. 오라클, H2인 경우 SEQUENCE, MySql은 IDENTITY를 선택한다.
  • IDENTITY: 데이터베이스에서 기본키를 생성한다. 보통 데이터베이스의 AUTO_INCREMENT를 사용한 생성이다.
  • SEQUENCE: 데이터베이스 시퀀스를 사용해서 기본키를 생성한다. 보통 SEQUENCE라는 오브젝트를 통해 생성한다. 만약 시퀀스를 사용자 정의로 하고 싶다면 @SequenceGenerator으로 생성기를 미리 정의해서 사용할 수 있다.
  • TABLE: 테이블을 사용해서 기본키를 생성한다. 시퀀스를 흉내내는 키 생성 전용 테이블을 사용한다. 이를 사용하기 위한 테이블을 @TableGenerator으로 등록한다.
  • UUID: 버전 JPA 3.1.0 이후에 추가되었다. UUID(Universally Unique IDentifier)는 Java VM에서 생성된다. UUID를 사용하려면 식별자 타입은 문자열로 정의해야 한다.

generator 속성이 이미 정의된 제너레이터를 선택할 수 있다. @SequenceGenerator나 @TableGenerator에 의해 정의한 제너레이터는 GenerationType.SEQUENCE나 GenerationType.TABLE인 경우에 generator 속성에 적용해서 사용할 수 있다.

기본키 생성 전략은 데이터베이스 종류에 따라서 지원이 달라지기 때문에 사용하는 데이터베이스에 맞게 선택해야 한다. 자동으로 할 경우 H2는 SEQUENCE로 되며 직접 INDENTITY로 설정할 수 있다. 이런 부분은 데이터베이스에 따라서 달라 질 수 있다는 의미이다.

@SequenceGenerator 어노테이션

@GeneratedValue에서 사용할 제너레이터를 @SequenceGenerator을 통해 정의할 수 있다. 설정할 수 있는 속성이 아래와 같다.

  • name: 생성기 이름
  • sequenceName: 데이터베이스에 등록된 시퀀스 이름
  • initialValue: 시작값
  • allocationSize: 한번 시퀀스 호출에 증가 값
  • catalog: 데이터베이스 catalog 이름
  • schema: 데이터베이스 schema 이름

실제 데이터베이스에서 실행되는 SQL는 다음과 같은 형식이 된다.


create sequence {catalog}.{schema}.{sequenceName}
start with {initialValue} increment by {allocationSize}

@SequenceGenerator는 엔티티 필드나 엔티티 타입에 정의할 수 있다. 만약 공용으로 사용하려면 엔티티 타임에 정의한다.

@Entity
@SequenceGenerator(
  name="genMember",
  sequenceName="SEQ_MEMBER",
  initialValue=1,
  allocationSize=1,
)
open class Member {
//...
}

그렇지 않고 특정 식별자에서만 사용한다면 @GeneratedValue와 같이 정의할 수 있다.

@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator="genMember")
@SequenceGenerator(name="GEN_MEMBER_ID", sequenceName="SEQ_MEMBER",
  initialValue = 1, allocationSize = 1,
)
private var id:Long? = null

@TableGenerator 어노테이션

GeneratedValue.TABLE을 사용할 경우 키 생성용 테이블을 @TableGenerator으로 등록한다. 설정할 수 있는 속성은 다음과 같다.

  • name: 생성기 이름
  • table: 키 생성용 테이블 이름(기본값 hibernate_sequence)
  • pkColumnName: 기본키로 사용할 식별자용 컬럼 이름(기본값 sequence_name)
  • pkColumnValue: 기본키에 해당하는 식별자 값 저장용 컬럼 이름
  • valueColumnName: 기본키에서 생성된 식별자를 구분할 이름
  • initialValue: 시작값
  • allocationSize: 식별자 한번 할당 후에 증가 값(기본값 50)
  • catalog: 데이터베이스 catalog 이름
  • schema: 데이터베이스 schema 이름
  • uniqueConstraints(DDL): 유니크 제약조건
    위의 속성에 의해 실제 데이터베이스에서 실행되는 SQL는 다음과 같은 형식이 된다. pkColumnName와 pkColumnValue의 이름으로된 테이블 컬럼이 생성된다. 그리고 현재 사용하는 기본키 식별자 이름을 valueColumnName을 사용한다.
create table {catalog}.{schema}.{table} (
  {pkColumnValue} bigint,
  {pkColumnName} varchar(255) not null,
  primary key ({pkColumnName})
)

insert into {catalog}.{schema}.{table}({pkColumnName}, {pkColumnValue})
values ({valueColumnName},{initialValue})

@TableGenerator 없이 사용한다면 기본 DDL은 다음과 같게 된다.

create table hibernate_sequences (
  next_val bigint,
  sequence_name varchar(255) not null,
  primary key (sequence_name)
)

테이블에서 식별자 값을 생성하는 단계는 다음과 같다.

  1. 등록된 키를 조회한다.
  2. 다음 키를 갱신하는 과정을 거친다.

항상 키 생성 테이블에는 다음에 사용할 키 값이 저장되어 있다.

    select tbl.next_val from hibernate_sequences tbl where tbl.sequence_name=? for update
    update hibernate_sequences set next_val=? where next_val=? and sequence_name=?

실제 @TableGenerator을 사용하는 예를 보자.

@Entity
@TableGenerator(
  name="genMember",
  table="GEN_ID",
  pkColumnName="GEN_KEY",
  pkColumnValue="GEN_VALUE",
  valueColumnName="ID_MEMBER",
  initialValue=0,
  allocationSize=1,
)
open class Member {
  @Id
  @GeneratedValue(strategy = GenerationType.TABLE, generator="genMember")
  private var id:Long? = null
  //...
}

@GeneratedValue에 stragegy를 GenerationType.TABLE으로 지정하고 generator에 @TableGenerator에서 만든 이름을 지정한다.

@Column 어노테이션

객체 필드를 테이블 컬럼에 매핑에 대해 설정한다. 다음은 설정할 수 있는 속성이다.

  • name: 매핑할 컬럼 이름
  • insertable: insert시 포함 여부(읽기 전용시 false, 기본: true)
  • updatable: update 허용 여부(읽기 전용시 false, 기본: true)
  • table: 다른 테이블에도 매핑 설정
  • length(DDL): String 문자열 길이 제약(기본: 255)
  • unique(DDL): 유니크 제약 조건(단일 컬럼에 제약조건, 기본: false)
  • nullable(DDL): null 허용 여부(false시 NOT NULL, 기본: true)
  • columnDefinition(DDL): 컬럼 직접 설정
  • precision, scale(DDL): BigDecimal타입의 전체자리수, 소수점 자리수 (기본: 0,0)

만약 @Column을 설정하지 않는다면 기본값이 적용된다. 그리고 컬럼명은 필드명이된다. @Column을 사용하는 간단한 예를 보자.

@Entity
open class Member {
    @Id
    @GeneratedValue
    private var id:Long? = null

    @Column(name="name", nullable=false, unique=true, length=50)
    private var name:String? = null

    @Column(precision=10, scale=0)
    private var point: BigDecimal = BigDecimal.ZERO
//...
}

name이라는 필드에 컬럼 매핑정보를 설정했다. 컬럼명은 “name”이고 널이 되면 않되고, 유니크해야하고, 문자열은 최대 50자까지 제한한다. 실행되는 DDL을 보면 다음과 같다.

create table member (
  point numeric(10,0),
  id bigint not null,
  name varchar(50) not null unique,
  primary key (id)
)

@Column의 정보가 DDL에 영향을 준다. 쿼리를 안다면 columnDefinition를 사용해 직접 지정할 수 있다.

  @Column(columnDefinition="varchar(50) not null unique")
  private var name:String?,

결과적으로는 동일하다. 어노테이션으로 지원하지 않는 상세한 설정도 가능하다. 이는 반대로 특정 데이터베이스에 종속될 수도 있다.

엔티티 정의 개선된 방법

엔티티로 사용하는 클래스는 기본 생성자가 필요하고 기본으로 필드를 접근할 수 있는 getter와 setter가 필요하다. 그러나 실제로 엔티티에 getter와 setter을 추가하는 방법은 권장하는 방법은 아니다. 먼저 간단한 엔티티를 정의해보자.

@Entity
open class Member {
  @Id
  private var id:Long? = null
  private var name:String = ""

  fun getId(): Long? = id
  fun setId(id: Long?) {
    this.id = id
  }
  fun getName(): String? = name
  fun setsName(name: String?) {
    this.name = name
  }
}

보통 정의하는 엔티티 형태이다. 여기서 setter에 의해서 값을 설정하는게 아니라 생성자에서 입력 받도록 하고 싶다.

@Entity
open class Member (
  @Id
  private var id:Long?,
  private var name:String,
) {
  constructor() : this(null, "") {}
  fun getId(): Long? = id
  fun getName(): String? = name
}

생성자가 추가되었기 때문에 자동 생성되었던 기본 생성자가 추가되지 않는다. 그래서 중간에 기본 생성자도 추가해줘야 한다. 이렇게 함으로써 외부에서 내부 필드 접근을 제한할 수 있다.

여기서 추가 작업할 수 있는 부분이 엔티티를 바로 생성할 수 없도록 하는 부분이다. 이때 사용하는 방법이 빌드 패턴을 사용할 수 있다. JPA에서는 기본 생성자가 필수이기 때문에 반드시 필요하지만 기본 생성자가 굳이 public을 할 필요는 없다. 기본 생성자를 protected로 선언하면 외부에서 생성할 수 없도록 제한할 수 있다.추가된 생성자도 외부에서 호출 할 수 없도록 접근 제한할려고 한다.

@Entity
open class Member internal constructor (
  @Id
  private var id:Long?,
  private var name:String,
) {
  companion object {
    fun Builder(): MemberBuilder = MemberBuilder()
  }
  protected constructor() : this(null, "") {}
  fun getId(): Long? = id
  fun getName(): String? = name
}

class MemberBuilder {
  private var id:Long? = null
  private var name:String = ""

  fun setId(id:Long?): MemberBuilder {
    this.id = id
    return this
  }
  fun setName(name:String): MemberBuilder {
    this.name = name
    return this
  }
  fun build(): Member = Member(id, name)
}

이제 Member 인스탄스 생성을 Builder를 통해서 생성할 수 있게 되었다. 그러나 매번 Builder을 정의해서 사용하는게 쉽지 않다. 그렇기 때문에 서비스 계층을 만들어서 엔티티와 같은 모듈로 포함해서 생성할 수 있도록 만들 수 있다. 이렇게 하면 외부에서 엔티티 객체 생성을 제한할 수 있다. 또한, Lombok을 사용하면 getter, setter, builder를 간단하게 추가할 수 있다.

마무리

앞에서 소개한 5가지 어노테이션만으로 기본적인 엔티티에서 데이터베이스 테이블로 매핑이 가능하다. 또한, 엔티티 정의에 대해서도 간단하게 다루었다. 엔티티 정의하는 많은 방법 중에 하나일뿐이다. 이외에도 여러 방법이 있고 각각 장단점이 있다. 간단하게 검색을 해보면 정말 다양하게 나온다. Kotlin이 Java보다 유연한 언어라서 더 좋은 방법이 있을 수 있다.

부족한 글이지만 여러분에게 도움이 되었으면 하네요. 모두 즐거운 코딩생활되세요. ospace.

참고

[1] 최범균, JPA 프로그래밍 입문, 가메출판사, 2017

[2] 김영한, 자바 ORM 표준 JPA 프로그래밍, 에이콘, 2015

[3] Chapter 4. Entity, https://openjpa.apache.org/builds/1.2.3/apache-openjpa/docs/jpa_overview_pc.html, 2024.04.05

반응형