이번 글에서는 <기본키 매핑, 필드와 컬럼 매핑>에 대해 알아보겠습니다.
이 시리즈 글은 김영한 님의 강의, 을 보고 적은 것임을 알려드립니다. (강추)

기본 키 매핑


JPA가 제공하는 데이터베이스 기본 키 생성 전략은 다음과 같습니다.

  • 직접 할당: 기본 키를 애플리케이션에서 직접 할당, @Id만 사용
  • 자동 생성(GeneratedValue): 대리 키 사용 방식
    • IDENTITY: 기본 키 생성을 데이터베이스에 위임한다, MySQL
    • SEQUENCE: 데이터베이스 시퀀스를 사용해서 기본 키를 할당한다. ORACLE. @SequenceGenerator 필요
    • TABLE: 키 생성 테이블을 사용한다. 모든 DB에서 사용. @TableGenerator 필요
    • AUTO: 방언에 따라 자동 지정, 기본 값

이제 자동 생성하는 방법들에 대해 자세히 알아보자

IDENTITY 전략


IDENTITY는 기본 키 생성을 데이터베이스에 위임하는 전략입니다.
주로 MySQL, PostgreSQL, SQL Server, DB2에서 사용됩니다. IDENTITY 전략은 데이터베이스에 값을 저장하고 나서야 기본 키 값을 구할 수 있을 때 사용합니다.

 // IDENTITY 매핑 코드
@Entity
public class Member{

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
  ...
  }
private static void logic(EntityManager em) {
  Member member = new Member();
  em.persist(member);
  System.out.println("member.id = " + member.getId());
}
// 출력: member.id = 1

위 코드를 처음 보고, "트랜잭션 커밋을 하지 않았는데 실행되나?" 라는 의문을 가졌습니다.
하지만 아래와 같은 이유를 보고 가능하다는 것을 알았습니다.

❗️주의
엔티티가 영속 상태가 되려면 식별자가 반드시 필요합니다. 그런데 IDENTITY 식별자 생성 전략은 엔티티를 데이터베이스에 저장해야 식별자를 구할 수 있으므로 em.persist()를 호출하는 즉시 INSERT SQL이 데이터베이스에 전달됩니다. 따라서 이 전략은 트랜잭션을 지원하는 쓰기 지연이 동작하지 않습니다.

SEQUENCE 전략


데이터베이스 시퀀스는 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트입니다. SEQUENCE 전략은 이 시퀀스를 사용해서 기본 키를 생성합니다. 이 전략은 시퀀스를 지원하는 오라클, PostgreSQL, DB2, H2 데이터베이스에 사용됩니다.

// 시퀀스 DDL
CREATE TABLE MEMBER (
    ID BIGINT NOT NULL PRIMARY KEY,
    DATA VARCHAR(255)
)

// 시퀀스 생성
CREATE SEQUENCE MEMBER_SEQ START WITH 1 INCREMENT BY 1;
// 시퀀스 매핑 코드
@Entity
@SequenceGenerator(
  name = "MEMBER_SEQ_GENERATOR",
  sequenceName = "MEMBER_SEQ", //매핑할 데이터베이스 시퀀스 이름
  initialValue = 1, allocationSize = 1)
public class Member{

  @Id
  @GeneratedValue(strategy = GenerationType.SEQUENCE,
                  generator = "MEMBER_SEQ_GENERATOR")
  private Long id;
  ...
  }
// 시퀀스 사용 코드
private static void logic(EntityManager em) {
  Member member = new Member();
  em.persist(member);
  System.out.println("member.id = " + member.getId());
}
// 출력: member.id = 1

시퀀스 사용 코드는 IDENTITY 전략과 같지만 내부 동작 방식은 다릅니다.
SEQUENCE 전략은 em.persist()를 호출할 때 먼저 데이터베이스 시퀀스를 사용해서 식별자를 조회합니다.
그리고 조회한 식별자를 엔티티에 할당한 후에 엔티티를 영속성 컨텍스트에 저장합니다. 이후 트랜잭션을 커밋해서 플러시가 일어나면 엔티티를 데이터베이스에 저장합니다.
반대로 이전에 설명했던 IDENTITY 전략은 먼저 엔티티를 데이터베이스에 저장한 후에 식별자를 조회해서 엔티티의 식별자에 할당합니다.

@SequenceGenerator 속성

속성 기능 기본값
name 식별자 생성기 이름 필수
ㅤㅤㅤsequenceName 데이터베이스에 등록되어 있는 시퀀스 이름 hibernate_sequence
initialValue DDL 생성 시에만 사용됨. 시퀀스 DDL을 생성할 때 처음 시작하는 수를 지정한다. 1
allocationSize 시퀀스 한 번 호출에 증가하는 수(성능 최적화에 사용) 50
catalog, schema 데이터베이스 catalog, schema 이름

❗️주의: allocationSize의 기본값이 50인 것

TABLE 전략


TABLE 전략은 키 생성 전용 테이블을 하나 만들고 여기에 이름과 값으로 사용할 컬럼을 만들어 데이터베이스 시퀀스를 흉내내는 전략입니다.

  • 장점: 모든 데이터베이스에 적용 가능
  • 단점: 성능이 떨어짐
// TABLE 전략 키 생성 DDL
create table MY_SEQUENCE(
    sequence_name varchar(255) not null ,
    next_val bigint,
    primary key ( sequence_name )
)

sequence_name 컬럼을 시퀀스 이름으로 사용하고 next_val 컬럼을 시퀀스 값으로 사용합니다.

// TABLE 전략 매핑 코드
@Entity
@TableGenerator(
  name = "BOARD_SEQ_GENERATOR",
  table = "MY_SEQUENCES",
  pkColumnValue = "BOARD_SEQ", allocationSize = 1)
public class Member {

  @Id
  @GeneratedValue(strategy = GenerationType.TABLE,
                  generator = "BOARD_SEQ_GENERATOR")
  private Long id;
  ...
  }
// TABLE 전략 매핑 사용 코드
private static void logic(EntityManager em) {
  Member member = new Member();
  em.persist(member);
  System.out.println("member.id = " + member.getId());
}
// 출력: member.id = 1

@TableGenerator 속성

속성 설명 기본값
name 식별자 생성기 이름 필수
table 키생성 테이블명 hibernate_sequences
pkColumnName 시퀀스 컬럼명 sequence_name
valueColumnName 시퀀스 값 컬럼명 next_val
pkColumnValue 키로 사용할 값 이름 엔티티 이름
initialValue 초기 값, 마지막으로 생성된 값이 기준 0
allocationSize 시퀀스 한 번 호출에 증가하는 수 50
catalog, schema 데이터베이스 catalog, schema 이름
uniqueConstraints(DDL) 유니크 제약 조건을 지정할 수 있다

권장하는 식별자 전략


  • 데이터베이스 기본 키 제약 조건: null 아님, 유일, 변하면 안된다
  • 하지만 미래까지 이 조건을 만족하는 자연키는 찾기 어렵다. 대리키(대체키)를 사용하자.
  • 예를 들어 주민등록번호도 기본 키로 적절하지 않다. (회원 테이블의 기본 키로 주민등록번호를 사용하고 있는데, 나라에서 주민등록번호를 보관하지 말라고 한다면 기본키를 바꾸면서 연관된 테이블도 바꿔야하는 참사가 일어날 수 있음)
  • 권장: Long형 + 대체키 + 키 생성전략 사용 (결론적으로, AUTO_INCREMENT나 SEQUENCE 오브젝트 등을 사용하자)

SEQUENCE 전략과 TABLE 전략의 allocationSize 기본값이 50인 이유?


결론은 데이터베이스에 여러번 접근하는 것을 방지해서 성능적으로 장점이 있기 때문입니다.

  • SEQUENCE 전략에서는...
    SEQUENCE 전략은 데이터베이스 시퀀스를 통해 식별자를 조회하는 추가 작업이 필요합니다. 따라서 다음과 같이 데이터베이스와 2번의 통신을 합니다.

    1. 식별자를 구하려고 데이터베이스 시퀀스를 조회한다.
      ex) SELECT MEMBER_SEQ.NEXTVAL ...
    2. 조회한 시퀀스를 기본 값으로 사용해 데이터베이스에 저장한다.
      ex) INSERT INTO MEMBER ...

    위와 같이 시퀀스에 접근하는 것을 줄이기 위해 JPA는 @SequenceGenerator.allocationSize를 사용합니다. 간단히 설명하면, 여기에 설정한 값만큼 한 번에 시퀀스 값을 증가시키고 나서 그만큼 메모리에 시퀀스 값을 할당합니다.
    예를들어, allocation 값이 50이면 시퀀스를 한 번에 50을 증가시킨 다음에 1 ~ 50까지는 메모리에 시퀀스 값을 할당합니다. 그리고 51이 되면 시퀀스 값을 100으로 증가시킨 다음 51 ~ 100까지 메모리에서 식별자를 할당합니다.

    이 최적화 방법은 시퀀스 값을 선점하므로 여러 JVM이 동시에 동작해도 기본 키 값이 충돌하지 않는 장점이 있습니다. 반면에 데이터베이스에 직접 접근해서 데이터를 등록할 때 시퀀스 값이 한 번에 많이 증가한다는 점을 염두해두어야 합니다. 이런 상황이 부담스럽고 INSERT 성능이 중요하지 않으면 allocationSize의 값을 1로 설정하면 됩니다.

  • TABLE 전략에서는...
    TABLE 전략은 값을 조회하면서 SELECT 쿼리를 사용하고 다음 값으로 증가시키기 위해 UPDATE 쿼리를 사용합니다. 이 전략은 SEQUENCE 전략과 비교해서 데이터베이스와 한 번 더 통신하는 단점이 있습니다. 이 전략도 최적화하기 위해 allocationSize를 사용하면 되고, 방법은 SEQUENCE 전략과 같습니다.

필드와 컬럼 매핑


JPA가 제공하는 필드와 컬럼 매핑용 어노테이션들을 레퍼런스 형식으로 보여드리겠습니다.

필드와 컬럼 매핑 분류

분류 매핑 어노테이션 설명
필드와 컬럼 매핑 @Column 컬럼을 매핑
@Enumerated 자바의 enum 타입을 매핑
@Temporal 날짜 타입을 매핑
@Lob BLOB, CLOB 타입을 매핑
@Transient 특정 필드를 데이터베이스에 매핑하지 않음
기타 @Access JPA가 엔티티에 접근하는 방식을 지정

@Column


@Column은 객체 필드를 테이블 컬럼에 매핑합니다. 가장 많이 사용되고 기능도 많습니다. 속성 중에 name, nullabe이 주로 사용되고 나머지는 잘 사용되지 않는 편입니다.
insert, updatable 속성은 데이터베이스에 저장되어 있는 정보를 읽기만 하고 실수로 변경하는 것을 방지하고 싶을 때 사용합니다.

속성 기능 기본값
name 필드와 매핑할 데이틀의 컬럼 이름
insertable 엔티티 저장 시 이 필드도 같이 저장한다. false로 설정하면 이 필드는 데이터베이스에 저장하지 않는다. false 옵션은 읽기 전용일 때 사용한다. true
updatable 엔티티 수정 시 이 필드도 같이 수정한다. false로 설정하면 데이터베이스에 수정하지 않는다. false 옵션은 읽기 전용일 때 사용한다. true
table 하나의 엔티티를 두 개 이상의 테이블에 매핑할 때 사용한다. 지정한 필드를 다른 테이블에 매핑할 수 있다. 현재 클래스가 매핑된 테이블
nullable(DDL) null 값의 허용 여부를 설정한다. false로 설정하면 DDL 생성 시에 not null 제약조건이 붙는다 true
unique(DDL) @Table의 uniqueConstraints와 같지만 한 컬럼에 간단히 유니크 제약조건을 걸 때 사용한다. 만약 두 컬럼 이상을 사용해서 유니크 제약조건을 사용하려면 클래스 레벨에서 @Table.uniqueConstraints를 사용해야 한다.
ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤcolumnDefinition(DDL)ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ 데이터베이스 컬럼 정보를 직접 줄 수 있다. 필드의 자바 타입과 방언 정보를 사용해서 적절한 컬럼 타입을 생성한다.
length(DDL) 문자 길이 제약조건, String 타입에만 사용한다. 255
precision, scale(DDL) BigDecimal 타입에서 사용한다(BigInteger도 사용할 수 있다). precision은 소수점을 포함한 전체 자릿수를, scale은 소수의 자릿수다. 참고로 double, float 타입에는 적용되지 않는다. 아주 큰 숫자나 정밀한 소수를 다루어야 할 때만 사용한다. precision=19, scale=2

@Column을 생략한다면?
컬럼 어노테이션을 생략하면 대부분 @Column 속성의 기본값이 적용되는데, 자바 기본 타입일 때는 nullable 속성에 예외가 있습니다.
@Column 생략 시: 객체 타입일 때는 nullable이 기본값인 true가 되지만, 자바 기본 타입일 때는 JPA가 not null 제약조건을 추가합니다.
@Column 사용 시: 자바 기본 타입에 대해 not null 제약조건을 추가하지 않습니다. 따라서 자바 기본 타입에 @Column을 사용하면 nullable = false로 지정하는 것이 안전합니다.

@Enumerated


자바의 enum 타입을 매핑할 때 사용합니다.

속성 기능 기본값
value EnumType.ORDINAL: enum 순서를 데이터베이스에 저장, EnumType.STRING: enum 이름을 데이터베이스에 저장 EnumType.ORDINAL

❗️ 기본값인 ORDINAL은 enum의 순서가 바뀌면 안되므로 EnumType.STRING을 권장합니다.

@Lob


데이터베이스 BLOB, CLOB 타입과 매핑합니다.

@LOB에는 지정할 수 있는 속성이 없습니다. 대신에 매핑하는 필드 타입이 문자면 CLOB으로 매핑하고 나머지는 BLOB으로 매핑합니다.

  • CLOB: String, char[], java.sql.CLOB
  • BLOB: byte[], java.sql.BLOB

@Transient


이 필드는 매핑하지 않습니다. 따라서 데이터베이스에 저장하지 않고 조회하지도 않습니다. 객체에 임시로 어떤 값을 보관하고 싶을 때 사용합니다.

@Access


JPA가 엔티티 데이터에 접근하는 방식을 지정합니다.

  • 필드 접근: AccessType.FIELD는 필드에 직접 접근합니다. 필드 접근 권한이 private이어도 접근할 수 있습니다.
  • 프로퍼티 접근: AccessType.PROPERTY로 지정합니다. 접근자(getter)를 사용합니다.