본문 바로가기
개발 관련 학습 및 문제해결

[SPRING BOOT, Java] AWS S3 이미지 및 녹음 파일 올리기

by 날파리1 2022. 12. 14.

AWS S3를 통해 업로드하는 법을 검색하면 수많은 글들이 나오지만 Spring Boot 에 관련해서는 적당한 글이 없어서 한 번 자세히 써보고자 한다.

 

1. 세팅 및 전제조건

1. 우선 이것은 I AM 과 S3 서버를 세팅해서 올릴 수 있는 상태를 의미한다.

 

2. 아래처럼 세팅도 되있어야 올릴 때 또 번거로운 오류가 없다.

버킷 -> 권한 -> 객체 소유권

2. build.gradle implement

의존성을 주입하는 방법이 여러 방식이 있던데 다 나와 잘 맞지 않고 작동이 안되었다.

스피링 부트를 사용하고 있어서 다음과 같이 의존성 주입을 해주었다.

 

아래와 같은 스펙을 이용 

implementation'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' 를 해주었다.

입력후 코끼리 버튼은 필수 *

plugins {
   id 'org.springframework.boot' version '2.7.5'
   id 'io.spring.dependency-management' version '1.0.15.RELEASE'
   id 'java'
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-security'
	implementation 'org.springframework.boot:spring-boot-starter-validation'
	implementation 'org.springframework.boot:spring-boot-starter-web'

	implementation'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'

	developmentOnly 'org.springframework.boot:spring-boot-devtools'

	runtimeOnly 'com.h2database:h2'
	runtimeOnly 'org.postgresql:postgresql'

	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'org.springframework.security:spring-security-test'
}

3. application.properties 세팅

누가 봐도 이 글을 본다면 한국에 살고 있을거니 아래와 같이 입력 ( 본인 버킷명 , 엑세스, 시크릿 키는 알고있어야 한다.)

cloud.aws.s3.bucket=버킷명 입력
cloud.aws.credentials.access-key= 본인 엑세스 키
cloud.aws.credentials.secret-key= 본인 시크릿 키

# multipart 용량
spring.servlet.multipart.max-file-size: 10MB
spring.servlet.multipart.max-request-size: 10MB

# aws 지역 명
# ??? S3 bucket region ??
cloud.aws.region.static=ap-northeast-2
cloud.aws.stack.auto=false

4. Util package 에서 AwsS3Config 세팅

package com.example.seouldream.cocheline.config;

import com.amazonaws.auth.*;
import com.amazonaws.services.s3.*;
import org.springframework.beans.factory.annotation.*;
import org.springframework.context.annotation.*;

@Configuration
public class AwsS3Config {
  @Value("${cloud.aws.credentials.access-key}")
  private String accessKey;
  @Value("${cloud.aws.credentials.secret-key}")
  private String secretKey;
  @Value("${cloud.aws.region.static}")
  private String region;

  @Bean
  public AmazonS3Client amazonS3Client() {
    BasicAWSCredentials awsCredentials = new BasicAWSCredentials(accessKey,secretKey);
    return (AmazonS3Client) AmazonS3ClientBuilder.standard()
        .withRegion(region)
        .withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
        .build();
  }
}

5.서비스 로직 세팅

services 라는 패키지 아래에 아래와 같이 복사해서 붙여넣어준다.(서비스 로직이니까)

package com.example.seouldream.cocheline.utils;

import com.amazonaws.services.s3.*;
import com.amazonaws.services.s3.model.*;
import org.springframework.beans.factory.annotation.*;
import org.springframework.stereotype.*;
import org.springframework.web.multipart.*;

import java.io.*;
import java.util.*;

@Component
public class S3Uploader {
  private final AmazonS3Client amazonS3Client;

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

  public S3Uploader(AmazonS3Client amazonS3Client) {
    this.amazonS3Client = amazonS3Client;
  }

// 주어진 파일을 변환해서 업로드해주는 총체적 로직
  public String uploadFiles(
      MultipartFile multipartFile, String dirName) throws IOException {
    File uploadFile = convert(multipartFile).orElseThrow(() ->
        new IllegalArgumentException("error: MultipartFile -> File convert fail"));
    return upload(uploadFile, dirName);
  }

// 받아온 파일을 변환한 후 s3에 실제로 업로드 해주는 로직
  public String upload(File uploadFile, String filePath) {
	// 파일명이 겹치지 않도록 UUID로 고유 아이디값을 넣어준 후 생성
    String fileName = filePath + "/" + UUID.randomUUID() + uploadFile.getName();
    // s3 실제 업로드 로직
    String uploadRecordUrl = putS3(uploadFile, fileName);
    removeNewFile(uploadFile);
    return uploadRecordUrl;
  }

  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(File targetFile) {
    if (targetFile.delete()) {
      System.out.println("File delete success");
      return;
    }
    System.out.println("File delete fail");
  }

// 받아온 파일을 파일 형태로 생성후 스트림형태로 변환해서 생성
  private Optional<File> convert(MultipartFile file) throws IOException {
    File convertFile = new File(System.getProperty("user.dir") + "/" + file.getOriginalFilename());

    if (convertFile.createNewFile()) {
      try (FileOutputStream fileOutputStream = new FileOutputStream(convertFile)) {
        fileOutputStream.write(file.getBytes());
      }
      return Optional.of(convertFile);
    }
    return Optional.empty();
  }
}

 

컨버트 메서드를 아래와 같이 바꾸어 주어도 상관없다. close를 반드시 하라는 말이 있어서 이것은 close해주었다.

  public Optional<File> convert(MultipartFile file) throws IOException {
    File convertedFile = new File(file.getOriginalFilename());
    convertedFile.createNewFile();
    FileOutputStream fileOutputStream = new FileOutputStream(convertedFile);
    fileOutputStream.write(file.getBytes());
    fileOutputStream.close();
    return Optional.of(convertedFile);
  }

6. 컨트롤러 세팅

package com.example.seouldream.cocheline.controllers;

import com.example.seouldream.cocheline.utils.*;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.*;

import java.io.*;

@RestController
public class S3FileController {
  private final S3Uploader s3Uploader;

  public S3FileController(S3Uploader s3Uploader) {
    this.s3Uploader = s3Uploader;
  }

  @PostMapping("/upload")
  @ResponseStatus(HttpStatus.CREATED)
  public String upload(MultipartFile multipartFile) throws IOException {
    String fileUrl = s3Uploader.uploadFiles(multipartFile, "temporary");
    return fileUrl;
  }
}

POST 이지만 RequestBody 없이 그냥 하면된다. ( 의아했는데 그냥 multipartFile)을 받아오는 듯 하다.

"temporary"라고 적힌 부분은 버킷내에서 생성할 디렉토리 이름이다. 업로드 된 파일들은 해당 디렉토리 이름 아래에 저장된다.

 

**참고

업로드 문제

1. 업로드하는데 처음에는 다소 시간이 걸리는 듯 하다. 똑같이 해도 처음 10분동안은 네트워크 에러도 뜨고 성공적으로 올린 것이 반영도 안되었다.

녹음파일 문제

2. 이미지 파일이나 녹음파일이나 모두 잘 업로드 된다. 다만 카카오톡이나 휴대폰 녹음기로 작업한 m4a 확장자의 경우 mp4로 올라가기 때문에 서버에서 동영상으로 인식하여 업로드된 url 을 클릭하면 동영상 다운로드가 된다. 이를 방지하려면 mp3로 변환 후 올리면 된다

그렇지만 mp4확장자도 url을 프론트에 오디오 태그를 이용해 넣어주면 잘 작동된다.

 

예시 코드

<figure>
<figcaption />
<audio
controls
src="https://blah~ blah~.s3.ap-northeast-2.amazonaws.com/example.m4a"
>
<a href="https://blah~ blah~.s3.ap-northeast-2.amazonaws.com/example.m4a">
Download audio
</a>
</audio>
</figure>

 

프론트엔드에서

1. formdata 선언

2. input 타입을 file 형태로 한 후  녹음파일만 올리도록 할 것이면 accept를 아래와 같이 제한

3. onChange 함수로 아래처럼 받아줌 e.target.files[0] 과 formData.append 해주는 것이 포인트!

4. 받아온 formData를 백엔드로 그대로 전달해줄 것. 

const formData = new FormData();

const handleChangeRecord = (e) => {
    const record = e.target.files[0];

   formData.append('multipartFile', record);
   practicalTemplatesAdminFormStore.changeRecord(formData);
  };
  
	
	<div>
            <label htmlFor="input-record">녹음파일</label>
            <input
              id="input-record"
              type="file"
              name="record"
              accept="audio/*"
              onChange={handleChangeRecord}
            />
     </div>

댓글