본문 바로가기
Framework and Tool/JPA

JPA - 엔티티 맵핑 - 기본 키 맵핑

by ocwokocw 2021. 6. 27.

- 참조: 자바 ORM 표준 JPA 프로그래밍

- 기본 키 맵핑

지금 예제까지는 기본 키 필드에 @Id 어노테이션을 붙이고, 키 값은 어플리케이션에서 직접 할당하였다. MySQL 이나 오라클같은 DB 에서는 기본키를 시퀀스나 AUTO_INCREMENT 기능을 이용하기도 하는데, 하이버네이트도 여러 가지 기본 키 생성전략을 지원한다.

  • 직접 할당: 여태까지 예제에서 썼던 방법으로 직접 할당하는 방법이 있다.
  • 자동 생성 - IDENTITY: 기본 키 생성을 DB 에 위임한다.
  • 자동 생성 - SEQUENCE: DB 의 시퀀스를 이용하여 키를 할당한다.
  • 자동 생성 - 테이블: 키 생성 테이블을 사용한다.

직접 할당하는 경우 @Id 어노테이션만 붙이면 되고, 자동 생성 전략을 사용할 경우 @GeneratedValue 를 추가하여 전략을 골라야 한다.

 

키 생성 전략을 사용하려면 persistence.xml 에 아래 옵션을 true 로 설정해야 한다. 이 옵션의 기본값은 false 인데 이는 과거버전과의 호환성을 유지하기 위한 목적이므로 신규 구축하는 시스템이라면 true 를 추천한다.

	<property name="hibernate.id.new_generator_mappings" value="true"/>

- IDENTITY 전략

기본 키 생성을 DB 에 위임하는 전략으로 주로 MySQL, PostgreSQL, DB2 SQL Server 에서 사용한다.

 

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
private Long id;

 

위와 같이 자료형을 Long 으로 바꿔주고 @GeneratedValue 의 기본전략을 IDENTITY 로 설정한다.

 

private static void createMember() {
	
	EntityManager em = emf.createEntityManager();
	EntityTransaction tx = em.getTransaction();
	
	tx.begin();
	
	Member member = new Member();
	member.setUserName("ocwokocw1");
	
	em.persist(member);
	System.out.println("persist member");
	
	Member member2 = new Member();
	member2.setUserName("ocwokocw2");
	
	em.persist(member2);
	System.out.println("persist member2");
	
	Member member3 = new Member();
	member3.setUserName("ocwokocw3");
	
	em.persist(member3);
	System.out.println("persist member3");
	
	tx.commit();
	
	em.close();
	}

 

그리고 User 3 명을 persist 시킨다. H2 디비를 조회해보면 ID 를 명시적으로 설정하지 않았는데도, persist 시킨 엔티티들의 ID 가 자동적으로 늘어나는것을 볼 수 있다.

엔티티가 영속 상태가 되려면 식별자값이 반드시 필요하다고 했다. 그런데 IDENTITY 전략은 디비의 생성전략을 따라가고, 엔티티의 ID 값을 알려면 DB 에 Insert SQL 을 날려야 알 수 있다.

 

이전에 수행하던 예제들은 쓰기 지연 특성에 의해 commit 전에 SQL 이 한꺼번에 출력되었지만, 본 예제를 실행시켜보면 아래와 같이 persist 를 수행하자마자 곧바로 Insert SQL 들이 출력되었다.

 

Hibernate: 
    /* insert com.example.demo.member.Member
        */ insert 
        into
            member
            (id, age, create_date, description, last_modified_date, role_type, name) 
        values
            (null, ?, ?, ?, ?, ?, ?)
persist member
Hibernate: 
    /* insert com.example.demo.member.Member
        */ insert 
        into
            member
            (id, age, create_date, description, last_modified_date, role_type, name) 
        values
            (null, ?, ?, ?, ?, ?, ?)
persist member2
Hibernate: 
    /* insert com.example.demo.member.Member
        */ insert 
        into
            member
            (id, age, create_date, description, last_modified_date, role_type, name) 
        values
            (null, ?, ?, ?, ?, ?, ?)
persist member3

- SEQUENCE 전략

DB 의 시퀀스는 유일한 값을 반환한다. 그래서 어플리케이션 구축시 순번을 채번할 때, 중복을 막기 위한 방법으로 종종 이용된다. 

 

만약 SELECT MAX(SEQ) + 1 FROM SOME_TABLE 과 같은 쿼리를 이용하여 채번하면, WAS 가 여러대 일 경우 서로 다른 WAS 의 트랜잭션이 커밋되기전까지는 서로 동시에 채번로직이 진행중인지 알 수 없기 때문에 중복채번이 되는 경우가 많다. 간혹 어떤 사람은 synchronized 를 걸면 무조건 해결된다고 하는 사람도 있다. 하지만 이렇게 되면 해당 메소드가 block 되는 성능은 논외로 치더라도 block 자체가 jvm 1대 기준이라 결국 다른 was 에서 동시 수행된다. 반면 DB 에서 관리하고 있는 시퀀스 오브젝트는 결국 저장소는 하나이므 중복되지 않는 순번을 채번하는것이 가능하다.

 

만약 DB가 SEQUENCE 를 지원하고 있다면 SEQUENCE 오브젝트를 이용하여 식별자 값을 할당할 수 있다.

 

@Entity
@SequenceGenerator(name = "MEMBER_SEQ_GENERATOR",
	sequenceName = "MEMBER_SEQ",
	initialValue = 1,allocationSize = 1
)

.....

@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "MEMBER_SEQ_GENERATOR")
@Column(name = "ID")
private Long id;

 

@Entity 아래에 @SequenceGenerator 를 붙여준다. 각 속성의 의미는 아래와 같다.

  • name: 식별자 생성기 이름
  • sequenceName: DB 에 생성된 sequence 이름
  • initialValue: 처음으로 시작하는 수
  • allocationSize: 시퀀스 한 번 호출에 증사하는 수, 기본값 50
  • catalog, schema: DB catalog, schema 이름

위와 같이 설정하고 어플리케이션을 기동할 때 Log 를 살펴보면 sequence 를 생성한다.

 

Hibernate: 
    
    drop sequence if exists member_seq
14:40:53.582 [main] DEBUG org.hibernate.SQL - create sequence member_seq start with 1 increment by 1
Hibernate: create sequence member_seq start with 1 increment by 1

 

allocationSize 는 기본값이 50 인데 1 이 아닌 이유는 최적화와 관련이 있다. JPA 는 시퀀스에 접근하는 횟수를 줄이기 위해 한번에 시퀀스를 1 -> 50 까지 증가시키고, 그만큼을 메모리에 시퀀스 값을 할당한다


- TABLE 전략

키 생성 전용 테이블을 하나 만들고 DB 의 시퀀스를 흉내내는 전략이다. 이 전략의 장점은 테이블을 사용하는것이므로 모든 DB 에서 사용이 가능하다.

 

@Entity
@TableGenerator(
	name = "MEMBER_SEQ_GENERATOR"
	,table = "MY_SEQUENCE"
	,pkColumnValue = "MEMBER_SEQ", allocationSize = 1
)

.......

@Id
@GeneratedValue(strategy = GenerationType.TABLE, generator = "MEMBER_SEQ_GENERATOR")
@Column(name = "ID")
private Long id;

 

@TableGenerator 로 변경하여 속성을 위와 같이 준다. 각 속성은 아래와 같으며, 아래 4 가지 속성 말고도 꽤 여러가지 속성이 있다.

  • name: 해당 Generator 이름
  • table: 테이블 명
  • pkColumnValue: 시퀀스 컬럼명
  • allocationSize: 시퀀스 한 번 호출에 증가하는 수

@GeneratedValue 도 달라져야 하는데 strategy 를 SEQUENCE 에서 TABLE 로 변경해야 한다. generator 이름도 @TableGenerator 이름과 맞춰준다. 위와 같이 설정하고 어플리케이션 기동 Log 를 보면 아래와 같이 테이블을 생성한다.

 

Hibernate: 
    
    create table my_sequence (
       sequence_name varchar(255) not null,
        next_val bigint,
        primary key (sequence_name)
    )
20:57:11.981 [main] DEBUG org.hibernate.SQL - 
    
    insert into my_sequence(sequence_name, next_val) values ('MEMBER_SEQ',0)

................


Hibernate: 
    select
        tbl.next_val 
    from
        my_sequence tbl 
    where
        tbl.sequence_name=? for update
            
Hibernate: 
    update
        my_sequence 
    set
        next_val=?  
    where
        next_val=? 
        and sequence_name=?
persist member
Hibernate: 
    select
        tbl.next_val 
    from
        my_sequence tbl 
    where
        tbl.sequence_name=? for update
            
Hibernate: 
    update
        my_sequence 
    set
        next_val=?  
    where
        next_val=? 
        and sequence_name=?
persist member2

................

 

아까와 똑같이 Member 를 3 명 등록하는 코드이다. my_sequence 테이블을 생성한 후, MEMBER_SEQ 를 insert 한다. Member 등록시 log 를 보면 sequence 를 가져오는 쿼리를 호출하고 있음을 알 수 있다.

실제로 위와 같이 H2 서버에 테이블과 해당 Sequence 가 생김을 알 수 있다.


- AUTO

DB 종류에 따라 어떤걸 지원하는지 따져서 전략을 설정하지 않아도되는 AUTO 옵션도 있다. 방언에 따라서 전략을 자동설정하며 막무가내인 방법 같지만 의외로 장점도 있다.

 

DB 를 변경해도 코드를 수정하지 않아도 된다. 또 프로젝트 초반에 Id 생성전략이 정의되지 않았거나 프로토타입성 프로젝트를 만든다면 AUTO 로 프로젝트를 진행할수도 있다.


- 각 전략별 영속성 컨텍스트의 동작

엔티티를 영속성 컨텍스트로 영속화 시키려면 식별값이 있어야 한다고 했다. persist 호출시 각 전략별 동작은 아래와 같다.

  • 직접 할당: 프로그래머가 해당값을 직접 할당하는 코드를 작성한다.
  • SEQUENCE: DB 에서 시퀀스 값을 획득한 후 영속성 컨텍스트에 저장한다.
  • TABLE: 시퀀스 생성용 테이블에서 조회한 후 영속성 컨텍스트에 저장한다.
  • IDENTITY: 먼저 엔티티를 DB 에 저장한 후 식별자값을 획득하고, 영속성 컨텍스트에 저장한다.

- 자연 키와 대리 키

JPA 의 엔티티 기본 키와는 동떨어진 얘기긴 하지만 Key 를 어떤값으로 설정할것인가는 상당히 중요하다. 자연 키는 주민번호나 전화번호 같이 그 자체의 데이터가 의미를 가지면서 이를 Key 로 사용하는 경우이다. 반면 대리 키는 1,2,3 순번같이 비즈니스적으로 의미없는 키를 의미한다.

 

책에서 대리 키를 추천한다고 설명하고 있는데 개인적으로도 같은 의견이다. 과거에는 주민번호와 같은 필드로 키를 많이 지정했었다. 회사도 임직원마다 id 가 있는데, 중복이 절대 없을것 같지만 id 가 같은 경우가 있다. 또 id 를 변경해야하는 예외적인 사항도 발생하기 때문에 대리키를 key 로 정하면  이런 필드들이 변경되는 이슈에도 유연하게 대응할 수 있다.

댓글