GW LABS

Spring Boot Multi-threaded JDBCTemplate 본문

Programming/Java

Spring Boot Multi-threaded JDBCTemplate

GeonWoo Kim 2021. 12. 15. 20:23

Single Thread로 처리하기에는 너무 많은 양의 데이터를 처리해야 할 경우가 있다. 특히 마이그레이션 작업 시에 이런 일이 자주 발생한다. 신속하게 대용량 데이터를 DB에 저장 및 업데이트하기 위해서 Spring Boot와 Spring Boot JDBC를 Multi-Thread와 함께 사용해보았다. DB 작업에 Multithread를 적용하기 위해서 어떤 부분을 주의해야 하는지 함께 살펴보자.

 


 

DB 배치작업 소요시간을 줄이기 위해 선택한 Multithread

DB 배치작업 소요시간을 줄이기 위해서 멀티쓰레드 방식으로 구현을 진행하기로 결정했다면 아래와 같은 사항들을 고려해야 할 것이다.

  • Thread 단위로 데이터를 나눌 수 있는가?
    • 데이터를 Key 기준으로만 제어하는 작업이라면 Multithread로 구현을 할 때에도 비교적 쉽게 구현할 수 있다.
    • 만약 순서에 영향을 받거나 의존성이 있다면 더 복잡하게 구현이 진행됨으로 데이터 기준으로 스레드를 나눌 수 있을지 생각해보자.
  • 라이브러리들이 Thread-safe 한가?
    • 스프링부트에서 사용하고 있는 DB 관련 라이브러리들이 Thread-safe하지 않다면 문제가 될 것이다. 다행히도 대부분의 DB Connection 라이브러리들은 Thread-safe하며, 이 포스팅에서 사용하는 JDBCTemplate도 Thread-safe하다.
    • 스프링 부트에서는 Hikari ConnectionPool(이하 HikariCP)을 사용해서 DB Connection 제어한다. HikariCP 또한 Thread-safe하지만 다른 이슈가 있는데 아래에서 더 살펴보자.

 

HikariCP Dead lock

Multithread 환경에서 connection을 여러 개 사용한다면 Dead lock이 발생할 수 있다. 여러 개의 thread들이 작업을 수행하기 위해서 HikariCP에 DB Connection을 요청하는데 이 과정에서 자원을 차지하기 위한 경쟁이 발생하면 Dead Lock이 발생한다. 이를 방지하기 위해서는 아래의 마법의 공식을 참고하여 설계를 하면 된다.

  • 마법의 공식
    • pool size = 스레드 개수 * (하나의 Task에서 동시에 필요한 커넥션 수 - 1) + 1

위와 같은 마법의 공식과 더불어서 꼭 코드상에서 JDBCTemplate을 사용한다면 반드시 작업 후에 close를 통해 DB 커넥션을 HikariCP에 반납해야 한다. 

 

 

예제

아래에는 주요한 Multithread DB 처리 로직을 첨부했다. 핵심 로직을 설명하면 thread의 개수만큼 데이터를 나눠서 executorService의 invokeAll 함수를 실행하여 DB 처리를 수행하는 것이다. invokeAll은 파라미터로 받은 callable 리스트에 있는 모든 작업들을 병렬로 실행한다. 이런 식으로 데이터를 기준으로 작업을 분할할 수 있다면 invokeAll과 같은 Concurrent 패키지를 활용하여 작업을 손쉽게 수행할 수 있다.

 

public ExecutedTimeResponse multiThreaedInsert() {
        ExecutorService executorService = Executors.newFixedThreadPool(THREAD_COUNT);
        List<Callable<Void>> taskList = new ArrayList<>();

        StopWatch tableWatch = new StopWatch();
        tableWatch.start();

        int totalPage = INSERT_COUNT / THREAD_COUNT;
        for (int page = 0; page < totalPage; ++page) {

            // 쓰레드 개수만큼 Callable 적재
            for (int i = 0; i < THREAD_COUNT; ++i) {
                taskList.add(() -> {
                    sleepInsert();
                    return null;
                });
            }

            // 쓰레드 개수만큼 담아둔 작업리스트 실행
            try {
                executorService.invokeAll(taskList);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            taskList.clear();
        }

        tableWatch.stop();

        ExecutedTimeResponse executedTimeResponse = new ExecutedTimeResponse();
        executedTimeResponse.setExcutedTime(String.valueOf(tableWatch.getTotalTimeSeconds()));
        return executedTimeResponse;
    }

 

아래는 예제 프로젝트의 링크이다. 스프링 부트 프로젝트로 실행시켜 Get method로 /normal, /threaded를 실행시켜 걸리는 시간을 비교해 볼 수 있다. 참고로 db에 insert 하는 시간을 Thread.sleep을 통해 인위적으로 늘려놓은 상황이다.

 

GitHub - youlive789/spring-lab: A spring boot study project with cook book style.

A spring boot study project with cook book style. Contribute to youlive789/spring-lab development by creating an account on GitHub.

github.com

 

 

 

 

Reference

'Programming > Java' 카테고리의 다른 글

Spring Boot Multi DataSource  (3) 2021.12.14
Comments