본문 바로가기

Spring/JPA

JPA 기본키 할당 전략

들어가며

스프링 부트를 활용한 마이크로 서비스 개발 로 스터디를 진행하며, WAS가 여러개인 경우, JPA는 어떻게 ID를 할당해 동시성 문제를 해결하는지에 대한 질문이 나왔다. 그래서 자바 ORM 표준 JPA 프로그래밍 책과 인프런 강의 자바 ORM 표준 JPA 프로그래밍 - 기본편 을 다시 보며 정리한 내용이다.

예제 코드

@Entity
public class Member {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "ID")
    private String id;
}

기본키 할당 전략

직접 할당

  • 기본키를 애프리케이션에서 직접 할당한다.

자동 할당

  • 대리키 사용 방식
    • IDENTITY : 기본 키 생성을 데이터베이스에 위임한다.
    • SEQUENCE : 데이터베이스 시퀀스를 사용해서 기본 키를 할당한다.
    • TABLE : 키 생성 테이블을 사용한다.

IDENTITY 전략

기본키 생성을 데이터베이스에 위임하는 전략

CREATE TABLE BOARD(
    ID INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
    DATA VARCHAR(255)
);

INSERT INTO BOARD(DATA) VALUES('A');
INSERT INTO BOARD(DATA) VALUES('B');

데이터베이스에 값을 저장할 때 ID 칼럼을 비워두면 데이터베이스가 순서대로 값을 채워준다.

IDENTITY 전략은 데이터베이스에 값을 저장하고 나서야 기본 키 값을 구할 수 있을때 사용한다. 위 예제처럼 식별자(ID)가 생성되는 경우에는 @GeneratedValue 애너테이션을 사용하고 식별자 생성전략을 선택해야 한다.

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

예제

private static void logic (EntityManager em){
    Board board = new Board();
    em.persist(board);  // 데이터베이스에 저장됨

    System.out.println("board.id : " + board.getId());
}

em.persist() 를 호출하면 Board 엔티티를 저장한 직후에 할당된 식별자 값을 출력한다.

엔티티가 영속 상태가 되려면, 식별자가 반드시 필요하다. em.persist() 호출하는 즉시 INSERT SQL이 데이터페이스에 전달된다. 따라서 IDENTITY 전략은 트랜잭션을 지원하는 쓰기 지연이 동작하지 않는다.

강사 : 쓰기 지연전략이 한 트랜잭션 내에서는 크게 메리트가 있는건 아니다. 트랜잭션을 막 자르면 문제가 크다. 한 트랜잭션 안에서 INSERT 쿼리를 여러번 날린다고 성능이 비약적으로 좋아지지는 않았다.

SEQUENCE 전략

데이터베이스 시퀀스는 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트. 이 시퀀스를 사용해서 기본 키를 생성한다.

Sequence 전략을 사용하는 데이터베이스

  • 오라클, PostgreSQL, DB2, H2 등
CREATE TABLE BOARD(
    ID BIGINT NOT NULL PRIMARY KEY,
    DATA VARCHAR(255)
);

CREATE SEQUENCE BOARD_SEQ START WITH 1 INCREMENT BY 1;

예제

@Entity
// 시퀀스 생성기 등록
@SequenceGenerator(
    name = "BOARD_SEQ_GENERATOR",
    sequenceName = "BOARD_SEQ",
    initialValue = 1, allocationSize = 1
)
public class Board {
    @Id
    //키 생성전략 설정
    @GenaratedValue(
        strategy = GenerationType.SEQUENCE,
        generator = "BOARD_SEQ_GENERATOR"
    )
    private Long id;
}

em.persist() 를 호출하면 우선 데이터베이스 시퀀스를 사용해서 식별자를 조회한다. 그리고 조회한 식별자를 엔티티에 할당한 후에 엔티티를 영속성 컨텍스트에 저장한다. 이후 트랜잭션을 커밋해서 플러시가 일어나면 엔티티를 데이터베이스에 저장한다.

private static void logic (EntityManager em){
    Board board = new Board();

    System.out.println("==================")
    em.persist(board);  // Sequence를 가져온다.
    System.out.println("==================")

    em.flush();
}

em.persist(board); 호출하면 식별자로 사용할 Sequence 를 요청한다.

Hibernate:
    call next value for BOARD_SEQ

em.persiste() 를 호출하면 DB의 SEQUENCE 을 호출해 식별자를 받아온다. INSERT 쿼리를 여러번 호출하면 성능상 문제가 있을 수 있다. 그래서 식별자를 일정 개수만큼 미리 확보해서 사용할 수 있다.

@SequenceGenerator(
    name = "BOARD_SEQ_GENERATOR",
    sequenceName = "BOARD_SEQ",
    initialValue = 1, allocationSize = 50
)
  • sequence 값의 초기값은 1이고, 식별자를 50개씩 확보한다.
private static void logic (EntityManager em){
    Board board1 = new Board();
    Board board2 = new Board();
    Board board3 = new Board();

    System.out.println("==================")
    em.persist(board1);  // 1, 51
    em.persist(board2);  // MEM
    em.persist(board3);  // MEM
    System.out.println("==================")

    em.flush();
}

위 코드를 실행하면 아래와 같이 나온다.

create sequence BOARD_SEQ start with 1 increment by 50

call next value for BOARD_SEQ  //1로 맞춘다
call next value for BOARD_SEQ  //50개를 미리 확보한다.

WAS를 종료하면 미리 확보한 것은 어떻게 되는가?

  • 메모리에서 사라진다.
  • 1~30 까지 사용하고 WAS를 종료하면 31 ~ 50 은 사라진다.
  • 식별자에 구멍이 생기므로, 적당한 개수만큼 미리 확보하는게 중요하다.

식별자를 미리 확보하면, 여러 서버에서 각각 호출해도 식별자를 각각 확보하기 때문에 동시성 이슈는 해결할 수 있다.