티스토리 뷰

반응형

Node에서 활용하는 멀티파트 파일의 다운로드를 진행했었다면, 이번 글에서는 멀티파트 파일의 업로드 및 다운로드를 Java에서 진행하는 것으로 작성해보려 합니다. 

 

 

Express Router 를 사용해서 파일 다운로드 만들기

브라우저에서 버튼을 클릭할 때 파일을 다운로드하게 하는 방법입니다. 다운로드 zip Node 에서는 파일을 전송하기 위한 라우터가 필요한데, express를 설치합니다. $ npm i express const express = require('ex

abbo.tistory.com

먼저 FileController 입니다.

FileController.java

import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.UUID;

@RestController
@RequiredArgsConstructor
@RequestMapping("/v1/files")
public class FileController {
    private final FileService fileService;

    @PostMapping
    public ResponseEntity<?> upload(@RequestParam("data") MultipartFile data,
                                    @RequestParam("dirName") String dirName) throws IOException {
        File file = fileService.upload(data, dirName);
        UploadResponse uploadResponse = UploadResponse.builder()
                .fileId(file.getId().toString())
                .originName(file.getName())
                .path(file.getPath())
                .size(file.getSize())
                .extension(file.getExtension())
                .build();
        return ResponseEntity.ok().body(uploadResponse);
    }

    @GetMapping("/{id}")
    public ResponseEntity<Resource> download(@PathVariable("id") String id) throws IOException {
        File downloadFile = fileService.download(id);
        return ResponseEntity.ok()
                .contentType(MediaType.valueOf(MediaType.APPLICATION_OCTET_STREAM_VALUE))
                .contentLength(downloadFile.getSize())
                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + downloadFile.getFullName())
                .header(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate")
                .header(HttpHeaders.PRAGMA, "no-cache")
                .header(HttpHeaders.EXPIRES, "0")
                .body(downloadFile.getResource());
    }
}

그 다음으로 이를 수행하는 FileService 클래스입니다.

FileService.java

import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException

import static org.springframework.util.StringUtils.getFilenameExtension;

@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class FileService {
    private final FileRepository fileRepository;
    private final FileUploadRepository fileUploadRepository;

    public File upload(MultipartFile multipartFile, String dirName) throws IOException {
        String originalFilename = multipartFile.getOriginalFilename();
        String uploadPath = fileUploadRepository.upload(multipartFile, dirName);
        File file = new File(originalFilename, uploadPath, getFilenameExtension(originalFilename), multipartFile.getSize());
        return fileRepository.save(file);
    }

    @Transactional(readOnly = true)
    public File download(final String id) throws IOException {
        File file = fileRepository.findById(id)
                .orElseThrow();
        String uploadPath = file.getPath();
        Resource downloadResource = fileUploadRepository.download(uploadPath, file.getFullName());
        file.setResource(downloadResource);
        return file;
    }
}

FileRepository의 경우 JpaRepository이므로 데이터 이력을 위하여 필요한 Repository입니다. 우리에게 필요한 것은 AWS S3에 파일을 업로드하는 소스이므로 FileUploadRepository 내부를 확인해보겠습니다.

FileUploadRepository.java

import com.amazonaws.AmazonServiceException;
import com.amazonaws.SdkClientException;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.model.*;
import com.amazonaws.util.IOUtils;
import kr.co.packagename.encryption.Sha256Encryption;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Optional;

import static org.springframework.util.StringUtils.getFilename;
import static org.springframework.util.StringUtils.getFilenameExtension;

@Service
@RequiredArgsConstructor
public class FileUploadRepository {
    private final AmazonS3Client amazonS3Client;
    private final Sha256Encryption sha256Encryption;

    @Value("${cloud.aws.s3.bucket}")
    private String bucket;

    @Value("${spring.profiles.active}")
    private String profile;

    @Value("${cloud.aws.s3.bucketUrl}")
    private String bucketUrl;

    public String upload(MultipartFile multipartFile, String dirName) throws IOException {
        File uploadFile = convert(multipartFile).orElseThrow(() -> new IllegalArgumentException("MultipartFile -> File로 전환이 실패했습니다."));
        return upload(uploadFile, dirName);
    }

    private String upload(File uploadFile, String dirName) throws IOException {
        String extension = getFilenameExtension(uploadFile.getName());
        String encryptFileName = sha256Encryption.encryptString(getFilename(uploadFile.getName())) + "." + extension;
        String fileName = Path.of(profilePath(), dirName, datePath(), encryptFileName).toString();
        String uploadImageUrl = putS3(uploadFile, fileName);
        removeNewFile(uploadFile.getPath());
        return uploadImageUrl;
    }

    private String putS3(File uploadFile, String fileName) {
        amazonS3Client.putObject(new PutObjectRequest(bucket, fileName, uploadFile).withCannedAcl(CannedAccessControlList.PublicRead));
        return amazonS3Client.getUrl(bucket, fileName).toString();
    }

    private void removeNewFile(String filePath) throws IOException {
        Files.delete(Paths.get(filePath));
    }

    private Optional<File> convert(MultipartFile file) throws IOException {
        File convertFile = new File(file.getOriginalFilename());
        if (convertFile.createNewFile()) {
            try (FileOutputStream fos = new FileOutputStream(convertFile)) {
                fos.write(file.getBytes());
            }
            return Optional.of(convertFile);
        }
        return Optional.empty();
    }

    private String datePath() {
        return LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy" + File.separator
                + "MM" + File.separator + "dd"));
    }

    private String profilePath() {
        return ProfilePath.valueOf(profile.toUpperCase())
                .getPath();
    }

    public Resource download(String path, String fileName) throws IOException {
        S3Object headerOverrideObject = null;
        ByteArrayResource byteArrayResource = null;
        try {
            ResponseHeaderOverrides headerOverrides = new ResponseHeaderOverrides()
                    .withCacheControl("No-cache")
                    .withContentDisposition("attachment; filename=" + fileName);
            GetObjectRequest getObjectRequestHeaderOverride = new GetObjectRequest(bucket, path.replace(bucketUrl,""))
                    .withResponseHeaders(headerOverrides);
            headerOverrideObject = amazonS3Client.getObject(getObjectRequestHeaderOverride);
            byteArrayResource = new ByteArrayResource(IOUtils.toByteArray(headerOverrideObject.getObjectContent()));
        } catch (AmazonServiceException e) {
            e.printStackTrace();
        } catch (SdkClientException e) {
            e.printStackTrace();
        } finally {
            if (headerOverrideObject != null) {
                headerOverrideObject.close();
            }
        }
        return byteArrayResource;
    }

    @Getter
    enum ProfilePath {
        DEV("development"), STAGE("development"), PROD("production"), TEST("test");

        private String path;

        ProfilePath(String path) {
            this.path = path;
        }
    }
}

 

보시게 되면 먼저 운영환경에 따라 프로필로 구분을 지어 파일 업로드에 대한 처리를 진행합니다. AWS에서 계정은 이미 발급이 받았다는 가정 하에, 할당 받은 버킷을 application.yml 내부에 값을 저장하고 그 값을 추출하여 적용하는 것으로 개발 진행하였습니다. 암호화 모듈의 경우 각 회사별로 사용하는 방식이 다르기 때문에 예시로 SHA256 암호화를 진행하였습니다. 

반응형
댓글
공지사항