JPA : PK 생성 전략

    이 글은 자바 ORM 표준 JPA 프로그래밍을 학습하며 필요한 내용을 작성한 글입니다. 

     

    @Id

    해당 어노테이션을 특정 필드 위에 사용하면 이 필드는 앞으로 DB에서 PK로 사용하겠다는 것을 의미한다. DB 또한 그렇게 인식을 한다. 이 때, @Id만 입력이 된다면 필드에는 개발자가 값을 직접 채번해야한다. 

     

    @GeneratedValue

    @Id + @GenerateValue를 사용할 경우, PK를 개발자가 채번하는 것이 아니라 자동으로 DB에서 채번해주는 것을 의미한다. 따라서 채번의 부담성을 줄이기 위해 @GeneratedValue를 사용하는 것이 권장된다. 

     

    @GeneratedValue 전략

    public enum GenerationType {
        AUTO
        IDENTITY, 
        SEQUENCE, 
        TABLE, 
    }

    GenerationType은 총 4가지가 존재한다.

    • AUTO(Default) : IDENTITY / SEQUENCE / TABLE 중 DB가 권장하는 것으로 자동으로 선택한다.
    • INSERT : 기본 키 생성을 DB에 위임한다. MY SQL의 Auto_increment 같은 기능이다. 
    • SEQUENCE : DB의 시퀀스를 이용해 기본 키를 채번한다. 
    • TABLE : 테이블을 직접 하나 만들어서 채번한다. 

    보통 개발 단계에서는 채번 방식이 정해져 있지 않기 때문에 Auto를 사용한다. AUTO로 설정하게 되면 개발 단계에서 DB가 바뀌게 되어도 큰 문제가 없기 때문에 좋은 방식이 될 수 있다. 이후에 채번 방식이 정해지면 어노테이션에 어떤 전략을 사용하는지 값을 명시해준다. 

     

    채번 방식에 따라 JPA는 다르게 동작한다. 

    JPA는 영속성 컨텍스트를 이용한다. 영속성 컨텍스트에서 엔티티는 PK 값을 기준으로 관리가 된다. 따라서 위의 자동 키 생성 전략에 따라서 PK 값을 얻어오는 방식이 달라질 것이다. 이런 이유 때문에 채번 방식이 어떻게 되느냐에 따라 JPA는 전혀 다르게 동작한다. 

     

    IDENTITY 전략

    IDENTITY 전략은 DB에 데이터를 밀어넣는 순간 DB가 PK 값을 알려준다. 따라서 PK 생성 전략을 IDENTITY로 할 경우, em.persist()에 대해서는 "쓰기 지연 쿼리"가 실행되지 않는다. 무슨 말이냐면, em.persist()를 하는 순간 실제로 INSERT 쿼리가 DB로 나간다. DB에 값이 들어가는 순간, PK 값이 생기는데 JPA는 이 PK값을 받아서 엔티티를 영속화한다. 

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

    다음 형식의 엔티티가 있다고 가정하자.

    @Test
    void test0() {
        Member member = new Member();
        em.persist(member);
        System.out.println("===== HERE ====");
        em.flush();
    }

    그리고 위 테스트 코드를 수행한다고 가정해보자.

    실제 수행 결과를 보면 위와 같다. 원래대로라면 "===== HERE ====" 뒷쪽에 em.flush()에서 INSERT 쿼리가 나갔어야 한다. 그렇지만 IDENTITY 전략에서는 em.persist()를 하는 순간 바로 INSERT 쿼리가 나가는 것을 알 수 있다. 

     

    IDENTITY 전략의 문제

    IDENTITY 전략은 JPA의 Bulk Insert에 문제를 가져온다. JPA는 DB 설정해 쿼리 파라미터로 Bulk Insert를 할 수 있는 기능을 지원한다. 그렇지만 @GeneratedValue의 전략이 IDENTITY인 경우에는 Bulk Insert 기능은 자동으로 사용되지 않는다. 이유는 IDENTITY의 동작 특성 때문이다.

    JPA는 영속성 컨텍스트에 넣은 것을 쓰기 지연 저장소에 저장한 다음에 나중에 Bulk Insert로 집어넣는다. 그런데 영속성 컨텍스트에 영속화 하기 위해서는 PK 값이 필요하다. IDENTITY 전략은 DB에 저장하는 순간 PK 값을 알 수 있기 때문에 Bulk Insert가 자동으로 비활성화되게 된다. 

    따라서 채번 방식을 IDENTITY 전략으로 가져갈 경우 Bulk Insert를 위해서는 JdbcTemplate, Spring Batch 등을 이용해 직접 채번해서 넣을 수 있다. 

     

    JdbcTemplate으로 IDENTITY 전략 약점 극복

    @Transactional
    @RequiredArgsConstructor
    @Component
    public class testClass {
    
        private final JdbcTemplate jdbcTemplate;
    
        public void test1() {
            jdbcTemplate.batchUpdate("insert into member (member_id,name) values (?,?)", new BatchPreparedStatementSetter() {
                @Override
                public void setValues(PreparedStatement ps, int i) throws SQLException {
                    ps.setLong(1, Long.valueOf(i));
                    ps.setString(2, "abc" + i);
                }
    
                @Override
                public int getBatchSize() {
                    return 100;
                }
            });
        }
    }

    다음과 같이 JdbcTemplate의 BulkUpdate를 이용하면 직접 채번한 엔티티를 넣을 수 있다. 이 때 문제점은 엔티티 채번을 어떻게 유니크하게 할 것이냐가 되겠다.

    암튼 위의 쿼리를 실제로 실행시켜보면, 다음과 같이 Bulk Insert로 들어간 것을 확인할 수 있다.

     

     

    SEQUENCE 전략

    DB Sequence는 DB에서 사용하는 유일한 값을 순서대로 생성하는 특별한 DB 객체다. SEQUENCE 전략을 사용할 경우, 이 DB Sequnece를 이용해서 기본 키를 생성한다. DB Sequnce를 사용할 때, 미리 DB에서 일정한 양의 PK 값을 불러와서 메모리에 저장해두고 사용한다. 

    SEQUENCE 전략은 PK 값이 메모리 상에 이미 존재하기 때문에 em.persist()를 했을 때 SEQUENCE 값을 참조해서 PK 값을 배정하고, 그것을 영속성 컨텍스트에 영속화 시킨다. 아래 코드에서 확인할 수 있다. 

    @Entity
    @Data
    public class Member {
        @Id
        @GeneratedValue(strategy = GenerationType.SEQUENCE)
        @Column(name = "member_id")
        private Long id;
        private String name;
    }

    먼저 다음과 같이 엔티티를 설정해준다. 

    @Test
    void test0() {
        Member member = new Member();
        em.persist(member);
        System.out.println("===== HERE ====");
        em.flush();
    }

    다음 테스트 코드를 실행한다.

    실행 결과는 다음과 같다. 

    1. 영속화를 할 때, HIBERNATE_SEQUENCE에서 값을 불러온다. 그리고 영속화를 한다. 이 때, DB로 쿼리가 나가지 않는다.
    2. em.flush()를 하는 순간 INSERT 쿼리가 나간다. 

    즉, SEQUNECE는 INSERT를 할 때 메모리 상에 있는 값을 바로 가져와서 PK 값으로 만들고 영속화를 하기 때문에 쓰기 지연 쿼리가 정상적으로 동작하는 것을 알 수 있다. 

     

    SEQUENCE 전략 단점

    SEQUENCE 전략은 한 가지 큰 단점이 존재한다. 보통 DB SEQUENCE에서 값을 떼와서 메모리에 저장하고, 그 값을 읽는 형식으로 처리가 된다. 여기서 DB SEQUENCE에서 값을 1개씩만 가져올 때 문제가 된다. 그러면 엔티티는 매번 새로운 SEQ가 필요하기 때문에 매번 DB에서 새로운 값을 불러온다.

     

    이럴 경우 IDENTITY 전략보다 더 큰 문제가 된다. 왜냐하면 SEQUENCE 1개를 불러오는데 2개의 쿼리가 나가기 때문이다. 

    1. DB SEQUNECE의 값을 SELECT로 불러온다. → 1을 불러온다
    2. DB SEQUNECE의 값을 UPDATE 쿼리를 날린다. → DB SEQUENCE 값을 2로 올린다. 

    즉, 매번 두 번의 쿼리를 날려야 하기 때문에 큰 문제가 된다.

     

    이 때 해결할 수 있는 방식은 BATCH 채번이 있다. @SequenceGenerator를 이용하면, 직접 DB SEQ 객체를 만들 수 있다. 여기서 allocationSize라는 값을 설정할 수 있다. 이 값은 DB에서 한번에 불러올 SEQ의 값을 의미한다. AllocationSize를 500으로 설정할 경우, 메모리에 저장된 값을 통해 DB 접근 없이  0~500번까지 채번을 할 수 있게 된다. 그리고 DB의 SEQ 값은 501을 가리키고 있다. 이런 식으로 AllocationSize를 통해 Batch 채번이 가능하다.

     

    한 가지 문제점은 이렇게 사용하다가 서버가 꺼지게 되면, PK 값 중간에 구멍이 난다는 것이다. 예를 들어 250번까지 채번한 다음에 서버가 꺼졌다면, 251 ~ 500번의 PK 값은 영원히 채워지지 못하는 숙제가 된다. 

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

    OneToMany에서 CascadeType.ALL의 N+1 문제 야기  (2) 2023.11.18
    JPA : JPQL과 영속성 컨텍스트  (0) 2022.02.23
    JPA : Bulk 연산의 주의할 점  (0) 2022.02.23
    JPA : Collection과 JPA 동작 방식  (1) 2022.02.23
    JPA : 2차 캐시  (0) 2022.02.23

    댓글

    Designed by JB FACTORY