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>
'개발 관련 학습 및 문제해결' 카테고리의 다른 글
깃허브로 깃북 사용해보기 [git-book 사용하는 법] (0) | 2023.01.31 |
---|---|
[ Frontend / React ] multipartFile을 다른 속성값과 객체에 넣어 전달해주려고 할 때 1 (0) | 2022.12.27 |
컴퓨터 처럼 생각하기. 프론트 엔드 함수 실행시 디버깅 포인트 찾기[20221208-TIL] (0) | 2022.12.08 |
리액트,프론트 엔드, Page는 얼마만큼의 props를 가져야하나?, 컴포넌트에 프롭스 넘겨주기[20221207-TIL] (0) | 2022.12.07 |
AWS S3 버킷 만들기 (0) | 2022.12.04 |
댓글