[Spring + S3] 비동기로 이미지 업로드 속도 개선

Jinny·2024년 3월 31일
1

Spring

목록 보기
10/10

💬 들어가며

이전에 중고 거래 플랫폼 프로젝트를 하면서 동기로 처리하고 있던 이미지 업로드 속도를 개선하기 위해 비동기로 개선했던 적이 있는데, 취준하느라 미루었던 기록을 이제 작성하고자 한다.




📸 비동기로 이미지 업로드 속도 개선

문제점

해당 프로젝트는 당근 마켓과 같이 중고 거래 상품을 등록할 때 이미지를 등록할 수 있고,
이미지는 S3에 업로드되어 저장하고 있었다.

그런데 다음 코드 처럼 이미지 업로드 시 순차적으로 S3에 저장하고 있기 때문에
여러 개의 이미지를 업로드하는 경우, 속도가 매우 느렸다.

ImageService.java

포스트맨으로 여러 번 확인했을 때 응답을 받기까지 평균 5~6초의 시간이 소요되었다.
테스트한 이미지는 3MB 크기의 10개 이미지였다.

이미지 업로드를 순차적으로 할 필요는 없다고 생각했기 때문에 속도를 개선하기 위해 생각이 났던 방법은 비동기 처리였다.




@Async 적용

비동기 처리를 처음해보기 때문에 @Async 어노테이션을 공부하고 적용해보았다.
Spring에서는 @Async 어노테이션으로 비동기 처리를 지원한다.

@Async를 적용하기 위해서 다음과 같은 순서로 진행했다.

  1. 비동기 처리에 필요한 스레드 풀을 만들기 위한 Config 파일 작성
  2. 메서드에 @Async 적용

1. 비동기 처리에 필요한 스레드 풀을 만들기 위한 Config 파일 작성

  • @Async를 적용하기 위해서는 @EnableAsyncConfig 클래스 혹은 프로그램 Main 메서드에 붙여줘야 한다.
  • 어디에 붙이든 똑같이 동작하지만, 개인적으로 Main 메서드에 어노테이션이 덕지덕지 붙는게 싫어서 Config에 붙여주었다.
@EnableAsync // 필수!
@Configuration
public class AsyncConfig {

    @Bean(name = "imageUploadExecutor")
    public Executor imageUploadExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

        executor.setThreadGroupName("imageUploadExecutor");
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(20);
        executor.setQueueCapacity(50);
        executor.initialize();

        return executor;
    }
}

💡참고:

  • setCorePoolSize(): 동시에 실행시킬 쓰레드의 개수를 설정
  • setMaxPoolSize(): 스레드 풀의 최대 사이즈 설정
  • setQueueCapacity(): 스레드 풀 큐의 사이즈 설정
    • corePoolSize 개수를 넘어서는 task가 들어왔을 때 queue에 해당 task가 쌓인다.

해당 프로젝트에서는 이미지를 최대 10개까지 업로드할 수 있기 때문에 corePoolSize를 10으로 설정했는데,
maxPoolSizequeueCapacity는 어떻게 설정하는게 좋을지 모르겠어서 우선 예제 따라서 설정해주었다.


2. 메서드에 @Async 적용

그리고 이미지 업로드하는 메서드에 @Async 어노테이션을 적용했다

ImageUploader.java

	@Async("imageUploadExecutor")
    public URL uploadImage(MultipartFile multipartFile) {
        long startTime = System.currentTimeMillis();

        String uuid = UUID.randomUUID().toString();

        ObjectMetadata metadata = new ObjectMetadata();
        metadata.setContentLength(multipartFile.getSize());
        metadata.setContentType(multipartFile.getContentType());

        try {
            amazonS3.putObject(bucket, uuid, multipartFile.getInputStream(), metadata);
        } catch (IOException e) {
            throw new RuntimeException();
        }

        long endTime = System.currentTimeMillis();
        log.info("{} - 실행 시간: {}", Thread.currentThread().getName(), endTime - startTime);

        return amazonS3.getUrl(bucket, uuid);
    }

과연.. 속도가 얼마나 개선되었을까 두근거리며 포스트맨으로 요청을 날렸다.


3-1. Invalid return type for async method 에러 발생

(짠! 어림도 없지 ㅋㅋ)

에러 메시지 보니까 return 타입이 잘못된 것 같은데, 찾아보니까 @Asyncreturn valuevoid인 경우만 적용 가능하다.

그런데 우리 프로젝트는 이미지를 업로드하고 URL을 프론트에게 응답해줘야 했기 때문에 void면 안된다.

그렇담 어떻게 해야할까?
return value가 필요하다면 CompletableFuture를 사용해야 한다.


3-2. 메서드에 CompletableFuture 적용

ImageUploader.java

	@Async("imageUploadExecutor")
    public CompletableFuture<URL> uploadImage(MultipartFile multipartFile) {
        CompletableFuture<URL> future = new CompletableFuture<>();

        // 이미지 업로드 로직 생략...
        
        future.complete(amazonS3.getUrl(bucket, uuid));

        return future;
    }

ImageService.java

    private List<URL> uploadImages(List<MultipartFile> files) {
        List<CompletableFuture<URL>> futures = new ArrayList<>();

        for (MultipartFile file : files) {
            futures.add(imageUploader.uploadImage(file));
        }

        List<URL> urls = new ArrayList<>();
        futures.forEach(future ->
                urls.add(future.join())
        );

        return urls;
    }

⚠️ 주의:
@AsyncAOP 기반으로 작동되기 때문에 다음과 같은 경우 비동기 처리가 되지 않는다.

  1. 프록시를 생성해야 하기 때문에 private 메서드에서는 작동하지 않는다.
  2. 마찬가지 이유로 동일 클래스에서 내부 호출하는 메서드에서는 작동하지 않는다.
  3. Bean으로 관리되지 않고 있는 경우

다행(?)히 프로젝트 구조상 ImageServiceImageUpload가 분리되어 있었서 정상적으로 적용되었지만 꼭 주의해서 사용하자!

이젠 되겠지...?




결과!!

포스트맨

우와... 속도가 진짜 빨라졌다.
평균 5.5초 걸리던 업로드 속도가 평균 0.75초로 개선되었다.

동기 비동기 처리 속도가 이렇게 차이날 줄 몰랐다..


스레드 로그

실제로 비동기로 처리되고 있는지 로그도 확인해보았는데
멀티 스레드로 잘 동작하고 있었다. 🙂


🔗 참고

profile
공부는 마라톤이다. 한꺼번에 많은 것을 하다 지치지 말고 조금씩, 꾸준히, 자주하자.

1개의 댓글

comment-user-thumbnail
2024년 3월 31일

우와 우리 프로젝트에도 도입할 수 있겠네요. 감사합니다.

답글 달기