본문 바로가기

Spring/JPA

기본키 매핑

기본 키는 DB에서 특정 레코드를 유일하게 식별하기 위해 사용됩니다. 기본 (Primary Key) 테이블에서 레코드를 식별하는 사용되고 테이블의 레코드에는 기본 값이 반드시 존재해야 합니다. 다시 말해 기본 키는 테이블에 존재하는 수많은 데이터들을 구별할 있는 유일한 기준이 되는 속성이 됩니다. 기본 키는 null 아니고 유일해야 하며 변하지 않아야 합니다.
 
따라서 엔티티를 DB 테이블에 매핑할 엔티티 클래스에 필수적으로 기본 값을 설정해야 합니다. 기본 값을 설정하는 방법은 직접 할당과 자동 생성으로 나뉩니다. @Id 애노테이션만 사용한다면 직접 할당 방식으로 개발자가 엔티티의 기본 값을 직접 세팅해야 합니다. 직접 할당과 달리 자동 생성 방식은 @Id @GeneratedValue 애노테이션을 함께 사용하여 시스템이 자동으로 엔티티의 기본 값을 세팅하도록 만드는 방식입니다. 보통 자동 생성 방식을 많이 사용합니다.

@Id
private Long id;

@Id @GeneratedValue(strategy = GeneratinoType.AUTO)
private Long id;

 

위에서 자동 생성 방식을 보면 @GeneratedValue의 속성으로 strategy를 볼 수 있습니다. @GeneratedValue의 strategy 설정으로 시스템이 기본 키를 생성하는 방식이 달라집니다. 지금부터 네 가지 기본 키 생성 전략을 살펴봅시다.
 

@GeneratedValue 전략

GeneratedValue 애노테이션을 사용해 기본 값을 시스템에서 자동으로 생성하도록 가지 전략이 있습니다.
- AUTO:
방언에 따라 아래 가지 방식 하나를 자동으로 지정합니다. 기본값입니다.
- IDENTITY:
기본 생성을 DB 위임하는 전략입니다.
- SEQUENCE: DB
시퀀스 오브젝트를 사용해 기본 값을 생성하는 전략입니다.
- TABLE:
생성용 테이블을 사용하는 전략입니다.
 
먼저 IDENTITY 전략을 살펴봅시다.

 

IDENTITY 전략

@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;


IDENTITY 전략은 기본 키 생성을 DB에 위임하는 전략으로 MySQL, PostgreSQL, SQL Server, DB2에서 사용하는 전략입니다. IDENTITY 전략을 사용하면 엔티티의 Id 값을 넣지 않은 상태로 DB에 넘기고 DB에서 기본 키를 생성하여 설정하게 됩니다. DB에 엔티티가 들어가기 전에 개발자가 직접 Id 값을 설정하면 안 됩니다.
 
다시 말해 DB로 Id 값이 null인 엔티티가 INSERT 쿼리가 들어오면 그때 DB가 Id 값을 세팅해 줍니다.
여기서 문제는 DB에 값이 들어가야 엔티티의 Id 값이 생긴다는 부분입니다. 알다시피 엔티티를 persist 하면 엔티티가 바로 DB에 들어가지 않고 영속성 컨텍스트의 1차 캐시에 아래 그림과 같이 저장됩니다.

 

1 캐시에 저장될 엔티티의 @Id 값이 필요합니다. 영속성 컨텍스트에 저장되고 관리되려면 엔티티가 기본 값을 가져야 되기 때문에 IDENTITY 전략으로 DB 저장되어야 기본 값이 설정되는 경우에는 문제가 생깁니다.
 
이를 해결하기 위해 JPA에서는 엔티티의 기본 값이 IDENTITY 전략일 경우 em.persist 호출될 바로 DB INSERT 쿼리를 날리도록 만들었습니다. persist 시점에 즉시 INSERT 쿼리를 실행하고 DB에서 식별자를 조회하여 영속성 컨텍스트에서 식별자를 알고 있도록 만들어 문제를 해결했습니다. (이때 DB에서 설정한 식별자를 알도록 만들어 getId 하더라도 SELECT 쿼리가 나가지 않습니다.)
 
따라서 IDENTITY 전략을 사용하여 기본 값을 설정하는 엔티티의 경우 영속성 컨텍스트의 쓰기 지연 기능을 사용할 없습니다. 물론 영속성 컨텍스트의 쓰기 지연 기능이 트랜잭션 단위로 동작하기 때문에 성능 측면에서 중요하다고 수는 없습니다.
 
다음으로 SEQUENCE 전략을 살펴봅시다.

 

SEQUENCE 전략

@Entity
@SequenceGenerator(
    name = "USER_PK_GENERATOR",
    sequenceName = "USER_PK_SEQ",
    initailValue = 1,
    allocationSize = 50)
public class PkEx() {

    @Id @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;
}


기본 값을 자동으로 생성할 SEQUENCE 전략을 선택하면 DB 시퀀스에서 생성하는 유일한 값으로 기본 값을 설정됩니다. DB 시퀀스는 유일한 값을 순서대로 생성하는 특별한 DB 오브젝트입니다. 오라클, PostgreSQL, DB2, H2 데이터베이스에서 사용됩니다.
 
SEQUENCE 전략에서도 개발자가 직접 Id 값을 세팅해서는 안됩니다. SEQUENCE 전략도 기본 값을 생성하는 SequenceObject DB 관리하기 때문에 결국 DB 가봐야 기본 값을 있습니다.
 
그렇다면 SEQUENCE 전략을 선택한 엔티티를 persist 영속성 컨텍스트에 저장하면 어떻게 동작할까요?
엔티티를 persist 하면 Id 값을 call next value SQL DB 날려 Id 값을 먼저 얻고 엔티티에 해당 값을 세팅합니다. 세팅 이후에 영속성 컨텍스트에 저장됩니다. call next value SQL Id 값만 DB로부터 얻었기 때문에 엔티티를 저장하는 INSERT 쿼리를 날리지 않고, 트랜잭션이 커밋될 INSERT 쿼리가 날아갑니다. 따라서 영속성 컨텍스트의 쓰기 지연 기능을 활용할 있습니다
 
하지만 엔티티를 persist 때마다 Id 값을 DB로부터 얻어오면 네트워크 통신이 너무 많아져 성능이 떨어지지 않을까 고민할 있습니다. SEQUENCE 전략은 allocationSize 속성을 통해 이런 고민을 해결해 줍니다. allocationSize 속성으로 시퀀스 호출에 증가하는 수를 설정할 있습니다.
 
다시 말해 allocationSize 1 설정하면 번의 네트워크 연결로 SequenceObject 생성한 Id 값을 1개만 가져옵니다. allocationSize 100으로 설정하면 번에 100개의 Id 값을 가져옵니다. 번에 가져온 100개의 Id 값을 웹서버에서 모두 사용한 경우 다시 next 쿼리를 통해 100개의 순차적인 Id 값을 가져옵니다. SEQUENCE 전략의 allocationSize 속성의 기본값은 50으로 특별한 설정 없이는 번의 네트워크 연결에 50개의 Id 값을 DB에게 얻습니다. 만약 DB 시퀀스 값이 하나씩 증가하도록 설정되어 있다면 allocationSize 반드시 1 설정해야 합니다.
 
allocationSize 번에 여러 Id 값을 가져오는 방식은 미리 값을 DB 올려두는 방식이기 때문에 여러 대의 웹서버가 동시에 next 쿼리를 호출해도 문제가 생기지 않습니다. 단지 DB SeqeunceObject에서 관리하는 Id 값이 올라갈 뿐입니다. SequenceObject 생성하는 Id 값은 순차적으로 증가합니다. 다시 말해 1부터 100 생성했다면, 다음 요청에서는 101부터 200까지 생성하여 전달합니다.
 
SEQUENCE 전략의 initialValue 속성을 통해 시퀀스에서 처음 생성하는 수를 지정할 있습니다. initialValue 속성은 DDL 생성 시에만 사용됩니다.
 
엔티티 별로 따로 시퀀스를 관리하고 싶다면 코드처럼 SequenceGenerator 사용하면 됩니다. @SequenceGenerator 사용하지 않으면 기본적으로 hibernate sequence 전역에서 사용합니다.
 
이번에는 TABLE 전략을 살펴봅시다.

 

TABLE 전략

@Entity
@TableGenerator(
    name = "USER_PK_GENERATOR",
    table = "USER_PK_SEQ",
    pkColumnValue = "USER_SEQ", allocationSize = 1)
public class PkEx() {

    @Id @GeneratedValue(strategy = GenerationType.TABLE, generator="USER_PK_GENERATOR")
    private Long id;
}

 
TABLE 전략을 사용하면 생성 전용 테이블을 하나 만들어서 해당 테이블에서 기본 값을 뽑는 방식입니다. DB 시퀀스를 테이블로 흉내 전략입니다. 모든 DB 적용 가능하다는 장점이 있지만, 기본 값을 생성하기 위한 테이블을 만들어야 하기 때문에 성능 측면에 이슈가 있습니다.
 
@TableGenerator 사용하면 initalValue allocationSize 등의 속성을 설정할 있습니다. @TableGenerator SEQUENCE 전략의 @SequenceGenerator 동일하게 동작하기 때문에 설명은 생략하겠습니다.

 

기본 키 매핑 전략 선택 기준

그렇다면 기본 키 매핑의 @GeneratedValue 전략을 어떤 기준으로 선택해야 할까요?

  • IDENTITY 전략은 주로 데이터베이스 삽입 시점에 ID가 결정되어야 하는 경우 사용되며, 각 행을 독립적으로 삽입할 때 유리합니다.
  • SEQUENCE 전략은 애플리케이션 성능을 최적화할 수 있으며, 일괄 처리와 같이 성능이 중요한 상황에서 추천됩니다.
  • TABLE 전략은 호환성이 중요하거나 특정 데이터베이스 기술에 의존하지 않고자 할 때 고려할 수 있습니다.
  • AUTO 전략은 개발 초기 단계나 특정 데이터베이스 기술에 결정하지 않은 경우 사용할 수 있으나, 프로덕션 환경에서는 특정 전략을 명시적으로 지정하는 것이 좋습니다.

권장하는 식별자(PK) 전략

DB 기본 제약 조건은 가지가 있습니다. 아래  가지 조건을 만족해야 DB 기본 값으로 사용할 있습니다.
 
1.
기본 키는 null 아니어야 합니다.
2.
기본 키는 유일해야 합니다.
3.
기본 키는 변하면 됩니다.
 
위의 가지 조건 중에 가장 어려운 조건이 번째 변하면 된다는 조건입니다. 미래까지 조건을 만족하는 자연키는 찾기 어렵기 때문에 대리키를 사용해야 합니다. 여기서 자연키는 비즈니스적 의미가 있는 전화번호나 주민 번호등을 뜻합니다. 대리키는 GeneratedValue 의해 생성되는 값이나 랜덤 비즈니스랑 전혀 상관없는 값을 기본 값으로 사용해야 합니다.
 
결론적으로 권장하는 기본 값의 형태는 Long + 대체키(AutoIncrement, SequenceObject, UUID, Random) + 생성전략을 사용한 방식입니다


 


 

'Spring > JPA' 카테고리의 다른 글

일대일 연관관계 매핑  (1) 2024.10.17
연관관계 매핑 기초  (1) 2024.10.17
영속성 컨텍스트 플러시  (1) 2024.10.16
영속성 컨텍스트  (2) 2024.10.16
JPA가 등장한 배경  (0) 2024.10.16