파일을 S3에 비동기로 처리시 생기는 문제

Terror·2024년 10월 27일

최종 프로젝트

목록 보기
6/28

Overivew

  • 여러개의 파일을 엔티티에 저장하고, S3에 저장하는 과정에서 발생되는 오류에 대해 다룰 수 있습니다

Scenario

  1. 클라이언트에서 서버로 multipartfile을 전송함
  2. 톰캣의 임시파일에 저장함
  3. 비동기방식으로S3가 처리되기떄문에, 다른 쓰레드가 S3의 업로드를 처리함
  4. 하지만 메인쓰레드는 다른 쓰레드를 기다리지않기때문에 바로 종료됨
  5. 메인쓰드가 종료되면서 multipartfile은 삭제된다
  6. 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가 시작되는 시점에 해당 파일들은 날라간다
profile
테러대응전문가

0개의 댓글