이전에 중고 거래 플랫폼 프로젝트를 하면서 동기로 처리하고 있던 이미지 업로드 속도를 개선하기 위해 비동기로 개선했던 적이 있는데, 취준하느라 미루었던 기록을 이제 작성하고자 한다.
해당 프로젝트는 당근 마켓과 같이 중고 거래 상품을 등록할 때 이미지를 등록할 수 있고,
이미지는 S3에 업로드되어 저장하고 있었다.
그런데 다음 코드 처럼 이미지 업로드 시 순차적으로 S3에 저장하고 있기 때문에
여러 개의 이미지를 업로드하는 경우, 속도가 매우 느렸다.
ImageService.java
포스트맨으로 여러 번 확인했을 때 응답을 받기까지 평균 5~6초의 시간이 소요되었다.
테스트한 이미지는 3MB 크기의 10개 이미지였다.
이미지 업로드를 순차적으로 할 필요는 없다고 생각했기 때문에 속도를 개선하기 위해 생각이 났던 방법은 비동기 처리였다.
비동기 처리를 처음해보기 때문에 @Async
어노테이션을 공부하고 적용해보았다.
Spring
에서는 @Async
어노테이션으로 비동기 처리를 지원한다.
@Async를 적용하기 위해서 다음과 같은 순서로 진행했다.
- 비동기 처리에 필요한 스레드 풀을 만들기 위한 Config 파일 작성
- 메서드에
@Async
적용
@Async
를 적용하기 위해서는 @EnableAsync
를 Config
클래스 혹은 프로그램 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으로 설정했는데,
maxPoolSize
랑 queueCapacity
는 어떻게 설정하는게 좋을지 모르겠어서 우선 예제 따라서 설정해주었다.
@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);
}
과연.. 속도가 얼마나 개선되었을까 두근거리며 포스트맨으로 요청을 날렸다.
(짠! 어림도 없지 ㅋㅋ)
에러 메시지 보니까 return
타입이 잘못된 것 같은데, 찾아보니까 @Async
는 return value
가 void
인 경우만 적용 가능하다.
그런데 우리 프로젝트는 이미지를 업로드하고 URL
을 프론트에게 응답해줘야 했기 때문에 void
면 안된다.
그렇담 어떻게 해야할까?
return value
가 필요하다면 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;
}
⚠️ 주의:
@Async
는AOP
기반으로 작동되기 때문에 다음과 같은 경우 비동기 처리가 되지 않는다.
- 프록시를 생성해야 하기 때문에 private 메서드에서는 작동하지 않는다.
- 마찬가지 이유로 동일 클래스에서 내부 호출하는 메서드에서는 작동하지 않는다.
- Bean으로 관리되지 않고 있는 경우
다행(?)히 프로젝트 구조상
ImageService
랑ImageUpload
가 분리되어 있었서 정상적으로 적용되었지만 꼭 주의해서 사용하자!
이젠 되겠지...?
우와... 속도가 진짜 빨라졌다.
평균 5.5
초 걸리던 업로드 속도가 평균 0.75
초로 개선되었다.
동기 비동기 처리 속도가 이렇게 차이날 줄 몰랐다..
실제로 비동기로 처리되고 있는지 로그도 확인해보았는데
멀티 스레드로 잘 동작하고 있었다. 🙂
우와 우리 프로젝트에도 도입할 수 있겠네요. 감사합니다.