본문 바로가기

Programming/JPA

[JPA] 기본 키 매핑

반응형

기본 키 매핑 어노테이션

  • @Id
  • @GeneratedValue

기본 키 매핑 방법

  • 직접 할당: @Id만 사용.
  • 자동 생성: @GeneratedValue 사용.

직접 할당이 아닌 값을 생성해서 사용하고 싶다면 @GeneratedValue 어노테이션의 strategy 속성을 추가하면 됩니다.
생성 전략은 네 가지가 있습니다.

 

 

자동 생성 전략 - IDENTITY

  • 기본 키 생성을 데이터베이스에 위임합니다.
  • 주로 MySQL, PostgreSQL, SQL Server, DB2에서 사용합니다.
    • ex) MySQL의 AUTO_INCREMENT
  • JPA는 보통 트랜잭션 커밋 시점에 INSERT SQL 실행합니다.
  • AUTO_INCREMENT는 데이터베이스에 INSERT SQL을 실행한 이후에 ID값을 알 수 있습니다.
  • IDENTITY 전략은 EntityManager.persist() 시점에 즉시 INSERT SQL을 실행하고 DB에서 식별자를 조회합니다.

엔티티 설정

 

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

 

실행 결과(H2 데이터베이스)

 

create table Member (
    id bigint generated by default as identity,
    name varchar(255),
    primary key (id)
)

 

데이터베이스 방언을 MySQL로 변경한 후 실행하면 쿼리문이 다음과 같이 변경됩니다.

 

create table Member (
    id bigint not null auto_increment,
    name varchar(255),
    primary key (id)
) engine=MyISAM

 

이렇게 설정해서 테이블이 생성되면 INSERT SQL이 실행될 때마다 PK값이 자동으로 증가하는 것을 확인할 수 있습니다.

 

public class JpaMain {

    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

        EntityManager entityManager = emf.createEntityManager();

        EntityTransaction tx = entityManager.getTransaction();

        tx.begin();

        try {
            Member member = new Member();
            member.setName("Kim");
            entityManager.persist(member);

            // 트랜잭션 커밋 시점에 쿼리문 실행됨
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            entityManager.close();
        }
        emf.close();

    }
}

 

위 코드를 두 번 실행한 결과는 아래와 같습니다.
PK 컬럼으로 매핑된 id가 자동으로 증가하는 것을 확인할 수 있습니다.

 

 

NOTE

IDENTITY 생성 전략은 PK값을 설정하지 않고(null) INSERT SQL을 던지면 그 때 PK의 값을 세팅합니다. 즉 쿼리문 실행 이후에 PK값을 알 수 있습니다.
일반적인 JPA 동작 방식은 영속성 컨텍스트에 엔티티 정보를 가지고 있다가 요청이 들어오면 DB에 쿼리문을 날리는 방식으로 동작합니다. 그런데 IDENTITY 생성 전략에 따르면 쿼리문 실행 이후 PK가 생성됩니다. 여기서 논리적인 오류가 발생합니다. 영속성 컨텍스트에서 객체가 관리되려면 무조건 PK값이 있어야 하는데 IDENTITY 생성 전략은 그렇게 동작하지 않기 때문입니다.

그래서 IDENTITY 전략에서만 예외적으로 EntityManager.persist()가 호출되는 시점에서 바로 DB에 INSERT 쿼리문을 날립니다.(일반적으로 트랜잭션 커밋 시점에 쿼리문을 날립니다. 이미 PK값을 알고 있기 때문에 그렇게 동작할 수 있습니다.) persist()가 호출되는 즉시 INSERT 쿼리를 통해 DB에서 식별자를 조회해서 영속성 컨텍스트의 1차 캐시에 값을 넣습니다.

 

자동 생성 전략 - SEQUENCE

  • 데이터베이스 시퀀스는 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트 입니다.
  • 오라클, PostgreSQL, DB2, H2 데이터베이스에서 사용합니다.

생성 전략을 SEQUENCE로 변경 후 INSERT SQL을 실행해봅니다.

 

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

 

실행 결과는 다음과 같습니다.(H2 데이터베이스 기준)
먼저 시퀀스를 생성한 후 INSERT SQL을 작성할 때 시퀀스를 호출해서 값을 가져오고, 그 값을 PK에 넣는 것을 확인할 수 있습니다.

 

Hibernate: create sequence hibernate_sequence start with 1 increment by 1
Hibernate: 
    
    create table Member (
       id bigint not null,
        name varchar(255),
        primary key (id)
    )

Hibernate: 
    call next value for hibernate_sequence
Hibernate: 
    /* insert hellojpa.Member
        */ insert 
        into
            Member
            (name, id) 
        values
            (?, ?)

 

@GeneratedValue(strategy = GenerationType.SEQUENCE)만 작성하면 Hibernate가 만드는 기본 시퀀스 hibernate_sequence를 만들어서 사용합니다.


만약 테이블마다 시퀀스 오브젝트를 따로 관리하고 싶다면 @SequenceGenerator의 sequenceName 속성을 통해 커스터마이징 할 수 있습니다.

 

먼저 클래스 레벨에서 @SequenceGenerator 어노테이션 및 속성을 추가한 후 @GeneratedValue의 generator 속성에 @SequenceGenerator에서 설정한 시퀀스 이름을 삽입하면 됩니다.

 

@Entity
@SequenceGenerator(
        name = "MEMBER_SEQUENCE",
        sequenceName = "MEMBER_SEQ",
        initialValue = 1,
        allocationSize = 1)
public class Member {

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

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

 

위 설정 내용을 토대로 다시 코드를 실행합니다.
@SequenceGenerator에서 설정한 내용대로 등록한 시퀀스가 생성 및 작동되는 것을 확인할 수 있습니다.

 
Hibernate: create sequence MEMBER_SEQ start with 1 increment by 1
Hibernate: 
    
    create table Member (
       id bigint not null,
        name varchar(255),
        primary key (id)
    )

Hibernate: 
    call next value for MEMBER_SEQ
Hibernate: 
    /* insert hellojpa.Member
        */ insert 
        into
            Member
            (name, id) 
        values
            (?, ?)

 

@SequenceGenerator 속성

 

NOTE - SEQUENCE 전략의 세부 동작 방식

아래 코드는 위에서 부분적으로 설명한 엔티티 클래스와 실행 클래스를 한 곳에 모아 놓은 것입니다.
코드를 통해 SEQUENCE 전략의 동작 방식을 알아봅니다.

 

// 엔티티 클래스
@Entity
@SequenceGenerator(
        name = "MEMBER_SEQUENCE",
        sequenceName = "MEMBER_SEQ",
        initialValue = 1,
        allocationSize = 1)
public class Member {

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

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

// 실행 클래스
public class JpaMain {

    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

        EntityManager entityManager = emf.createEntityManager();

        EntityTransaction tx = entityManager.getTransaction();

        tx.begin();

        try {
            Member member = new Member();
            member.setName("Kim");

            System.out.println("========== persist() ==========");
            entityManager.persist(member);
            System.out.println("member.id = " + member.getId());
            System.out.println("========== persist() ==========");

            System.out.println();

            System.out.println("========== tx.commit() ==========");
            tx.commit();
            System.out.println("========== tx.commit() ==========");
        } catch (Exception e) {
            tx.rollback();
        } finally {
            entityManager.close();
        }
        emf.close();

    }
}

 

영속성 컨텍스트에서 엔티티를 관리하려면 PK값을 반드시 알고 있어야 합니다. 그리고 PK값은 @SequenceGenerator를 통해 DB에 생성된 MEMBER_SEQ(name 속성에 등록된 시퀀스 이름) 시퀀스에서 가져옵니다. 즉 영속성 컨텍스트 등록 이전에 먼저 DB의 시퀀스에서 해당 객체의 PK값을 가져오는 과정이 필요합니다.

 

그래서 JPA는 먼저 시퀀스에서 값을 가져온 다음 영속성 컨텍스트에 등록하는 방식으로 동작합니다. 명확한 이해를 위해 실행 로그를 확인해보겠습니다.

 

========== persist() ==========
Hibernate: 
    call next value for MEMBER_SEQ
member.id = 1
========== persist() ==========

========== tx.commit() ==========
Hibernate: 
    /* insert hellojpa.Member
        */ insert 
        into
            Member
            (name, id) 
        values
            (?, ?)
========== tx.commit() ==========

 

영속성 컨텍스트에 엔티티를 등록하는 persist() 메소드 호출 시 DB의 MEMBER_SEQ 시퀀스에서 PK로 사용할 값을 추출합니다. 그 다음에 영속성 컨텍스트에 엔티티를 저장합니다.
그리고 트랜잭션 커밋 시점에 INSERT 쿼리문을 던져서 DB 테이블에 등록합니다.

 

IDENTITY 전략에서는 persist() 호출 시점에 INSERT 쿼리문을 날리기 때문에 버퍼링이 불가능합니다. INSERT 쿼리문을 날려야 PK값을 알 수 있기 때문에 이렇게 동작합니다.


하지만 SEQUENCE 전략에서는 필요한 경우 요청을 모아 두었다가 한 번에 write 하는 방식의 버퍼링이 가능합니다.

NOTE - SEQUENCE 전략의 동작 방식은 성능 저하를 가져올 수 있다?

SEQUENCE 전략의 동작 방식은 어쨌든 네트워크를 계속 왔다 갔다 하면서 매번 PK값을 가져와서 처리하는 방식입니다. 이것이 반복되면 성능 저하 이슈가 있을 수 있습니다.


이 문제를 해결하기 위한 방법은 @SequenceGenerator의 allocationSize 속성값을 수정하는 것입니다.

 

아래 이미지를 보시면 allocationSize 속성의 기본값은 50 입니다. 숫자의 의미는, 생성하는 시퀀스 값입니다. 50으로 설정 후 코드를 실행하면 시퀀스에서 미리 PK로 사용할 값 50개를 한 번에 가져옵니다.(DB에서는 51로 세팅됨) 이렇게 미리 가져온 50개를 하나씩 사용하다가 50개가 모두 사용되면 다시 DB의 시퀀스에서 50개를 가져옵니다. 이번에는 50번 부터 100번까지 입니다. 참고로 DB에서는 시퀀스가 101로 세팅됩니다.

 

 

명확한 이해를 위해 복수의 값을 삽입하는 코드를 작성합니다.

 

// 엔티티 클래스
@Entity
@SequenceGenerator(
        name = "MEMBER_SEQUENCE",
        sequenceName = "MEMBER_SEQ",
        initialValue = 1,
        allocationSize = 50)
public class Member {

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

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

// 실행 클래스
public class JpaMain {

    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

        EntityManager entityManager = emf.createEntityManager();

        EntityTransaction tx = entityManager.getTransaction();

        tx.begin();

        try {
            Member member1 = new Member();
            member1.setName("A");

            Member member2 = new Member();
            member2.setName("B");

            Member member3 = new Member();
            member3.setName("C");

            System.out.println("========== persist() ==========");

            entityManager.persist(member1);
            entityManager.persist(member2);
            entityManager.persist(member3);

            System.out.println("member.id = " + member1.getId());
            System.out.println("member.id = " + member2.getId());
            System.out.println("member.id = " + member3.getId());

            System.out.println("========== persist() ==========");

            System.out.println();

            System.out.println("========== tx.commit() ==========");
            tx.commit();
            System.out.println("========== tx.commit() ==========");
        } catch (Exception e) {
            tx.rollback();
        } finally {
            entityManager.close();
        }
        emf.close();

    }
}

 

위 코드의 실행 로그는 아래와 같습니다.


persist() 시점에서 DB의 MEMBER_SEQ 시퀀스에서 값을 가져오는 쿼리인 call next value for MEMBER_SEQ 쿼리문을 두 번 날리는 것이 보입니다. 처음 호출되는 쿼리문의 결과는 1 입니다. 1부터 시퀀스가 시작하도록 설정했으니 1의 결과가 나옵니다. 두 번째 쿼리문의 결과는 51 입니다 SequenceGenerator 설정에서 1부터 시작하고 50씩 증가하도록 설정했으니 51이라는 결과가 나온 것입니다. 두 번의 쿼리문 실행을 통해 DB의 MEMBER_SEQ 시퀀스 값이 51이 되었습니다. 시퀀스 값 50개를 확보하기 위해 일부러 쿼리문을 두 번 실행한 것입니다. 그리고 미리 당겨온 50개의 시퀀스값은 메모리에 저장됩니다.

 

이제 member1 이후의 동작에서는 네트워크에 접속하지 않고 메모리에서 미리 당겨온 시퀀스값을 사용합니다.

 

========== persist() ==========
Hibernate: 
    call next value for MEMBER_SEQ
Hibernate: 
    call next value for MEMBER_SEQ
member.id = 1
member.id = 2
member.id = 3
========== persist() ==========

========== tx.commit() ==========
Hibernate: 
    /* insert hellojpa.Member
        */ insert 
        into
            Member
            (name, id) 
        values
            (?, ?)
Hibernate: 
    /* insert hellojpa.Member
        */ insert 
        into
            Member
            (name, id) 
        values
            (?, ?)
Hibernate: 
    /* insert hellojpa.Member
        */ insert 
        into
            Member
            (name, id) 
        values
            (?, ?)
========== tx.commit() ==========

 

단순하게 보면 allocationSize 설정값이 크면 클수록 좋아 보입니다. 한 9999개 미리 당겨와서 사용하면 굉장히 좋을 것 같습니다.
하지만 사용하다가 웹 서버가 내려가는 등의 공백이 생기면 사용지 않은 시퀀스 값이 날아가 버리는 문제가 발생합니다. 그래서 적당한 50-100 정도를 추천합니다.

자동 생성 전략 - TABLE

  • 키 생성 전용 테이블을 하나 만들어서 데이터베이스 시퀀스를 흉내내는 전략
  • @TableGenerator가 필요
  • 모든 데이터베이스 적용 가능하지만, 최적화 되지 않은 테이블을 직접 사용하기 때문에 성능상의 이슈가 있음. 운영 서버에서 사용하기에 적합하지 않다.
@Entity
@TableGenerator(
        name = "MEMBER_SEQUENCE",
        table = "MY_SEQUENCES",
        pkColumnValue = "MEMBER_SEQ",
        allocationSize = 1)
public class Member {

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

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

 

실행 결과 로그를 보면 시퀀스 테이블을 생성하는 것을 확인할 수 있습니다. 생성된 시퀀스 테이블을 통해 시퀀스가 동작합니다.

 

Hibernate: 
    
    create table Member (
       id bigint not null,
        name varchar(255),
        primary key (id)
    )
Hibernate: 
    
    create table MY_SEQUENCES (
       sequence_name varchar(255) not null,
        next_val bigint,
        primary key (sequence_name)
    )

 

@TableGenerator 속성 입니다.

 

 

NOTE

TABLE 전략도 SEQUENCE 전략과 마찬가지로 allocationSize 속성값 조정을 통해 시퀀스값을 미리 당겨와서 메모리에 저장한 다음 사용하는 방식의 구현이 가능합니다.

자동 생성 전략 - AUTO

  • 데이터베이스 방언에 따라 IDENTITY, SEQUENCE, TABLE 전략을 자동으로 선택합니다.

권장하는 식별자 전략

  • 기본 키는 null이 아니고, 유일하고, 변하면 안됩니다. 이 조건을 만족하는 자연키(natural key / 주민번호, 지역번호 등 비즈니스와 관련된 키)는 찾기 어렵습니다.
  • 그래서 비즈니스와 연관성이 없는 대리키(대체키) 사용을 추천합니다.
  • PK로 주민번호를 사용하고 있는데 정책 변화로 주민번호 사용이 금지된 상황이 발생했다. 이 경우 해당 테이블 뿐만이 아니라 PK를 FK로 JOIN하고 있는 다른 테이블에도 문제가 발생한다. 이 경우 주빈번호를 FK로 참조하고 있는 모든 테이블을 마이그레이션해야 하는 문제가 발생하기 때문에 자연키보단 대체키 사용을 추천한다.
  • 권장 포맷은 Long형(큰 범위의 수를 포용하기 위해서) + 대체키 + 키 생성전략의 조합을 사용한 것입니다.

참고

반응형