Overivew
- 여러개의 파일을 엔티티에 저장하고, S3에 저장하는 과정에서 발생되는 오류에 대해 다룰 수 있습니다
Scenario
- 클라이언트에서 서버로 multipartfile을 전송함
- 톰캣의 임시파일에 저장함
- 비동기방식으로S3가 처리되기떄문에, 다른 쓰레드가 S3의 업로드를 처리함
- 하지만 메인쓰레드는 다른 쓰레드를 기다리지않기때문에 바로 종료됨
- 메인쓰드가 종료되면서 multipartfile은 삭제된다
- S3에 업로드하던 쓰레드는 tomcat에 있는 임시파일이 삭제됨에 따라, 파일을 업로드할 수 없어 오류가 난다
코드
@Transactional
@Override
public void upload(List<MultipartFile> files, AuthUser authUser, Long targetId, AttachmentTargetType target) {
User user = User.fromAuthUser(authUser);
List<Path> paths = new ArrayList<>();
for ( MultipartFile file : files ) {
AttachmentValidator.isInExtension(file);
AttachmentValidator.isSizeBig(file);
Path path = pathService.mkPath(file,authUser,targetId,target);
paths.add(path);
saveAttachmentGetId(path,file,user,targetId,target);
}
s3Service.uploadAllAsync(paths,files);
}
- 클라이언트로부터 아이디들을 받고, 엔티티와 S3에는 비동기로 저장시키는 로직입니다
- 다음은 uploadAllAsync 메서드 내부입니다
@Async
public CompletableFuture<Void> uploadAllAsync(List<Path> paths, List<MultipartFile> files) {
log.info("paths length : {}", paths.size());
try {
for (int i = 0; i < paths.size(); i++) {
Path path = paths.get(i);
MultipartFile file = files.get(i);
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentType(file.getContentType());
metadata.setContentLength(file.getSize());
log.info("Uploading file {} to S3: {}", i, path.toString());
amazonS3Client.putObject(bucket, path.toString(), file.getInputStream(), metadata);
}
return CompletableFuture.completedFuture(null);
} catch(Exception e){
log.error("Error uploading file to S3", e);
return CompletableFuture.failedFuture(new FileException(FILE_IO_ERROR));
}
}
- 포스트맨에서 여러개의 이미지를 첨부하고 실행시켜보면...
java.nio.file.NoSuchFileException: /private/var/folders/vc/98tjrwxj5fx5xm1yggq1mz0c0000gn/T/tomcat.8080.2300255632793782800/work/Tomcat/localhost/ROOT/upload_b759f251_e622_4caf_9eab_bf6336dde52b_00000002.tmp
at java.base/sun.nio.fs.UnixException.translateToIOException(UnixException.java:92) ~[na:na]
at java.base/sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:106) ~[na:na]
at java.base/sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:111) ~[na:na]
at java.base/sun.nio.fs.UnixFileSystemProvider.newByteChannel(UnixFileSystemProvider.java:218) ~[na:na]
at java.base/java.nio.file.Files.newByteChannel(Files.java:380) ~[na:na]
at java.base/java.nio.file.Files.newByteChannel(Files.java:432) ~[na:na]
at java.base/java.nio.file.spi.FileSystemProvider.newInputStream(FileSystemProvider.java:422) ~[na:na]
at java.base/java.nio.file.Files.newInputStream(Files.java:160) ~[na:na]
at org.apache.tomcat.util.http.fileupload.disk.DiskFileItem.getInputStream(DiskFileItem.java:196) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
at org.apache.catalina.core.ApplicationPart.getInputStream(ApplicationPart.java:97) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile.getInputStream(StandardMultipartHttpServletRequest.java:260) ~[spring-web-6.1.13.jar:6.1.13]
at com.sparta.doguin.domain.attachment.service.s3.S3Service.uploadAllAsync(S3Service.java:71) ~[main/:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:355) ~[spring-aop-6.1.13.jar:6.1.13]
at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:196) ~[spring-aop-6.1.13.jar:6.1.13]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-6.1.13.jar:6.1.13]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:768) ~[spring-aop-6.1.13.jar:6.1.13]
at org.springframework.aop.interceptor.AsyncExecutionInterceptor.lambda$invoke$0(AsyncExecutionInterceptor.java:113) ~[spring-aop-6.1.13.jar:6.1.13]
at org.springframework.util.concurrent.FutureUtils.lambda$toSupplier$0(FutureUtils.java:74) ~[spring-core-6.1.13.jar:6.1.13]
at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1768) ~[na:na]
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136) ~[na:na]
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635) ~[na:na]
at java.base/java.lang.Thread.run(Thread.java:840) ~[na:na]
- 오류가 난다
- 메인쓰레드가 종료되고, multipartfile이 저장되있는 임시 파일들이 삭제되면서 s3에 업로드 하지못하는 문제다
해결
@Async
public CompletableFuture<Void> uploadAllAsyncs(List<Path> paths, List<byte[]> fileBytesList) {
try {
for (int i = 0; i < paths.size(); i++) {
Path path = paths.get(i);
byte[] fileBytes = fileBytesList.get(i);
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(fileBytes.length);
try (InputStream inputStream = new ByteArrayInputStream(fileBytes)) {
amazonS3Client.putObject(bucket, path.toString(), inputStream, metadata);
}
}
return CompletableFuture.completedFuture(null);
} catch (Exception e) {
log.error("Error uploading file to S3", e);
return CompletableFuture.failedFuture(new FileException(FILE_IO_ERROR));
}
}
- 메인쓰레드에서 파일의 바이트들을 저장시켜놓고, 인자값으로 넘겨준후에
- 비동기 쓰레드에서 처리하게 하면 해결은된다
찜찜...
- 해결은 하긴 했지만, 이 방식이 맞는지는 잘 모르겠다 더 공부하자
오늘 난 무엇을 알았는가?
- 클라이언트에서 서버로 multipartfile형식으로 데이터를 보내면 내장 서버 tomcat을 쓰고있다면, 임시디렉토리에 파일들이 위치한다
- GC가 시작되는 시점에 해당 파일들은 날라간다