(Batch Insert) JPA IDENTITY 기본키 생성 한계 극복 시도 - MySQL

Jang990·2024년 9월 4일
0

Insert 성능 개선

목록 보기
3/3

다시 Batch Insert로

앞서 본 글을 통해서 이제 기본키 생성에 IDENTITY 방식을 사용하면 Hibernate가 JDBC 수준에서 batch insert를 비활성화한다고 나와있는 이유가 어느정도 이해가 되실겁니다.

앞의 글 정리

  1. 기본적으로 JPA는 저장하면 영속성 컨텍스트에 엔티티를 저장해야 한다.
  2. 영속성 컨텍스트에 저장하려면 식별자(ID)가 있어야 한다.
  3. MySQL의 경우 Auto-Increment를 사용하면서 시퀀스처럼 ID를 가져올 수 없다.
  4. 단 건의 Insert쿼리를 여러번 보내자...

Table 생성 전략을 사용하면 되는거 아닌가?

MySQL을 사용할 때 IDENTITY를 쓰면 Batch Insert가 안되고 Sequence도 없으니 그냥 Table을 사용한다는 가장 간단해보이는 해결 방법입니다.

한 번 적용해봅시다.

MySQL을 사용할 때 시퀀스 방식을 사용한다면 시퀀스가 없기 때문에 테이블 방식이 적용됩니다.

@Entity
public class SequenceEntity {
    @Id @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;
    private int something;
    public SequenceEntity(int something) { this.something = something; }
}

Batch Insert를 적용하려면 batch_size를 설정해줘야 합니다.
몇개씩 묶어서 Insert 쿼리를 날릴 것인지 설정해주는 것입니다.
만약 5로 설정했다면 100개를 5개씩 나눠서 20번의 Insert 쿼리가 발생하게 됩니다.

spring:
    datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/test_c?rewriteBatchedStatements=true # 꼭 설정
        hikari:
            username: root
            password: 1234
    jpa:
        open-in-view: false
        hibernate:
            ddl-auto: update
        show-sql: true
        properties:
            hibernate:
                jdbc.batch_size: 5 # 꼭 설정
                format_sql: true
                use_sql_comments: true

saveAll을 호출하면 다음과 같은 로그를 확인할 수 있습니다.

Hibernate:
	select next_val as id_val from sequence_entity_seq for update
Hibernate:
	update sequence_entity_seq set next_val= ? where next_val=?
    
Hibernate: 
    /* insert for com.tutorial.java.batch.entity.SequenceEntity */
    insert into sequence_entity (something, id) values (?, ?)
Hibernate: 
    /* insert for com.tutorial.java.batch.entity.SequenceEntity */
    insert into sequence_entity (something, id) values (?, ?)

테스트 코드에서 saveAll을 호출한 경우

테이블 방식은 DB에게 기본키 생성을 위임하지 않기 때문에 쓰기지연이 적용됩니다.
쓰기지연이 적용되지만 테스트는 기본적으로 롤백되기 때문에 Insert 쿼리를 확인할 수 없습니다.
EntityManger에서 명시적으로 flush를 호출하거나 롤백을 해제해줘야 insert 쿼리를 콘솔로 확인할 수 있습니다.

왜 insert 쿼리가 여러번 나가죠?

스프링 콘솔에는 insert 쿼리가 여러번 나간 것으로 보이지만
DB 로그를 확인해보면 Batch Insert된 로그를 확인할 수 있습니다.

-- db 로그
select next_val as id_val from sequence_entity_seq for update
update sequence_entity_seq set next_val= 351 where next_val=301
insert into sequence_entity (something,id) values (0,302),(1,303),(2,304),(3,305),(4,306)

결론 : 잠금 때문에 사용하지 않는게 좋다.

DB 로그를 하나씩 확인해보면 Select For Update로 ID가 될 데이터인 next_val에 락을 건 것을 확인할 수 있습니다.

이 말은 결국 Insert가 진행된 트랜잭션에서 모든 작업을 커밋하거나 롤백을 해서 나오기 전에,
다른 트랜잭션에서는 Insert를 진행할 수 없다는 것을 의미합니다.

Batch Insert의 성능을 개선하기 위해 테이블을 사용했지만
다른 트랜잭션에서는 Insert 작업 자체가 불가능해졌습니다.
이런 락으로 인한 성능 저하 때문에 사용하지 않는 것을 권장하고 있습니다.

더 자세한 성능 비교와 이유들을 확인하고 싶다면 다음 글을 참고해주세요.
https://vladmihalcea.com/why-you-should-never-use-the-table-identifier-generator-with-jpa-and-hibernate/



Auto-Increment도 시퀀스처럼 사용하면 되지 않을까?

실제로 auto_increment_increment를 변경해서 auto_increment의 증가값을 바꿀 수 있습니다.
그럼 이 방식으로 시퀀스처럼 사용하면 되지 않을까요?

MySQL의 Auto-Increment와 오라클의 시퀀스의 차이점은 시퀀스는 테이블과 독립적으로 사용할 수 있지만,
Auto-Incrementauto_increment_increment를 변경하면 모든 테이블에 적용된다는 것입니다.

결론 : 적절한 사용처가 아니다.

auto_increment_increment는 언제 변경할까요?

대게 auto_increment_increment다중 DB 서버를 사용하는 경우 변경합니다.
그래서 auto_increment_incrementauuto_increment_increment와 함께 사용됩니다.

auto_increment_increment : AUTO_INCREMENT 열에서 증가 값의 크기(이하 증가값)
auto_increment_offset : AUTO_INCREMENT 열의 시작 지점(이하 시작지점)

예를들어 2개의 MySQL 쓰기 서버 A, B를 뒀다고 가정해봅시다.
A,B 서버를 증가값를 2로 설정하고,
A, B서버의 시작지점을 각각 1과 2로 설정합니다.
그러면 A서버는 홀수(1,3,5, ..), B서버는 짝수(2,4,6, ..)를 ID로 사용합니다.
이렇게 A서버와 B서버의 ID값 충돌이 발생하지 않도록 만들 수 있습니다.

하지만 여기서 Batch-Insert를 위해서 A서버에 증가값를 임의로 변경한다면 충돌이 발생할 수 있습니다.

Auto-Increment는 시퀀스와 다르게 모든 테이블에 적용되기 때문에, 특히나 이렇게 여러 DB 서버를 두었을 때 증가값를 변경하는 것은 적절하지 않을 수 있습니다.


결국 JdbcTemplate인가?

JdbcTemplate을 통해 네이티브 쿼리를 작성하는 방식은 안좋은 점이 많습니다.
예를 들어 컬럼명이 변경된다면 쿼리도 함께 수정해주어야 합니다.
또한 SQL을 문자열로 작성하는 방식이다보니 오류 여부를 컴파일러로 체크할 수 없습니다.

MySQL을 사용하는 개발자분들은 모두 이런 Batch Insert 문제를 겪었습니다.
그래서 이것을 해결하기 위해 나온 QueryDSL-EntityQL를 사용하면 됩니다.

QueryDSL-EntityQL를 사용하고 싶다면 참고하기: https://jojoldu.tistory.com/558

하지만 이 방식은 사전 설정이 좀 복잡합니다.
그래서 위 글에서도 간단한 Batch Insert가 필요하다면 JdbcTemplate을 사용하고,
JdbcTemplate을 사용한 Bulk Insert 구현의 어지러움을 느끼는 분이라면 과하지만 한번만 설정하면 이후엔 운영하기가 너무 쉬운 EntityQL을 한번 고민해보는 것이 좋다고 말하고 있습니다.


번외) DB 로그 확인하기

역시 눈으로 직접 확인해보는게 좋겠죠?
JdbcTemplate을 사용했을 때 BatchInsert가 발생하는지 DB 로그를 활성화해서 직접 확인해보겠습니다.

간단한 확인을 위해 테스트용 엔티티를 만들었습니다.

@Entity
@Data
public class IdentityEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private int something;

    public IdentityEntity(int something) {
        this.something = something;
    }
}

로그 확인 방법

쿼리 로그를 활성화하고 로그가 저장될 위치에 파일을 확인합시다.

-- 로그 저장될 위치 확인하기
SHOW VARIABLES LIKE 'datadir'; # 로그 위치
SHOW VARIABLES LIKE 'general_log_file'; # 로그 파일 이름

-- 로그 활성화 하기
SHOW VARIABLES LIKE "general_log%"; // 쿼리 로그 활성화 여부 파악
SET GLOBAL general_log = 'ON'; // 로그 활성화
SET GLOBAL general_log = 'OFF'; // 로그 비활성화

JdbcTemplate

우리가 관심있는 부분은 로그의 가장 마지막 부분이니까 아래부터 읽읍시다.

### 원래 로그에는 시간정보도 있지만 관심사가 아니니 생략합니다.
Id Command    Argument
### Spring DBCP 설정 과정
87 Connect	root@localhost on test_c using SSL/TLS
87 Query	/* mysql-connector-j-8.3.0 (Revision: ...) */ SELECT ... # JDBC 드라이버(mysql-connector-j)가 연결 후 세션 변수를 조회하여 현재 세션의 환경 설정을 확인
87 Query	SET character_set_results = NULL
87 Query	SET autocommit=1
87 Query	SELECT @@session.transaction_read_only
87 Query	SELECT @@session.transaction_isolation
87 Query	SELECT WORD FROM INFORMATION_SCHEMA.KEYWORDS WHERE RESERVED=1 ORDER BY WORD
87 Query	SELECT @@character_set_database, @@sql_mode
88 Connect	root@localhost on test_c using SSL/TLS
...
88 Query	SELECT @@session.transaction_read_only
89 Connect	root@localhost on test_c using SSL/TLS
... 96까지 같은 내용 반복
96 Query	SELECT @@session.transaction_read_only
#####################


### JdbcTemplate의 batch insert 작업
87 Query	SELECT ... # test_c 데이터베이스 내의 모든 테이블에 대한 정보를 조회
87 Query	SELECT ... # test_c 데이터베이스 내의 모든 컬럼에 대한 상세 정보를 조회
87 Query	SET autocommit=0
87 Query	SELECT @@session.transaction_read_only
87 Query	INSERT INTO test_entity (something) VALUES(111),(222),(333),(444),(555)
87 Query	rollback # 테스트 트랜잭션이므로 완료되면 롤백
87 Query	SET autocommit=1
#####################


### Spring DBCP 정리
87 Quit	
... 87~96 반복
96 Quit
#####################

ID가 87인 커넥션(스레드)가 처리하는 것이 우리가 JdbcTemplate로 만든 요청입니다.
확실히 values로 한 번에 저장되는 것을 확인할 수 있습니다.

처음에 87에서 96 커넥션(스레드)에 하는 작업이 뭘까?

Spring 환경으로 테스트를 진행할 때 DB 커넥션 풀을 구성하기 때문에 커넥션을 미리 만들어 두는 작업입니다.
HikariCP가 10개의 커넥션(HikariCP 디폴트 커넥션 개수)을 미리 만들어 두는 작업을 합니다. 그래서 HikariCP에서는 87 ~ 96까지 10개의 커넥션을 연결을 맺어놨습니다.
마지막에 87~96을 한 번에 Quit하는 것도 그 이유입니다.

JPA saveAll

커넥션에 관한 로그는 다 지웠습니다.

2024-08-31T06:34:40.521865Z	   63 Query	SET autocommit=0
2024-08-31T06:34:40.937988Z	   63 Query	insert into test_entity (something) values (111)
2024-08-31T06:34:40.948027Z	   63 Query	insert into test_entity (something) values (222)
2024-08-31T06:34:40.955962Z	   63 Query	insert into test_entity (something) values (333)
2024-08-31T06:34:40.957121Z	   63 Query	insert into test_entity (something) values (444)
2024-08-31T06:34:40.958111Z	   63 Query	insert into test_entity (something) values (555)
2024-08-31T06:34:40.965593Z	   63 Query	rollback
2024-08-31T06:34:40.967088Z	   63 Query	SET autocommit=1
profile
공부한 내용을 적지 말고 이해한 내용을 설명하자

0개의 댓글