JPA - 기본키 생성 전략
관계형 데이터베이스에서 테이블은 각각의 행을 구별하는 기본키가 필요하다.
기본키를 만들 때는 주민등록번호, 주문번호 같은 '자연키' 를 사용할 수도 있고,
논리적 의미가 하나도 없는 하나씩 증가하는 순번 같은 '대체키'를 사용할 수도 있다.
기본키로 어떤 것을 선택하면 좋을까?
주문번호 관련 생성 정책이 변경되거나 주민등록번호를 더 이상 저장하면 안된다는 정책이 생기는 등 자연키는 기본키로 정할 수 있는 올바른 전략이 아닐 수도 있다. 상황에 따라 다르겠지만 대체키를 권장한다.
대체키를 사용하고자 할 때는
Integer
타입 (약 10자리수, 21억 정도까지 표현) 대신Long
타입 (약 19자리, 9백경 정도까지 표현)을 사용하여 표현 범위를 늘리는 것이 좋다.
@GeneratedValue(strategy =)
그렇다면 이 기본키는 어떻게 생성할까?
JPA 엔티티에서 @GeneratedValue
어노테이션을 기본키 필드 위에 붙이면, 기본키(대체키)를 자동으로 생성해주는 전략을 사용할 수 있다.
전략은 크게 4가지 인데 하나씩 살펴보자.
IDENTITY
@Entity
public class Badge {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}
기본키 생성을 hibernate 가 아니라, 데이터베이스가 하도록 위임한다.
데이터를 저장한 후 commit 시점에 최적의 상황을 고려하여 쿼리를 실행시키는 것이 아니라, persist()
시점에 insert 문이 바로 실행되고 DB 에서 기본키 값을 생성한다. 그리고 이 키 값을 가져와서 JPA 1차 캐시에 (key, entity) 키, 값을 저장한다.
따라서 IDENTITY 옵션 사용시 write-behind (쓰기지연) 기능을 무시한다. write-behind 는 한 트랜잭션 안에서 이루어지는 성능 최적화이므로 이 부분은 이루어지지 않아도 양보가 가능하다. 단, bulk insert 가 안되는 문제가 있을 수 있다. (다른 해결책이 있음)
- MySQL, PostgreSQL, SQL Server, DB2 등에서 주로 사용한다.
테스트를 위해 docker로 간단하게 MySQL 환경을 마련해보자.
# docker hub 로부터 자동으로 mysql 이미지를 가져와서 도커 컨테이너를 실행한다.
$ docker run -d -p 3306:3306 --name wordbe-mysql -e MYSQL_ROOT_PASSWORD=1234 -e MYSQL_DATABASE=mytest -e MYSQL_USER=wordbe -e MYSQL_PASSWORD=1234 mysql
# mysql 서버와 인터렉트모드
$ docker exec -i -t wordbe-mysql bash
스프링부트에서는 mysql 을 연결할 수 있도록 드라이버를 먼저 설치한다.
dependencies {
implementation 'mysql:mysql-connector-java'
}
그리고 스프링부트 설정에 mysql 정보를 추가한다.
application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/mytest
username: wordbe
password: 1234
테스트코드를 입력한 뒤, 실행시켜보면
@Test
void GeneratedValue_IDENTITY_전략() {
Badge badge = new Badge("칭찬왕");
badgeRepository.save(badge);
}
create table badge (
id bigint not null auto_increment,
name varchar(255),
primary key (id)
) engine=InnoDB
insert into badge (name) values ('칭찬왕');
테이블은 MySQL 의 Native Generator 를 통해 기본키가 생성되게 된다. insert 문이 실행되는데 id는 데이터베이스에서 자동으로 입력되어 들어간다.
SEQUENCE
Hibernate 가 만들어주는 시퀀스를 사용한다.
데이터베이스(Oracle 등)가 제공해주는 시퀀스를 사용할 수 있다. @SequenceGenerator
등록이 필요하다.
@Entity
@SequenceGenerator(
name = "BADGE_SEQ_GENERATOR",
sequnceName = "BADGE_SEQ",
initialValue = 1,
allocationSize = 50)
public class Badge {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "BADGE_SEQ_GENERATOR")
private Long id;
private String name;
}
데이터 저장시 (persist()) 데이터베이스에서 시퀀스를 조회해서 JPA 1차 캐시에 (key, entity) 키, 값을 저장한다.
데이터베이스에 시퀀스를 조회하러 DB I/O 요청 cost가 생긴다. 하지만, initialValue 와 allocationSize 를 조정함으로써 메모리에 미리 순번을 저장해 놓을 수 있다. 따라서 대량의 insert 쿼리가 주로 발생하는 엔티티라면 이 값을 설정해주면 좋을 것이다.
initialValue 는 기본값이 1, allocationSize 는 기본값이 50이다. 즉 1부터 한번에 50개의 순번을 가져온다. 한 번에 더 많이 가져오려면 allocationSize = 100 등으로 이 값을 늘리면 될 것이다.
TABLE
키 생성용 테이블을 별도로 사용하여 (마치 시퀀스처럼), 키를 생성한다.
모든 데이터베이스에서 적용가능하지만, 성능이 좋지 않으므로 실제 운영에서 잘 사용되지 않는다.
@Entity
@TableGenerator(
name = "BADGE_KEY_GENERATOR",
table = "BADGE_KEYS",
pkColumnValue = "BADGE_KEY"
initialValue = 1,
allocationSize = 50)
public class Badge {
@Id
@GeneratedValue(strategy = GenerationType.TABLE, generator = "BADGE_KEY_GENERATOR")
private Long id;
private String name;
}
AUTO
@Entity
public class Badge {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;
}
이 전략옵션은 말그대로 '자동'으로 적절한 전략을 알아서 매핑시킨다. @GeneratedValue
라고만 쓰면 기본으로 AUTO 옵션을 사용한다.
Hibernate 는 적절한 옵션을 알아내기 위해 아래 논리대로 탐색한다.
기본키 타입이
UUID
일 때UUID Generator
가 된다.Number 타입이면, hibernate.id.new_generator_mapping 옵션(기본값 true)을 확인한다.
2-1. false 이면
Native Generator
를 사용한다. (MySQL 의 경우 auto_increment)2-2. true 이면,
Sequence Generator
를 사용한다. 2-2-1. 이 때 데이터베이스가 Sequence 를 지원하지 않는다면
TABLE Generator
를 사용한다.
만약, MySQL 을 사용하고, 아무 옵션도 주지 않았다면, AUTO 로 적용되어 SEQUENCE 로 기본키를 자동으로 생성하게 될 것이다.
- 위 예제에서는 Number 타입(Long) 이고, hibernate.id.new_generator_mapping = true (기본값) 이다.
- 데이터베이스는 H2 를 이용했으므로 Sequence 전략을 따른다.
- 테스트 결과는 아래와 같다.
@DataJpaTest
@Rollback(value = false)
class BadgeTest {
@Autowired
BadgeRepository badgeRepository;
@Test
void GeneratedValue_AUTO_전략() {
Badge badge = new Badge("칭찬왕");
badgeRepository.save(badge);
}
}
create table badge (
id bigint generated by default as identity,
name varchar(255),
primary key (id)
)
...
call next value for hibernate_sequence
2021-09-25 17:22:10.889 DEBUG 62185 --- [ main] org.hibernate.SQL :
insert
into
badge
(name, id)
values
(?, ?)
2021-09-25 17:22:10.891 TRACE 62185 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [VARCHAR] - [칭찬왕]
2021-09-25 17:22:10.891 TRACE 62185 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [2] as [BIGINT] - [1]
자연키 생성
마지막으로 기본키를 사용자가 원하는 전략으로 만들어 주고 싶을 경우 아래와 같이 사용하면 된다.
@Entity
public class Badge {
@Id
private Long id;
private String name;
@PrePersist
public void generateId() {
if (this.id != null) {
throw new InvalidOperationException("id가 있는 경우엔 새로운 id를 생성할 수 없습니다.");
}
this.id = RandomStringUtils.randomAlphanumeric(12);
}
}
영문대소문자와 숫자가섞인 랜덤한 12자리를 ID로 만들도록 임의로 설정하였다.
이미 id 가 있는 경우의 엔티티를 save 에서 새로운 아이디를 발급할 수 있는 상황이 혹시나 있을 수 있으므로 예외처리해준다.
'Java, Kotlin, Spring > JPA' 카테고리의 다른 글
일대다(1:N) 페이징 처리(OneToMany Pagination) (1) | 2021.11.28 |
---|---|
JPA - 연관관계 매핑(N:1, 1:N, 1:1, N:N) (0) | 2021.09.21 |
JPA - 단방향, 양방향 연관관계 매핑 (0) | 2021.09.20 |
JPA - 상속 객체와 테이블 매핑 (0) | 2021.09.19 |
Spring Data JPA - Entity, 관계 매핑 (0) | 2021.02.11 |
댓글