티스토리 뷰

쇼핑몰 사이트 같은 경우 하나의 상품에 여러 개의 사진이 들어간다. 10개 이상 사진 업로드를 할 경우 서버에서 처리하는 시간이 대략 2초 이상 걸리다 보니 성능 개선이 필요성을 느꼈다. 처리속도를 개선하기 위해 비동기 방식의 멀티스레드를 이용해서 처리하면 싱글 스레드보다 멀티 스레드가 훨씬 빠른 장점이 있기 때문에 Spring에서 제공해주는 ThreadPoolTaskExecutor 이용해서 처리하였다.

실제로 파일 15개 싱글 스레드로 구현할 경우 대략 2초 걸린 것을 확인할 수 있다.

 

 

1. ThreadPoolTaskExecutor 개념과 설정


[ ThreadPoolTaskExecutor 란? ]

ThreadPoolTaskExecutor는 Spring에서 제공하는 멀티 스레드 작업을 관리하는 유용한 도구입니다. 이를 사용하면 효율적으로 스레드 풀을 관리하고, 애플리케이션의 성능을 최적화할 수 있습니다. 특정 스레드 개수 + 작업 큐 + 스레드 풀 개수를 만들어 놓고 작업을 작업 큐에 올려놓아 적입 끝난 스레드가 큐에서 작업을 꺼내 기능을 수행하는 방식이다. 분업화로 인한 업무효율이 향상되는 장점이 있다. 아무리 작업 처리 요청이 폭주하여도 스레드의 전체 개수가 늘어나지 않기 때문에 애플리케이션의 성능이 급격하게 저하되지 않는다.

 

 

[ ThreadPoolTaskExecutor 설정 ]

@EnableAsync
@Configuration
public class AsyncConfig {
    private static int CORE_POOL_SIZE= 15;
    private static int MAX_POOL_SIZE = 25;
    private static int QUEUE_CAPACITY = 10;
    private static String THREAD_NAME_PREFIX = "async-task";

    @Bean
    public Executor asyncTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(CORE_POOL_SIZE);
        executor.setMaxPoolSize(MAX_POOL_SIZE);
        executor.setQueueCapacity(QUEUE_CAPACITY);
        executor.setThreadNamePrefix(THREAD_NAME_PREFIX);
        executor.initialize();
        return executor;
    }
}

@EnableAsync 어노테이션을 선언하면 비동기 처리할 수 있는 기능을 활성화한다.
ThreadPoolExecutor 인스턴스를 초기화할 때 여러 가지 설정값들이 있는데 그중 대표적인 옵션 3가지가 있다.


corePoolSize
thread-pool에 항상 살아있는 thread의 최소 개수입니다.


maxPoolSize
thread-pool에서 사용할 수 있는 최대 thread의 개수입니다.


queueCapacity
thread-pool에서 사용할 최대 queue의 크기입니다.

 

추가설정(선택)
keepAliveSeconds
maxPoolSize가 모두 사용되다가 idle로 돌아갔을 때 종료하기까지 대기하는 걸리는 시간입니다.
예를들어 core size가 5이며, max size가 15라고 하겠습니다. 요청이 많아져서 thread pool이 바빠져서 추가적으로 10개의 thread를 생성해서 15개의 thread를 사용하게됩니다. 그리고 얼마 후 바빠진게 해소가 되어서 이제 10개의 추가된 thread는 한가한(idle) 상태가 되게됩니다. 그리고 자원의 절약을 위해 한가한 상태의 thread가 죽게(die)됩니다.

thread가 idle 상태에서 die 상태가 되기까지 대기하는 시간을 keepAlivesSeconds 옵션으로 설정할 수 있습니다. thread를 다시 생성하는 비용과 idle 상태로 유지하는 비용의 trade-off를 잘 생각해서 설정하면 좋습니다.

 

[ ThreadPoolTaskExecutor의 기본적인 동작원리 ]

1. 현재 점유하고 있는 스레드의 개수가 corePoolSize만큼 있을 때 요청이 오면 지정된 queueCapacity의 개수만큼 요청을 큐에 넣는다.
2. 현재 점유하고 있는 스레드의 개수가 corePoolSize만큼 있고 큐에 담긴 요청이 queueCapacity의 개수만큼 있을 때 요청이 오면 maxPoolSize만큼 스레드 풀을 생성한다.
3. 만약 현재 점유하고 있는 스레드의 개수가 maxPoolSize만큼 있고 큐에 담긴 요청이 queueCapacity의 개수만큼 있을 때 요청이 오면 java.util.concurrent.RejectedExecutionException 예외가 발생된다.

 

 

 

2. ThreadPoolTaskExecutor 사용법


private final Executor executor;

public void upload(MultipartFile[] multipartFiles) throws Exception {
    long startTime = System.currentTimeMillis();
    for (int i = 0; i < multipartFiles.length; i++) {
        MultipartFile file = multipartFiles[i];
        String fileName = createFileName(file.getOriginalFilename());
        Runnable runnable = () -> {
            ObjectMetadata objectMetadata = new ObjectMetadata();
            objectMetadata.setContentLength(file.getSize());
            objectMetadata.setContentType(file.getContentType());

            try (InputStream inputStream = file.getInputStream()) {
                amazonS3.putObject(new PutObjectRequest(bucket, fileName, inputStream, objectMetadata)
                        .withCannedAcl(CannedAccessControlList.PublicRead));
            } catch (IOException e) {
                e.printStackTrace();
            }
        };
        executor.execute(runnable);
    }
    long endTime = System.currentTimeMillis();
    System.out.println(String.format("코드 실행 시간: %20dms", endTime - startTime));
}

Executor를 주입받은 뒤 처리하고자 할 작업을 Runnable 인터페이스의 run() 메서드에 정의하고
Executor의 execute 메서드로 Runnable를 인자로 넘겨주면 된다.


실행

덕분에 응답처리시간을 2초 -> 0초로 줄여져서 보다 빠른 응답처리를 할 수 있게 되었다.

'∙Java & Spring' 카테고리의 다른 글

Feign Client 사용하기  (0) 2022.09.24
동시성 이슈 해결하기  (0) 2022.09.24
Junit Mock 사용하기  (0) 2022.05.06
인프런 스프링 핵심 원리 정리  (0) 2022.04.20
JWT 구조  (0) 2022.04.16