[jpa] 기본키(pk) 매핑 방법 및 생성 전략 Reference

인프런 강의 참고

Goal

  • 기본 키 매핑 방법
  • 기본 키 자동 생성 전략 4가지
    • IDENTITY
    • SEQUENCE
    • TABLE
    • AUTO
  • 기본 키 생성 전략

기본 키 매핑

@Id @GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
  1. 직접 할당
    • @Id 만 사용
  2. 자동 생성
    • @Id와 @GeneratedValue를 같이 사용
    • 네 가지 전략이 있다.

자동 생성 전략 (네 가지)

IDENTITY

개념

  • @GeneratedValue(strategy = GenerationType.IDENTITY)
  • 기본 키 생성을 데이터베이스에 위임
  • 즉, id 값을 null로 하면 DB가 알아서 AUTO_INCREMENT 해준다.
    • Ex) MySQL, PostgreSQL, SQL Server DB2 등
public class Member {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id; 
}
// H2
create table Member (
  id varchar(255) generated by default as identity,
  ...
)
// MySQL
create table Member (
  id varchar(255) auto_increment,
  ...
)

특징

  • IDENTITY 전략은 entityManager.persist() 시점에 즉시 INSERT SQL을 실행하고 DB에서 식별자를 조회한다.
    • JPA는 보통 트랜잭션 commit 시점에 INSERT SQL을 실행한다.
    • [추가 설명]
      • IDENTITY 전략은 id 값을 설정하지 않고(null) INSERT Query를 날리면 그때 id의 값을 세팅한다.
        • AUTO_INCREMENT는 DB에 INSERT SQL을 실행한 이후에 id 값을 알 수 있다.
        • 즉, id 값은 DB에 값이 들어간 이후에서야 알 수 있다는 것이다.
      • Q. id 값을 DB에 값이 들어간 이후에 알게 됐을 때의 문제점?
        • 영속성 컨텍스트에서 해당 객체가 관리되려면 무조건 pk 값이 있어야 한다.
        • 하지만 이 경우 pk 값은 DB에 들어가봐야 알 수가 있다.
        • 다시 말해서, IDENTITY 전략의 경우 영속성 컨텍스트의 1차 캐시 안에 있는 @Id 값은 DB에 넣기 전까지는 세팅을 할 수 없다는 것이다. (JPA 입장에서는 Map의 key 값이 없으니까 해당 객체의 값을 넣을 수 있는 방법이 없다.)
      • A. 해결책?
        • IDENTITY 전략에서만 예외적으로 entityManager.persist()가 호출되는 시점에 바로 DB에 INSERT 쿼리를 날린다. (다른 전략에서는 이미 id 값을 알고 있기 때문에 commit 하는 시점에 INSERT 쿼리를 날린다.)
        • 위의 과정을 통해 entityManager.persist()가 호출되자마자 INSERT SQL을 통해 DB에서 식별자를 조회하여 영속성 컨텍스트의 1차 캐시에 값을 넣는다. (SELECT 문을 다시 날리지 않아도 된다.)
  • 단점: 모아서 INSERT 하는 것이 불가능하다.
    • 하지만, 버퍼링해서 Write 하는 것이 큰 이득이 있지 않기 때문에 크게 신경쓰지 않아도 된다.
    • 하나의 Transaction 안에서 여러 INSERT Query가 네트워크를 탄다고 해서 엄청나게 비약적인 차이가 나지 않는다.

SEQUENCE

개념

  • @GeneratedValue(strategy = GenerationType.SEQUNCE)
  • 데이터베이스 Sequence Object를 사용
    • DB Sequence는 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트
    • 테이블 마다 시퀀스 오브젝트를 따로 관리하고 싶으면 @SequenceGenerator에 sequenceName 속성을 추가한다.
  • 즉, DB가 자동으로 숫자를 generate 해준다.
    • Ex) Oracle, PostgreSQL, DB2, H2 등
  • @SequenceGenerator 필요
@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; 
}
// 1부터 시작해서 1 증가 
create sequence MEMBER_SEQ start with 1 increment by 1

특징

  • SEQUENCE 전략은 id 값을 설정하지 않고(null) generator에 매핑된 Sequence 전략(“MEMBER_SEQ”)에서 id 값을 얻어온다.
    • 해당 Sequence Object는 DB가 관리하는 것이기 때문에 DB에서 id 값을 가져와야 한다.
  • Q1. id 값을 DB에 값이 들어간 이후에 알게 됐을 때의 문제점?
    • 위의 IDENTITY 전략과 마찬가지의 상태
  • A1. 해결책?
    • 1) entityManager.persist()를 호출하기 전에 DB의 Sequence에서 pk 값을 가져와야 한다.
      • hibernate: call next value for MEMBER_SEQ이 수행됨.
      • 현재의 값: member.id = 1
    • 2) DB에서 가져온 pk 값을 해당 객체의 id 에 넣는다.
    • 3) 이후에 entityManager.persist()를 통해 영속성 컨텍스트에 해당 객체가 저장되는 것이다.
      • 이 상태에서는 아직 DB에 INSERT 쿼리가 날라가지 않고, 영속성 컨텍스트에 쌓여있다가 트랜잭션 commit 하는 시점에 INSERT 쿼리가 날라간다.
    • 4) 필요한 경우 버퍼링이 가능하다.
      • IDENTITY 전략에서는 INSERT 쿼리를 날려야 pk를 알 수 있었기 때문에 버퍼링이 불가능하다.
  • Q2. 위의 해결책이면 계속 네트워크를 왔다갔다 해야되기 때문에 성능 상의 저하가 있지 않을까?
    • SEQUENCE 전략을 사용할 경우, Sequence를 매번 DB에서 가지고오는 과정에서 네트워크를 타기 때문에 성능 상의 저하를 가져올 수 있다.
    • 차라리 INSERT Query를 한 번에 날리는 것이 낫지 않을까?
  • A2. allocationSize 속성값 (기본값: 50) 이용
    • 이를 해결하기 위한 성능 최적화의 방법으로 allocationSize 옵션을 사용한다.
    • 1) 이 옵션을 사용하면 next call을 할 때 미리 DB에 50개를 한 번에 올려 놓고 (DB는 sequence가 51로 세팅된다.) 메모리 상에서 1개씩 쓰는 것이다.
    • 2) 50개를 모두 사용하면 그 때 또 next call을 날려서 다시 50개를 올려 놓는다. (DB는 sequence가 101로 세팅된다.) 메모리에서 sequence를 가져와 51부터 사용할 수 있다.
      • create sequence MEMBER_SEQ start with 1 increment by 50
    • 예시)
      // 1(1로 맞추기 위한 dummy 호출), 51(최적화를 위한 호출)
      em.persist(member1); // next call 2번 호출 
      em.persist(member2); // MEM
      em.persist(member3); // MEM
      
  • Q3. 옵션값을 50보다 큰 수로 정하면 좋지 않나?
  • A3. 이론적으로는 더 큰 수로 설정할수록 성능은 좋아진다.
    • 하지만, 중간에 애플리케이션(웹 서버)를 내리는 시점에 사용하지 않는 seq 값이 날라간다. 즉, 중간에 구멍이 생긴다.
    • 중간의 seq 공백이 큰 문제가 되는 것은 아니지만, 그래도 낭비가 되는 것이므로 적당한 50~100 사이로 설정하는 것이 좋다.

@SequenceGenerator 속성

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

TABLE

개념

  • @GeneratedValue(strategy = GenerationType.TABLE)
  • 키 생성 전용 테이블을 하나 만들어서 데이터베이스 시퀀스를 흉내내는 전략
  • @TableGenerator 필요
@Entity
@SequenceGenerator(
  name = "MEMBER_SEQ_GENERATOR", 
  table = "MY_SEQUENCES", // 데이터베이스 이름 
  pkColumnValue = "MEMBER_SEQ",
  allocationSize = 1)
public class Member {
  @Id
  @GeneratedValue(strategy = GenerationType.TABLE,
                  generator = "MEMBER_SEQ_GENERATOR")
  private Long id; 
}
create table MY_SEQUENCES (
  sequence_name varchar(255) not null,
  next_val bigint,
  primaty key ( sequence_name )
)

특징

  • 장점: 모든 데이터베이스에 적용 가능
  • 단점: 최적화 되어있지 않은 테이블을 직접 사용하기 때문에 성능상의 이슈가 있음
  • 운영 서버에서는 사용하기에 적합하지 않다.
    • Why? DB에서 관례로 쓰는 것이 있기 때문에
  • Q. Table 전략에서 allocationSize = 50이고, 서버가 여러 대인 경우에는 문제가 없을까?
  • A. 문제 없다.
    • 미리 값을 올려두는 방식이기 때문에 여러 대의 서버가 붙더라도 상관이 없다.
    • 웹 서버 10대가 동시에 호출하는 경우, 순차적으로 사용하여 값이 올라가 있을 것이다.
      • A 서버: 1~50
      • B 서버: 51~100 …

@TableGenerator 속성

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

AUTO

개념

  • @GeneratedValue(strategy = GenerationType.AUTO)
  • 기본 설정 값
  • 방언에 따라 위의 세 가지 전략을 자동으로 지정한다.

권장하는 식별자 전략

기본 키 제약 조건

  1. null이 아니다.
  2. 유일하다.
  3. 변하면 안된다.
  • 미래까지 이 조건을 만족하는 자연키는 찾기 어렵다. 그 대신 대리키/대체키를 사용하자.
    • 자연키 (Natural Key)
      • 비즈니스적으로 의미가 있는 키
      • Ex) 전화번호, 주민등록번호 등
    • 대리키/대체키 (Generate Value)
      • 비즈니스적으로 상관없는 키
      • Ex) Generate Value, 랜덤 값, 유휴 값 등
  • 예를 들어, 주민등록번호도 기본 키로 적절하지 않다.
    • Why? 갑자기 개인정보 보호의 목적으로 DB에 주민등록번호를 저장하지 말라 조건이 들어온다. 이때, 주민등록번호를 pk로 사용하고 있는 테이블뿐만 아니라 해당 테이블의 pk를 fk로 JOIN하고 있는 다른 테이블에서도 문제가 생긴다.
    • 즉, 주민등록번호를 fk로 참조하고 있는 모든! 테이블을 마이그레이션 해야 한다.

권장하는 식별자 구성 전략

(Long형) + (대체키) + (적절한 키 생성 전략) 사용

  1. Long Type (아래 참고)
  2. 대체키 사용: 랜덤 값, 유휴 ID 등 비즈니스와 관계없는 값 사용
  3. AUTO_INCREMENT 또는 Sequnce Object 사용
  • [참고] Q. id 값(pk 값)은 어떤 타입을 써야 할까?
    • int
      • 0이 있다.
    • Integer
      • 10억 정도 까지만 가능하다.
    • Long
      • 채택!
      • Long의 크기가 Integer의 2배지만 애플리케이션 전체로 봤을 때의 영향은 작다고 볼 수 있다.
      • 오히려 10억이 넘어갔을 때 해당 id 값을 타입을 변경하는 것이 더 어렵다.

Reference