JPA : Batch 처리하기


    Batch 처리하기

    DB에는 수백만건의 데이터를 한번에 처리해야하는 상황도 올 수 있다. 이럴 경우 가장 조심해야 하는 부분은 "메모리 부족 오류"이다. JPA는 영속화를 하게 되며 영속성 컨텍스트의 1차 캐시와 쓰기지연 저장소에 SQL 쿼리를 저장한다. 수백만건을 한방에 처리한다고 하면, 수백만건에 대한 엔티티 + 쿼리가 계속 메모리 상에 저장되고 있는 것이다. 이것은 메모리 부족 오류 문제를 야기한다. 

    이런 문제점을 해결 하기 위해서는 "나눠서 처리"를 해야한다. 나눠서 DB에 값을 밀어넣거나 가져오고, 필요한 연산을 하고 영속성 컨텍스트를 초기화해준다. 그리고 다시 또 나눠서 처리를 하는 방식이다. 이 경우, 메모리 부족 오류는 개선할 수 있지만 DB 커넥션을 자주 타게 되면서 동작 속도 관점에서는 손해를 볼 수 밖에 없다. 

     


    저장 Batch 처리

    저장을 하게 되면 Persist 메서드를 사용한다. Persist 메서드를 사용하면 영속성 컨텍스트에 엔티티를 보관하고 있다. 수백만건이 쌓이게 되면 메모리 부족 문제가 생길 수 밖에 없다. 따라서 주기적으로 em.clear(), em.flush() 처리를 해주면서 문제를 해결할 수 있다. 

     

    테스트 코드1 : Batch 처리 없이 저장

    @Test
    @DisplayName("저장 : Batch 처리 X")
    void test1() {
    
        for (int i = 0; i < 100000; i++) {
            Member member = new Member();
            em.persist(member);
        }
    
    }


    테스트 코드2 : Batch 처리 저장

    
    @Test
    @DisplayName("저장 Batch 처리 O")
    void test2() {
    
        int cnt = 0 ;
        for (int i = 0; i < 100000; i++) {
            Member member = new Member();
            em.persist(member);
    
    
            if (i % 100 == 0) {
                em.flush();
                em.clear();
            }
        }
    }

    Batch 처리를 할 경우에 현재의 Count가 얼마인지 확인하고 특정 Count에서 em.flush() + em.clear()를 적절하게 하는 방법을 사용해주면 된다.

    각 테스트 코드 결과를 비교했다. Batch 처리 없이 한방에 보내는 경우는 35초가 걸렸으나, Batch 처리를 하게 될 경우 2배 이상의 시간이 더 필요한 것을 확인할 수 있다. 

     


    수정 Batch 처리

    수정 Batch 처리 역시 마찬가지다. JPA의 Update 구문 같은 것들을 사용할 수도 있지만, 엔티티를 불러와서 수정 Batch 처리를 하는 경우도 있다. 이 경우도 마찬가지인데 갈래는 동일하다. 적당히 여러번 나누어서 처리를 하면 된다. 수정 Batch 처리는 페이징 기능으로 처리한다. 페이징 기능으로 0~100페이지, 100~200페이지, 200~300페이지 등 나누어 처리하고 em.flush() + em.clear()로 해결할 수 있다. 

     

    테스트코드1 - 페이징으로 배치 처리

    @Test
    @DisplayName("저장 : Batch 처리 X + 수정 : 페이징으로 Batch 처리")
    void test3() {
    
        for (int i = 0; i < 1000; i++) {
            Member member = new Member();
            em.persist(member);
        }
    
        em.flush();
        em.clear();
    
        PageRequest page = PageRequest.of(0, 100);
    
        for (int i = 0; i < 10; i++) {
    
            List<Member> result = queryFactory.selectFrom(member)
                    .offset(page.getOffset())
                    .limit(page.getPageSize())
                    .fetch();
    
            for (Member member1 : result) {
                member1.setName("memberA");
            }
    
            em.flush();
            em.clear();
    
    
            PageRequest nextPage = PageRequest.of(page.getPageNumber(), 100);
            page = nextPage;
        }
    }

    100개 단위로 Pageable 객체를 만들어 페이징 기능을 이용해 회원 엔티티를 불러왔다. 이후, 이름을 "memberA"로 셋팅해준 후 더티 체킹을 통해 Update 구문이 나가도록 설정했다. 이렇게 되면 select 1번에 Update 구문 100번이 나가게 되는 것을 확인할 수 있다.

     

     


    정리

    한번에 수백만건의 엔티티를 처리해야하는 Batch 처리의 경우 한번에 처리하게 된다면 반드시 메모리 부족 오류가 발생한다. 따라서 이 경우는 나누어 처리하는 것이 가장 좋으며, 수정 쿼리의 경우는 페이징 기능을 이용해 나누어 처리하는 방법을 권장한다.

     

     

     

    코드

    https://github.com/chickenchickenlove/JpaSelfStudy/blob/89bc3bec94477b88204c947cfa3eb97fb67550f9/studyJpa/test/java/selfjpa/studyjpa/repository/BatchTest.java

    댓글

    Designed by JB FACTORY