배포 후 파일 다운로드 문제 해결

뚜우웅이·2023년 8월 16일
0

SpringBoot웹

목록 보기
20/23

파일 다운로드 문제 수정

현재 파일을 업로드하는 경우 프로젝트의 resource/static/files 디렉터리에 파일이 저장되어 다른 컴퓨터에서 파일을 다운로드 하지 못하는 문제가 발생합니다.
이 문제를 해결하기 위해서 파일을 업로드하는 위치와 다운로드하는 위치를 바꿔줘야 합니다.
AWS의 S3를 이용하여 파일을 업로드, 다운로드를 구현해줍니다.

AWS S3 생성


퍼블릭 엑세스를 차단하면 외부 접속이 불가능해 차단을 비활성화 해줍니다.

보안 자격 증명(IAM) 생성

AWS Identity and Access Management(IAM)은
AWS 리소스에 대한 액세스를 안전하게 제어할 수 있는 웹 서비스입니다.

왼쪽 메뉴바에서 사용자를 선택 후 사용자 추가를 클릭해줍니다.

버킷에 접근을 위해 필요한 계정을 생성하는 것입니다.

그룹에 속해있지 않기 때문에 직접 연결을 클릭해줍니다.
s3를 검색하여 FullAccess를 선택해줍니다.

key 발급 받기

방금 생성한 사용자를 클릭한 뒤 보안 자격 증명으로 이동해줍니다.

밑에 내리면 있는 엑세스 키 메뉴에서 키를 생성해줍니다.

Spring Boot와 S3 연결

pom.xml에 의존성을 추가

<dependency>
			<groupId>com.amazonaws</groupId>
			<artifactId>aws-java-sdk-s3</artifactId>
			<version>1.12.523</version>
</dependency>

apllication.properties에 region 설정

aws.region=ap-northeast-2

AWS S3 클라이언트 설정

@Configuration
public class AmazonS3Config {

    @Value("${aws.access.key}")
    private String awsAccessKey;

    @Value("${aws.secret.key}")
    private String awsSecretKey;

    @Value("${aws.region}") // application.properties에서 설정한 리전 값
    private String awsRegion;

    @Bean
    public AmazonS3 amazonS3Client() {
        BasicAWSCredentials awsCredentials = new BasicAWSCredentials(awsAccessKey, awsSecretKey);
        return AmazonS3Client.builder().withRegion(Regions.fromName(awsRegion)) // 설정한 리전 사용
                .withCredentials(new AWSStaticCredentialsProvider(awsCredentials)).build();
    }
}

amazonS3Client는
Spring Bean으로 생성된 AWS S3 클라이언트를 나타냅니다. AWS SDK의 AmazonS3 클래스는 Amazon S3 서비스와 상호 작용하기 위한 메서드와 기능을 제공하는 Java API를 제공합니다. AmazonS3Client는 AmazonS3 인터페이스의 구현체 중 하나로, 실제로 Amazon S3 서비스와 통신하여 파일 업로드, 다운로드, 객체 생성 및 관리 등의 작업을 수행합니다.

properties 수정

aws.region=ap-northeast-2
aws.access.key=다운받은 키
aws.secret.key=다운받은 키

다운받은 키를 추가해줍니다.

파일 업로드 및 DB 저장

FileService

@Service
public class FileService {

    private final AmazonS3 amazonS3Client;
    private final FileRepository fileRepository;

    public FileService(AmazonS3 amazonS3Client, FileRepository fileRepository) {
        this.amazonS3Client = amazonS3Client;
        this.fileRepository = fileRepository;
    }

    public void deleteById(Long fileId) {
        FileData fileData = fileRepository.findById(fileId).orElseThrow(() -> new IllegalArgumentException("File not found"));

        // S3 버킷에서 객체(파일) 삭제
        amazonS3Client.deleteObject("myhomewebbucket", fileData.getFilepath());

        // DB에서 파일 정보 삭제
        fileRepository.deleteById(fileId);
    }

    public List<FileData> findByBoardId(Long boardId) {
        return fileRepository.findByBoardId(boardId);
    }

}

BoardController

파일 업로드 코드

@PostMapping("/form")
    public String form(@Valid Board board, BindingResult bindingResult , Authentication authentication,
                       @RequestParam("file") MultipartFile[] files) throws IOException {
        boardValidator.validate(board, bindingResult);
        if (bindingResult.hasErrors()) {
            return "board/form";
        }
        String username = authentication.getName();
        for (MultipartFile file : files) {
            if (!file.isEmpty()) {
                String filename = file.getOriginalFilename();
                int filesize = (int) file.getSize();
                String filetype = file.getContentType();
                UUID uuid = UUID.randomUUID();
                String randomFileName = uuid + "_" + filename;

                ObjectMetadata metadata = new ObjectMetadata();
                metadata.setContentType(filetype);
                metadata.setContentLength(filesize);

                InputStream inputStream = file.getInputStream();

                PutObjectRequest putObjectRequest = new PutObjectRequest("myhomewebbucket", "files/" + randomFileName, inputStream, metadata);
                amazonS3Client.putObject(putObjectRequest);

                FileData newFile = new FileData();
                newFile.setFilename(filename);
                newFile.setFilesize(filesize);
                newFile.setFiletype(filetype);
                newFile.setFilepath("files/" + randomFileName);
                newFile.setUploadDate(LocalDateTime.now());

                board.addFile(newFile);
            }
        }
        // 기존 파일 정보 유지
        if (board.getId() != 0) {
            List<FileData> oldFiles = fileService.findByBoardId(board.getId());
            for (FileData oldFile : oldFiles) {
                board.addFile(oldFile);
            }
        }
        boardService.save(username, board);
        return "redirect:/board/list";
    }

MultipartFile
MultipartFile은 스프링 프레임워크에서 제공하는 인터페이스로, 클라이언트가 웹 폼을 통해 업로드한 파일의 내용을 나타내는 객체입니다. 파일 업로드 시 클라이언트로부터 받은 파일 데이터를 이 객체를 통해 처리할 수 있습니다. MultipartFile 객체는 파일 이름, 파일 크기, 파일 내용 등을 제공합니다.

ObjectMetadata
ObjectMetadata는 AWS S3 객체에 대한 메타데이터를 설정하는 클래스입니다. 파일 업로드 시 해당 파일의 메타데이터를 설정할 수 있습니다. 주로 파일의 컨텐츠 타입(Content-Type)이나 파일 크기 등의 정보를 설정하는 데 사용됩니다.

PutObjectRequest
PutObjectRequest는 AWS SDK의 클래스로, S3 버킷에 객체를 업로드하는 요청을 생성하는 역할을 합니다.
bucketName: 파일을 업로드할 S3 버킷의 이름입니다.
key: S3 버킷 내에서 객체의 키를 지정합니다. 파일의 경로 및 이름을 나타냅니다.
inputStream: 업로드할 파일의 내용을 포함하는 InputStream 객체입니다.
metadata: 업로드할 객체의 메타데이터를 설정하는 ObjectMetadata 객체입니다.

amazonS3Client.putObject(putObjectRequest)
이 코드는 실제로 S3에 파일을 업로드하는 작업을 수행합니다. putObject 메서드를 호출하여 업로드 요청을 실행합니다. 이때 PutObjectRequest 객체가 가지고 있는 정보를 바탕으로 파일이 업로드됩니다. 업로드 후에는 해당 파일이 S3 버킷에 저장됩니다.

BoardApiController

파일 다운로드 및 삭제

@GetMapping("/files/{fileId}")
    public ResponseEntity<Resource> downloadFile(@PathVariable Long fileId) throws IOException {
        FileData fileData = fileRepository.findById(fileId).orElseThrow(() -> new FileNotFoundException("File not found"));
        System.out.println(fileData.getFilepath());
        S3Object s3Object = amazonS3Client.getObject("myhomewebbucket", fileData.getFilepath());
        S3ObjectInputStream inputStream = s3Object.getObjectContent();

        ByteArrayResource resource = new ByteArrayResource(IOUtils.toByteArray(inputStream));

        return ResponseEntity.ok()
                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileData.getFilename() + "\"")
                .contentType(MediaType.parseMediaType(fileData.getFiletype()))
                .body(resource);
    }

    @DeleteMapping("/files/delete/{fileId}")
    public void deleteFile(@PathVariable Long fileId) {
        FileData fileData = fileRepository.findById(fileId).orElseThrow();
        String filePath = fileData.getFilepath(); // S3 객체의 키로 사용

        // S3 버킷에서 객체(파일) 삭제
        amazonS3Client.deleteObject("myhomewebbucket", filePath);

        // DB에서 파일 정보 삭제
        fileService.deleteById(fileId);
    }

현재 다운로드 코드에서는 파일 이름이나 내용에 한글이 있으면 다운로드가 되지 않는 문제가 있습니다.

다운로드 메소드 수정

@GetMapping("/files/{fileId}")
    public void downloadFile(@PathVariable Long fileId, HttpServletResponse response) throws UnsupportedEncodingException, FileNotFoundException {
        FileData fileData = fileService.findById(fileId);
        if (fileData == null) {
            throw new FileNotFoundException();
        }

        String key = fileData.getFilepath();
        S3Object s3Object = amazonS3Client.getObject("myhomewebbucket", key);
        S3ObjectInputStream inputStream = s3Object.getObjectContent();

        response.setContentType(fileData.getFiletype());

        String originalFilename = fileData.getFilename();
        String encodedFilename = URLEncoder.encode(originalFilename, StandardCharsets.UTF_8.toString()).replaceAll("\\+", "%20");

        response.setHeader("Content-Disposition", "attachment; filename=\"" + encodedFilename + "\"");
        response.setHeader("Content-Transfer-Encoding", "binary");
        response.setContentLength((int) fileData.getFilesize());

        try {
            FileCopyUtils.copy(inputStream, response.getOutputStream());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

Content-Disposition 헤더를 설정할 때 파일 이름을 올바르게 UTF-8로 인코딩합니다. 여기서는 URLEncoder.encode 함수를 사용합니다. 공백 문자는 +로 변환되기 때문에 %20으로 교체해야합니다. 이 작업을 완료하면 정상적으로 한글 파일 이름이나 파일 내용을 포함하는 파일을 다운로드할 수 있습니다.

response.setHeader("Content-Disposition", "attachment; filename=\"" + encodedFilename + "\"");: 응답 객체의 헤더에 인코딩된 파일 이름을 포함하여 Content-Disposition을 설정합니다. attachment는 웹 브라우저에 파일을 다운로드할 것임을 알려줍니다.

profile
공부하는 초보 개발자

1개의 댓글

comment-user-thumbnail
2023년 8월 16일

많은 도움이 되었습니다, 감사합니다.

답글 달기