Spring Batch : JdbcBatchItemWriter / JpaItemWriter

    이 게시글은 인프런 정수원님의 강의를 보고 복습하며 작성한 글입니다.

    JdbcBatchItemWriter

    JdbcBatchItemWriter는 JdbcBatch의 bulk insert / update / delete 연산을 활용하는 Writer다. bulk 연산이 무엇인지 처음에는 감이 안 잡혔는데 아래와 같다고 한다.

    # 기본 SQL
    insert into customer(id, birthDate) values(val1, val5);
    insert into customer(id, birthDate) values(val2, val6);
    insert into customer(id, birthDate) values(val3, val7);
    insert into customer(id, birthDate) values(val4, val8);
    
    # Bulk insert
    insert into customer(id, birthDate) values
    (val1, val5),
    (val2, val6),
    (val3, val7),
    (val4, val8);

    기본 SQL은 4개의 Insert를 위해 4번의 DB Connection이 필요하다. 그렇지만 Bulk Insert를 하게 될 경우 4개의 Insert를 위해 1번의 DB Connection만 있으면 된다. 100,000개의 Entity를 넣어야 한다면 그 성능차이는 더 많이 날 것이다. 따라서 대규모의 Batch Writer가 필요한 상황이라면, Spring Batch를 이용한 Bulk 연산을 반드시 고려해봐야한다. 

     

    JdbcBatchItemWriter API

    JdbcBatchItemWriter를 사용하기 위해서는 Builder 클래스를 생성하고, Builder 클래스가 지원하는 API로 Batch 설정을 해줘야한다. Builder 클래스가 지원하는 API는 다음과 같다.

    • dataSource : DataSource를 입력해야함. DI로 처리 가능
    • sql : ItemWriter가 사용할 쿼리 문장 설정
    • assertUpdates : 트랜잭션 이후 적어도 하나의 Row가 Update, Delete, Insert가 되지 않을 경우 예외발생여부 설정. 기본값은 true
    • beanMapped() : Pojo 기반으로 Insert SQL의 Value를 맵핑함. 엔티티 객체라고 생각하면 편함
    • columnMapped() : Key, Value 기반으로 Insert SQL의 Values를 맵핑
    • build() : Writer를 만듦. 

     

     

    JdbcBatchItemWriter API 삽질 방지

    SQL문을 작성할 때는 into에 Column을 설정해주지 않앗다면, DB Table에 있는 Column 순서와 맵핑이 된다. 따라서 이 부분을 숙지하고 Values에 값을 집어넣어주는 것이 좋다. 반대로 into에 Column을 설정해두었다면, 그 값에 맞추어 Values에 파라메터를 맵핑하면 된다. 

     

    JdbcBatchItemWriter의 예제 코드

    @Bean
    public ItemWriter<Customer2> customItemWriterJdbcBatch() {
    
        return new JdbcBatchItemWriterBuilder<Customer2>()
                .dataSource(dataSource)
                .sql("insert into customer2(customer2_id, first_Name, last_Name, birth_Date) values (:id, :firstName, :lastName, :birthDate)")
                .beanMapped()
                .build();
    }

    나는 다음과 같이 코드를 작성했다. 긴 코드는 아래 테스트 코드에서 확인 가능하다.

     

     

    JdbcBatchItemWriter의 구조도 

    1. JdbcBatchItemWriter는 ColumnMapped, beanMapped냐에 따라 다른 형식의 Provider를 사용한다. Provider는 필요한 형식으로 값을 Mapping해서 writer() 메서드로 넘겨준다.
    2. jdbcTemplate으로 값이 전달되면, jdbcTemplate의 batchUpdate 메서드를 이용하여 값을 넣어준다. 

    중요한 것은 이 친구는 Chunk Size만큼 반복해서 DB에 쿼리를 보내는 것이 아니라, 단 한번에 쿼리를 보낸다. JPAItemWriter가 Chunk Size만큼 반복해서 DB에 쿼리를 보내는 것과 비교하면 매우 빠르다고 할 수 있다. 

     

    JdbcBatchItemWriter 클래스 확인

    items.get(0)을 한 후, 이 값이 Map 형태인지를 확인한다. Map 형태라면, ColumnMapped 형식인 것으로 이해를 하고, Map을 바꾸어 batchUpdate 쿼리를 해주는 것을 확인할 수 있다. 그게 아니라 beanMapped() 형식으로 왔다면, else 문을 타고 내려와서 batchArgs에 Parameter를 넣어주고, 그 값을 batchUpdate에 전달해서 batch Query를 실행한다. 

    또한, values에 직접적인 값을 배정하지 않고 (?, ?, ?) 형식으로 넣는 방법도 지원하는 것을 알 수 있다. 아래쪽 Else 문에서 해당 쿼리문에 대한 처리를 확인할 수 있다. 

     

     

    JpaItemWriter

    JpaItemWriter는 JPA의 Entity 기반으로 데이터를 처리한다. JPA를 사용하기 위해 EntityManagerFactory를 주입받아야 한다. 중요한 것은 JpaItemWriter는 Bulk 연산이 아니라는 점이다. Entity를 하나씩 Chunmk 크기만큼 insert / Merge 한 다음에 Flush를 한다. 이 말은 Chunk의 크기가 10이라고 하면, 하나의 Chunk를 실행할 때 Insert Query가 10개가 나간다는 말이다. 즉, 성능이 떨어질 가능성이 올라간다. 

    JpaItemWriter는 당연한 이야기지만, JPA 형식이기 때문에 받는 Item이 Entity 클래스 타입이어야 한다. 

     

    JpaItemWriter API

    JpaItemWriter를 사용하기 위해서는 JpaItemWriterBuilder를 생성해서, 해당 Builder에 API로 설정을 한 후 build를 통해 만들 수 있다. JpaItemWriterBuilder가 제공하는 API는 아래에서 확인 가능하다.

    • usePersist : persist / merge 중 어떤 것을 사용해 Entity를 처리할지를 결정.
    • entityManagerFactory : EntityManger를 생성하기 위한 Factory를 DI해야함.
    • build : JpaItemWriter를 만들어 줌

     

    JpaItemWriter 클래스 코드 작성

    @Bean
    public ItemWriter<Customer3> customItemWriterJpaBatch() {
        return new JpaItemWriterBuilder<Customer3>()
                .entityManagerFactory(emf)
                .usePersist(true)
                .build();
    }

    나는 다음과 같이 코드를 작성했다. 긴 코드는 아래 테스트 코드에서 확인 가능하다.

     

     

    JpaItemWriter 클래스

    JpaItemWriter는 위의 클래스 다이어그램처럼 동작한다. 기본적으로 JPA와 동일하게 동작하고, usePersist의 설정값이 어떻냐에 따라 persist로 DB에 밀어넣을지, merge로 DB에 밀어넣을지가 결정된다. 기본적으로는 당연하지만 persist로 밀어넣는 것이 추천이 된다. merge는 select 쿼리가 2번이 나가게 되고, DB의 값을 더럽힐 가능성이 있기 때문이다(DB에는 값이 존재, 이번에 받은 값이 Null일 경우 Update가 안되고, Null값이 DB에 들어가게 됨)

    중요한 부분은 Chunk Size만큼 Insert를 반복한다는 소리다. 예를 들어 ChunkSize가 100이면, 하나의 Chunk를 처리하는데 100번의 Insert 쿼리가 나가는 것으로 이해할 수 있다. 

     

    JdbcBatchItemWriter / JpaItemWriter 성능 비교

    동일한 조건에서 24000개의 값을 DB에 밀어넣을 때, JdbcBatchItemWriter와 JpaItemWriter의 성능 차이가 얼마나 나는지를 확인해봤다. 당연한 이야기지만 JdbcBatchItemWriter의 속도가 비교도 할 수 없을만큼 빨랐다. JpaItemWriter를 이용했을 때, 값을 insert 하기 위해 mySQL에서는 PK 값을 불러오는 Select 쿼리가 나가는 것도 있겠지만, 가장 큰 것은 JDBC는 Bulk 연산을 하지만 JPA는 Bulk 연산을 하지 못하기 때문으로 이해를 할 수 있다. 

    항목 샘플(ea) Chunk Size 걸린 시간(ms)
    JpaItemWriter 10000 10 355,522
    JdbcBatchItemWriter 39,795
    JdbcBatchItemWriter 10000 10000 2,597
    JdbcBatchItemWriter 20400 20400 4,911
    JpaItemWriter 20400 20400 279,843

    실제 수행속도는 Sample마다 다르긴 하지만, Bulk 연산이 최소 10배 이상 빠른 것으로 보인다. Sample이 많아지면 많아질수록 시간 차이는 더욱 많이 날 것 같다. 

     

     

    테스트 코드

    https://github.com/chickenchickenlove/springbatchstudy/tree/main/SpringBatchLecture/main/java/io/springbatch/springbatchlecture/dbwriter

     

    댓글

    Designed by JB FACTORY