JPA에서 엔티티와 데이터베이스의 기본 키(Primary Key) 매핑을 알아보겠습니다.
데이터베이스에는 유일성 최소성을 만족하는지 여부에 따라 몇가지 키 종류가 존재합니다.
유일성이란, 여러 레코드들 중, 하나의 레코드를 특정지을 수 있는 키의 특성입니다.
예를들어 이름,생일과 같은 특성은 레코드를 하나로 특정지을 수 없지만, [**학교 , 학번]**의 조합이나 [**주민등록번호]**와 같은 특성은 레코드를 유일하게 특정지을 수 있으므로 유일성을 만적하는 특성입니다.
최소성이란, 레코드를 유일하게 식별하는데 꼭 필요한 최소한의 특성만 선택되어야한다는 키의 특성입니다.
예를 들어, [이름, 나이, 주민등록번호] 의 속성 조합으로 유일성을 만족하지만, 이름과 나이 없이 주민등록번호만으로 레코드를 유일하게 식별할 수 있으므로, 최소성을 만족하지 못하는 키이고, [주민등록번호] 로 구성된 키는 최소성을 만족한다고 할 수 있습니다.
데이터베이스 시스템에서는 보통 레코드의 기본 키를 설정하기 위한 디폴트 설정이 있습니다. 앞서 소개한 주민등록번호나 학교+학번은 사실 비즈니스 상 변경될 수도 있는 값이고, 인덱스를 통해 효율적으로 탐색하기에는 부적절한 키입니다.
그렇기때문에 데이터베이스 시스템에서는 비즈니스 로직과 관계 없는 고유한 값을 만들어 기본 키로 채택하는데요, 이번 글은 JPA에서 엔티티와 데이터베이스 테이블 간 “기본 키”를 어떻게 생성하고 매핑하는지 알아보겠습니다.
참고로 좋은 데이터베이스 기본 키를 선택하는 기준은 아래와 같습니다.
JPA로 엔티티를 한번 이상 만들어봤다면, 기본적으로 아래 구조를 띄는것을 알 수 있습니다.
@Entity
public class Member{
@Id
@Column(name="id")
private String id;
}
이렇게 구성하면, Member테이블의 Key인 Id필드는 언제 지정될까요? 영속성 관리에서 배운것처럼, 엔티티는 바로 데이터베이스에 저장되지 않고 1차캐시에 저장됩니다.
고치기
만약 데이터베이스의 기본 키 생성 전략이 직접할당일 경우에는, 애플리케이션에서 엔티티의 키를 설정할 방법이 없을것입니다.
JPA에서는 데이터베이스 벤더별로 각기 다른 키 생성 전략과 자바 엔티티를 매핑하기 위해, @Id 컬럼에 여러 옵션들을 제공합니다.
기본키를 어떤 방식으로 생성하느냐에 따라, 개발자가 키를 직접 설정하는 직접할당 전략, 자동으로 키를 생성하는 IDENTITY, SEQUENCE, TABLE, AUTO 전략을 살펴봅니다.
아래의 Java 타입을 선언한 뒤, Id애너테이션을 통해 필드가 기본 키로 매핑되도록 합니다.
@Id필드 타입
Id 애너테이션은 다음과 같은 자바 타입에서만 적용 가능합니다
기본 키 직접 할당 전략은 프로그래머가 엔티티매니저에 영속되기 전에 엔티티의 키를 직접 할당하는 방식입니다.
이 방식에서 식별자 값 없이 영속시키면 PersistenceExeption이 발생합니다.
설정방법: Id 필드에 @Id 애너테이션을 적용하면 기본키를 직접 할당방식으로 테이블이 생성됩니다.
IDENTITY전략은 기본 키 생성 전략을 데이터베이스에 위임하는 방식입니다.
보통 AUTO_INCREMENT같은 자동 키 생성방법을 제공하는 MySQL, Postgresql, DB2등에서 사용합니다.
설정방법: 필드에 GeneratedValue 애너테이션, strategy 옵션을 GenerationType.IDENTITY로 설정
@Entity
public class Member{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
}
em.persist(member) → insert query 날리고 → select query(id 포함된 레코드) → 영속성 컨텍스트에 저장
주의사항
영속성 컨텍스트에 저장될때는 필수로 식별자 값이 필요한데, IDENTITY 전략에서는 기본 키를 데이터베이스에서 지정하므로, 식별자를 알 수없다.
그러므로 IDENTITY전략을 사용하는 엔티티는 영속되는 시점에 INSERT쿼리를 데이터베이스에 전달한다.
따라서 IDENTITY전략에서는 트랜잭션을 지원하는 쓰기지연이 동작하지 않는다.
최적화 방법
IDENTITY전략에서는 영속 시 INSERT쿼리를 데이터베이스에 전달하고, 식별자를 얻기 위해 추가로 조회작업을 해야해서 총 2번의 통신이 이뤄진다.
JDBC3에서 추가된 Statement.getGeneratedKeys()를 사용하면, 데이터를 저장하면서 동시에 생성된 기본 키 값도 얻어올 수 있다.
데이터베이스에서 시퀀스는 유일한 값을 순서대로 생성하는 데이터베이스 오브젝트입니다. Sequence를 제공하는 Oracle,Postgresql, DB2,H2등에서 사용가능합니다.
SEQUENCE방법은 이를 통해 기본 키를 생성합니다.
설정방법:
@Entity
@SequenceGenerator(
name="MEMBER_SEQ_GENERATOR",
sequenceName="MEMBER_SEQ",
initialValue=1,
allocationSize=1
)
public class Member(){
@Id
@GeneratedValue(strategy=GenerationType.SEQUENCE)
private Long id;
}
@SequenceGenerator의 옵션이 다양하게 존재하는것을 확인할 수 있다.
참고사항1. 시퀀스 동작구조
IDENTITY전략과 마찬가지로 ,데이터베이스에서 식별자 값을 가져와야 하지만 IDENTITY전략이 영속성 컨텍스트에서 Insert 쿼리를 날린 뒤 조회를 한것과는 달리, SEQUENCE 전략에서는 데이터베이스 시퀀스만 조회해 식별자값만 가져온다.
SELECT emp_seq.NEXTVAL
, emp_seq.CURRVAL
FROM dual
따라서 SEQUENCE전략은 트랜잭션을 사용하는 쓰기지연 방식을 지원하지만, 시퀀스를 추가적으로 조회한다.
참고사항 2. allocationSize의 기본값이 50이다.
hibernate.id.new_generator_mappings 설정이 true인 경우, allocationSize는 기본적으로 50으로 설정됩니다. 이는 JPA의 시퀀스 접근횟수를 줄이기 위함이고, 아래와 같이 동작합니다.
멀티서버 환경에서는 엔티티가 생성된 순서대로 ID가 증가하지는 않지만, Insert성능 측면에서 가장 훌륭함.
TABLE전략은 키 생성용 테이블을 만들고, 이름과 값으로 사용할 컬럼을 만들어 데이터베이스 시퀀스를 흉내내는 전략입니다. 또한, 테이블을 사용하므로 모든 데이터베이스에서 적용가능합니다.
설정방법:
@TableGenerator(
name="BOARD_SEQ_GENERATOR",
table="MY_SEQUENCE",
pkColumnValue="BOARD_SEQ", allocationSize=1)
public class Board{
@Id
@GeneratedValue(strategy = GenerationType.TABLE,
generator = "BOARD_SEQ_GENERATOR")
private Long id;
}
위와 같이 등록 하게 되면, 아래와 같이 시퀀스 이름과 시퀀스 값을 컬럼으로 갖는 테이블을 만들어 시퀀스를 관리합니다
앞서 @TableGenerator의 속성들 중 Column 같은 이름에 사용되는 속성들이 많이 보이는데요, 시퀀스테이블을 구성할 때 사용하는 이름입니다.
sequence_name | next_val |
---|---|
BOARD_SEQ | 102 |
MEMBER_SEQ | 50 |
참고사항: 테이블전략 동작구조
(시퀀스)테이블의 값을 조회하고, 이 값을 기반으로 Update쿼리를 한번 더 날리기때문에, 시퀀스 전략보다 네트워크 통신을 1회 더 한다는 단점이 있습니다.
이는 SEQUENCE방법과 같이 allocationSize를 통해 최적화할 수 있습니다.
@GeneratedValue.strategy의 기본 설정값인 AUTO전략입니다.
AUTO 전략은 선택한 데이터베이스 방언에 따라 IDENTITY, SEQUENCE, TABLE중 하나를 자동으로 선택합니다.
JPA에서 키 생성 전략은 INSERT성능에 중요한 영향을 미친다!!!
Table 방식과 Sequence 방식은 근본적으로 어떤 차이점이 있나요? (시퀸스는 테이블과 어떻게 다른가요?)