기본키 매핑, @Id, @GeneratedValue
필드를 테이블의 컬럼과 매핑하는 애노테이션도 있지만 해당 필드를 기본키로 매핑하는 애노테이션은 @Id, @GeneratedValue이다.
@Id 애노테이션은 기본키를 직접 만들어서 지정해주는 경우에 사용한다.
하지만, MySQL의 Auto Increment나 Oracle의 Sequence 같이 기본키 값을 자동으로 생성되도록 하기 위해서는 @Id 뿐만 아니라 @GeneratedValue를 함께 지정해줘야한다.
@GeneratedValue
@GeneratedValue는 JPA가 테이블의 기본키 값을 자동으로 생성해주는 애노테이션이다. DB에 따라 자동으로 생성해주는 방식이 다른 것과 유사하게 자동생성 전략을 지정해주어야한다.
public enum GenerationType {
TABLE, // 키 생성용 테이블을 직접 생성해 사용하며 모든 DB에서 사용 가능하다.
SEQUENCE, // Oracle, PostgreSQL, DB2, H2 에서 시퀀스를 생성해 사용한다.
IDENTITY, // MySQL, PostgreSQL, DB2, SQL Server에서 사용하며 DB에 기본키 생성을 위임한다.
AUTO; // Dialect에 따라 자동으로 생성 전략을 지정한다.
private GenerationType() {
}
}
GenerationType.TABLE
Table 전략은 기본키를 사용할 수 있는 전용 테이블을 하나 생성하여 사용하는 전략이다. @TableGenerator 애노테이션을 이용해 키 생성 테이블 명과 식별자 이름 등을 지정한다.
public @interface TableGenerator {
String name(); // 식별자 생성 이름
String table() default ""; // 키 생성 전용 테이블 명
String catalog() default "";
String schema() default "";
String pkColumnName() default ""; // 시퀀스 컬럼명
String valueColumnName() default ""; // 시퀀스 값 컬럼명
String pkColumnValue() default ""; // 키로 사용할 컬럼 값 이름
int initialValue() default 0; // 초기 값
int allocationSize() default 50; // 한번 호출에 증가하는 수
UniqueConstraint[] uniqueConstraints() default {};
Index[] indexes() default {};
}
// 사용 방식
@Entity
@TableGenerator(name = "MEMBER_SEQ_TEST", table = "CUSTOM_SEQUENCE", pkColumnValue = "MEMBER_SEQ", allocationSize = 1)
public class Member {
@Id @GeneratedValue(strategy = GenerationType.TABLE, generator = "MEMBER_SEQ_TEST")
private Long id;
}
위와 같이 지정하는 경우 DB내에 “CUSTOM_SEQUENCE”라는 이름의 테이블이 생성되며, 해당 테이블은 MEMBER_SEQ라는 시퀀스 키와 NEXT_VAL이 저장된다.
따라서, 각각의 엔티티에 지정된 Table 전략의 키 생성 컬럼들을 DB에 지정한 후 호출 시마다 테이블을 조회해 사용한다. 이런 경우 성능적으로 떨어진다는 단점이 있다.
GenerationType.IDENTITY
기본키 생성을 DB에 위임해서 사용한다. DB에 위임해서 사용하기 때문에 실제로 DB에 데이터가 적재되기 이전에는 JPA가 Id값을 모른다.
이러한 경우, persist가 호출 되는 경우에 영속성 컨텍스트 내 1차 캐시에 값이 적재될 수가 없다.
그렇기 때문에 IDENTITY 전략을 사용하는 경우 persist는 예외적으로 commit이 일어나기 전에 실제 DB에 insert 쿼리를 날린 후 Id 값을 조회해 1차 캐시에 적재한다.
따라서, IDENTITY 전략을 사용하는 경우에는 쓰기 지연 SQL 저장소에 쌓지 않고 바로 입력된다는 단점이 있다.
@Entity
public class Member {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
}
GenerationType.SEQUENCE
유일한 값을 순선대로 생성해 사용하는 시퀀스 오브젝트를 자동으로 만들어서 사용한다. SEQUENCE 전략을 사용하면 JPA는 우선 시퀀스를 하나 생성한다.
객체를 생성해 데이터를 지정하고 persist하는 경우 Id 값을 지정하지 않았기 때문에 DB에서 id 값을 조회해와야 한다.
SEQUENCE 전략은 IDENTITY와 다르게 생성한 시퀀스를 호출해 다음 값을 가져온 후 데이터의 id 값으로 지정한 뒤 쓰기 지연 SQL 저장소에 보낸다.
이 후 commit 시점에 쓰기 지연 SQL 저장소에 있던 쿼리들이 DB 내로 반영된다.
public @interface SequenceGenerator {
String name(); // 시퀀스 식별자 이름
String sequenceName() default ""; // DB에 실제 등록되는 시퀀스 이름, 해당 시퀀스 이름으로 next val 조회
String catalog() default "";
String schema() default "";
int initialValue() default 1; // 시퀀스 DDL 생성시 초기 값을 지정
int allocationSize() default 50; // 시퀀스 호출 시 증가하는 수 지정 (call next value for 시퀀스)
}
SEQUENCE 전략은 TABLE 전략과 비슷한 방식으로 사용한다.
@Entity
@SequenceGenerator(name = "MEMBER_SEQ_TEST", sequenceName = "member_seq", initialValue = 1, allocationSize = 1)
public class Member {
@Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "MEMBER_SEQ_TEST")
private Long id;
}
allocationSize 값이 50으로 설정되어 있는 이유
JPA에서 @GeneratedValue를 사용하는 경우 Id필드에 데이터를 직접 할당하지 않기 때문에 실제 DB에서 다음 Id 값을 조회해온다.
이 때, DB에 접근하기 때문에 비용이 발생하는데, 해당 값이 1인 경우에는 매번 데이터를 지정할 때 마다 DB에 접근하게 된다.
따라서, 성능적인 이슈를 줄이기 위해, 한 번에 시퀀스를 호출 할때 50개 정도 호출해 와 50개 만큼의 여유분을 할당 받고 최초시퀀스 호출 이후의 호출은 여유 분을 모두 사용하기 전 까지 메모리에서 next value를 가져와 사용한다.
실제 DB에는 50개를 할당받았다는 값이 저장되고, 다음 호출 시에는 51번 부터 100번까지의 값을 할당 받는다.
여러 대의 서버에서 동시에 호출하는 경우 각각의 시퀀스 호출마다 할당 받는 값의 범위가 다를 것이기 때문에 동시성 이슈는 없다.
📄 References
김영한님의 자바 ORM 표준 JPA 프로그래밍 - 기본편 : https://www.inflearn.com/course/ORM-JPA-Basic/dashboard