기본키 생성 및 제약 조건과 @GeneratedValue의 네 가지 strategy에 대해 이해한다.
인프런 강의 참고
Goal
- 기본 키 매핑 방법
- 기본 키 자동 생성 전략 4가지
- IDENTITY
- SEQUENCE
- TABLE
- AUTO
- 기본 키 생성 전략
기본 키 매핑
@Id @GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
- 직접 할당
- @Id 만 사용
- 자동 생성
- @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 문을 다시 날리지 않아도 된다.)
- IDENTITY 전략에서만 예외적으로
- IDENTITY 전략은 id 값을 설정하지 않고(null) INSERT Query를 날리면 그때 id의 값을 세팅한다.
- 단점: 모아서 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를 알 수 있었기 때문에 버퍼링이 불가능하다.
- 1)
- 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)
- 기본 설정 값
- 방언에 따라 위의 세 가지 전략을 자동으로 지정한다.
권장하는 식별자 전략
기본 키 제약 조건
- null이 아니다.
- 유일하다.
- 변하면 안된다.
- 미래까지 이 조건을 만족하는 자연키는 찾기 어렵다. 그 대신 대리키/대체키를 사용하자.
- 자연키 (Natural Key)
- 비즈니스적으로 의미가 있는 키
- Ex) 전화번호, 주민등록번호 등
- 대리키/대체키 (Generate Value)
- 비즈니스적으로 상관없는 키
- Ex) Generate Value, 랜덤 값, 유휴 값 등
- 자연키 (Natural Key)
- 예를 들어, 주민등록번호도 기본 키로 적절하지 않다.
- Why? 갑자기 개인정보 보호의 목적으로 DB에 주민등록번호를 저장하지 말라 조건이 들어온다. 이때, 주민등록번호를 pk로 사용하고 있는 테이블뿐만 아니라 해당 테이블의 pk를 fk로 JOIN하고 있는 다른 테이블에서도 문제가 생긴다.
- 즉, 주민등록번호를 fk로 참조하고 있는 모든! 테이블을 마이그레이션 해야 한다.
권장하는 식별자 구성 전략
(Long형) + (대체키) + (적절한 키 생성 전략) 사용
- Long Type (아래 참고)
- 대체키 사용: 랜덤 값, 유휴 ID 등 비즈니스와 관계없는 값 사용
- AUTO_INCREMENT 또는 Sequnce Object 사용
- [참고] Q. id 값(pk 값)은 어떤 타입을 써야 할까?
- int
- 0이 있다.
- Integer
- 10억 정도 까지만 가능하다.
- Long
- 채택!
- Long의 크기가 Integer의 2배지만 애플리케이션 전체로 봤을 때의 영향은 작다고 볼 수 있다.
- 오히려 10억이 넘어갔을 때 해당 id 값을 타입을 변경하는 것이 더 어렵다.
- int
관련된 Post
- JDBC, JPA/Hibernate, Mybatis의 차이에 대해 알고 싶으시면 JDBC, JPA/Hibernate, Mybatis의 차이를 참고하시기 바랍니다.
- ORM의 개념에 대해 알고 싶으시면 ORM이란을 참고하시기 바랍니다.