본문 바로가기

개발 고민

MultipartFile 업로드에 관하여

어디에서든지 파일을 다루는 시스템을 설계하고 구현할 일이 생긴다.

도메인은 다르고 파일의 형태는 다르지만, 뭔가 항상 다루는 MultipartFile.

 

한번 구현해 보자.

 

설계


필요한 기능은 다음과 같다.

  1. 단일 파일 업로드
  2. 업로드한 전체 파일 조회
  3. 업로드한 파일 제거

이번 구현에서 고려하지 않을 기능은 다음과 같다.

  1. DB
  2. 다중 파일 업로드
  3. API controller

예제 소스에서는 서버가 실행되는 위치에 업로드한 파일을 저장할 예정이다. 

단순 업로드 구현


@Slf4j
@Service
public class FileService {
    @Value("${file.upload-path}")
    private String uploadDirectory;

    public boolean saveFile(MultipartFile multipartFile) {
        //파일이 없으면 별도의 처리를 한다.
        if (multipartFile == null || multipartFile.isEmpty()) {
            return false;
        }

        String fileName = multipartFile.getOriginalFilename();
        File targetFile = new File(uploadDirectory, fileName);
        try{
            FileCopyUtils.copy(multipartFile.getInputStream(), new FileOutputStream(targetFile));

            //저장을 완료하면, multipartFile로 만든 파일은 제거한다.
            targetFile.delete();
        }catch (IOException ioException){
            //파일 저장 실패시 에러 핸들링.
            log.error(ioException.toString(),ioException);
            return false;
        }
        
        return true;

    }
}

MultipartFile를 받아서 파일로 원하는 위치에 저장한다. 
파라미터로 받은 MultipartFile를 File객체로 변환하며, 원하는 위치에 실제 파일이 만들어진다.

 

허나.. 용량이 좀 큰 파일을 업로드하면, FileSizeLimitExceededException 에러를 만난다.. 

해결해 보자

 

큰 용량을 업로드해 보자


결론부터 말하면, Spring Boot에 있는 내장 톰캣에서 업로드 용량에 제한을 걸어서 나오는 에러다.

mulipart 설정을 세팅하는 로직

 

YAML 또는 properties를 통한 mulipart설정이 없다면, connector에서 값을 가져와 세팅한다.

connector를 보니, Spring Boot 2.7.12(Java 17) 기준 2MB다.

 

그러면, YAML 또는 properties를 통해 업로드 제한을 변경해 보자!

Spring Boot 2.7 기준 설정

application.yml에 설정을 추가하면 보다 큰 용량도 업로드 가능하다.

출처: https://docs.spring.io/spring-boot/docs/2.7.x/api/org/springframework/boot/web/servlet/MultipartConfigFactory.html

공식 문서를 보면 max-file-size는 MultipartFile 1개의 최대 업로드 사이즈, max-request-sizes는 multipart/form-data한 개의 요청의 최대 업로드 사이즈 설정이다.

 

만일.. 파일이 5GB, 10GB처럼 크면 어떻게 될까..?

MultipartFile이 커지면 힙메모리에 부하가 갈 것이다. 메모리 부족현상은 당연히 발생할 것이다.

 

분할 업로드 구현


하나의 파일을 분할해서 업로드하면, 서버의 트래픽은 증가하지만, 각 요청의 응답 속도는 빨라진다.

추후 서버를 증설하면 증가된 서버 트래픽도 해결된다.

 

분할 업로드를 직접 IDC에 업로드하는 방법보다는 AWS S3를 이용해서 간단하게 구현해 보자.

 

AWS S3에서도 큰 파일을 업로드할 때 파일을 분할해서 업로드할 수 있게 SDK를 제공한다.

AWS S3의 분할 파일 업로드 프로세스는 다음과 같다.

 

  1. 분할 파일을 업로드한다고 S3에게 알려준다.
  2. 분할 파일을 업로드한다.
  3. 분할 파일을 다 업로드했다고 S3에게 알려준다.

1번 과정에서 S3는 id를 제공한다. id를 이용해서 분할파일을 업로드하고 3번 과정을 통해 S3는 분할 파일을 합쳐서 하나의 온전한 파일로 만든다.

 

이를 간단하게 구현하면 다음과 같다.

@Service
@RequiredArgsConstructor
public class ChunkedFileService {
    private final AmazonS3Client amazonS3Client;

    public InitiateMultipartUploadResult initiateUploadChunkedFile() {
        //S3 분할 파일 업로드 init
        InitiateMultipartUploadResult initiateMultipartUploadResult = amazonS3Client.initiateMultipartUpload(
                new InitiateMultipartUploadRequest("버킷 명", "업로드 위치"));

        //필요할 경우 DB에 분할 파일 업로드 정보 저장.

        return initiateMultipartUploadResult;
    }


    /**
     * 분할 파일을 업로드한다.
     *
     */
    public UploadPartResult uploadChunkedFile(String uploadId, Integer chunkSeq, MultipartFile multipartFile) throws IOException {
        ObjectMetadata objectMetadata = new ObjectMetadata();
        objectMetadata.setContentType(multipartFile.getContentType());
        objectMetadata.setContentLength(multipartFile.getSize());

        UploadPartRequest uploadPartRequest = new UploadPartRequest()
                .withBucketName("버킷 명")
                .withKey("업로드 위치")
                .withUploadId(uploadId)//initiateMultipartUpload를 통해 받은 id
                .withPartNumber(chunkSeq) // 순차적인 part number (1~10,000)
                .withInputStream(multipartFile.getInputStream())
                .withObjectMetadata(objectMetadata)
                .withPartSize(multipartFile.getSize()); // chunk file size

        return amazonS3Client.uploadPart(uploadPartRequest);
    }

    /**
     * 업로드 한 분할 파일을 하나의 파일로 합친다.
     */
    public CompleteMultipartUploadResult mergeChunkedFile(String uploadId, List<PartETag> eTagList) {
        CompleteMultipartUploadRequest completeRequest = new CompleteMultipartUploadRequest(
                "버킷 명",
                "업로드 위치",
                uploadId,
                eTagList);//분할 파일을 업로드 하는 과정에서 받은 eTag들의 list

        return amazonS3Client.completeMultipartUpload(completeRequest);
    }
}

 

마무리


 

대용량의 파일을 업로드할 때, S3를 이용하면, upload id, etag 등 다양한 값을 사용하게 된다.

클라이언트에게 upload id, etag 등 필요한 값을 주는 방법도 있고 원한다면 서버가 DB에 upload id, etag를 저장하고 관리하는 방법도 고려할 수 있다.

'개발 고민' 카테고리의 다른 글

JPA IdentifierGenerator의 connection에 대해..  (0) 2023.09.04
단일 책임 원칙과 클린코드 V2  (0) 2023.08.09