S3-2021.12.13
1. S3(Simple Storage Service)란?
- 파일 서버의 역할을 하는 서비스
- 일반적인 파일 서버는 트래픽이 증가함에 따라서 장비를 증설하는 작업을 해야 하는데 S3는 이와 같은 것을 대행한다.
- 트래픽에 따른 시스템적인 문제는 걱정할 필요가 없어진다.
- 파일에 대한 접근 권한을 지정 할 수 있어서 서비스를 호스팅 용도로 사용하는 것을 방지 할 수 있다.
1-1. 주요특징
-
많은 사용자가 접속을 해도 이를 감당하기 위해서 시스템적인 작업을 하지 않아도 된다.
(아마존 인프라가 대행)
-
저장할 수 있는 파일 수의 제한이 없다.
-
최소 1바이트에서 최대 5TB의 데이터를 저장하고 서비스 할 수 있다.
-
파일에 인증을 붙여서 무단으로 엑세스 하지 못하도록 할 수 있다.
-
HTTP와 BitTorrent 프로토콜을 지원한다.
-
REST, SOAP 인터페이스를 제공한다.
-
데이터를 여러 시설에서 중복으로 저장해 데이터의 손실이 발생할 경우 자동으로 복원한다.
(하드디스크 저장보다 안전)
-
버전관리 기능을 통해서 사용자에 의한 실수도 복원이 가능하다.
-
정보의 중요도에 따라서 보호 수준을 차등 할 수 있고, 이에 따라서 비용을 절감 할 수 있다. (RSS)
1-2. 주요개념
-
객체(object): AWS는 S3에 저장된 데이터 하나 하나를 객체라고 명명하는데, 하나 하나의 파일이라고 생각하면 된다.
-
버킷(bucket): 객체가 파일이라면 버킷은 연관된 객체들을 그룹핑한 최상위 디렉토리라고 할 수 있다.
버킷 단위로 지역(region)을 지정 할 수 있고, 또 버킷에 포함된 모든 객체에 대해서 일괄적으로 인증과 접속 제한을 걸 수 있다.
-
버전관리: S3에 저장된 객체들의 변화를 저장.
예를들어 A라는 객체를 사용자가 삭제하거나 변경해도 각각의 변화를 모두 기록하기 때문에 실수를 만회할 수 있다.
-
BitTorrent: 분산된 파일 배포 시스템이라고 정의 할 수 있다.
여기서 분산이란 하나의 서버에서 파일을 배포하는 것이 아니라, 파일을 가지고 있는 컴퓨터들로부터 조금씩 파일을 다운받은 후에 이것을 붙여서 완전한 파일을 만드는 방식이다.
대용량의 파일을 배포할 때 BitTorrent를 사용하면 비용을 크게 절감 할 수 있다.
-
RSS(Reduced Redundancy Storage): 일반 S3 객체에 비해서 데이터가 손실될 확률이 높은 형태의 저장 방식.
대신에 가격이 저렴하기 때문에 복원이 가능한 데이터, 이를테면 섬네일 이미지와 같은 것을 저장하는데 적합하다.
그럼에도 불구하고 물리적인 하드 디스크 대비 400배 가량 안전하다는 것이 아마존의 주장
-
Glacier: 영어로는 빙하라는 뜻으로 매우 저렴한 가격으로 데이터를 저장 할 수 있는 아마존의 스토리지 서비스
2. Spring Boot AWS S3 사진 업로드
2-1. AWS S3 Bucket 생성
- 버킷 이름 입력
- 리전 설정: 아시아 태평양(서울) 설정
- 객체 소유권: ACL 활성화 됨 설정
- 퍼블릭 액세스 차단 설정 해제
2-2. AWS S3 Bucket 권한 설정
- 생성된 버킷 권한 설정
- 버킷 클릭 -> 권한 탭 -> 버킷 정책 -> 편집 -> 정책 생성기
- principal:
- Actions: GetObject, PutObject
- ARN: 복사한 ARN + / 입력 (버킷 클릭 -> 속성 탭 -> ARN 복사)
- Add Statement 클릭 -> 확인 -> Generate Policy -> 복사 및 붙여넣기
2-3. IAM 사용자 권한 추가
- IAM 서비스 -> 사용자 -> 사용자 추가
- 사용자 이름
- AWS 액세스 유형 선택: 액세스 키 - 프로그래밍 방식 액세스 -> 다음 클릭
- 권한 설정: 기존 정책 직접 연결 -> AmazonS3FullAccess 체크 -> 다음 클릭
- 사용자 만들기 -> .csv 파일 다운로드 및 보관 하기 (액세스 키, 비밀 액세스 키 소스 코드에서 사용)
2-4. Spring Boot로 파일 업로드
- 의존성 추가
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.0.1.RELEASE'
많은 사용자가 접속을 해도 이를 감당하기 위해서 시스템적인 작업을 하지 않아도 된다.
(아마존 인프라가 대행)
저장할 수 있는 파일 수의 제한이 없다.
최소 1바이트에서 최대 5TB의 데이터를 저장하고 서비스 할 수 있다.
파일에 인증을 붙여서 무단으로 엑세스 하지 못하도록 할 수 있다.
HTTP와 BitTorrent 프로토콜을 지원한다.
REST, SOAP 인터페이스를 제공한다.
데이터를 여러 시설에서 중복으로 저장해 데이터의 손실이 발생할 경우 자동으로 복원한다.
(하드디스크 저장보다 안전)
버전관리 기능을 통해서 사용자에 의한 실수도 복원이 가능하다.
정보의 중요도에 따라서 보호 수준을 차등 할 수 있고, 이에 따라서 비용을 절감 할 수 있다. (RSS)
객체(object): AWS는 S3에 저장된 데이터 하나 하나를 객체라고 명명하는데, 하나 하나의 파일이라고 생각하면 된다.
버킷(bucket): 객체가 파일이라면 버킷은 연관된 객체들을 그룹핑한 최상위 디렉토리라고 할 수 있다.
버킷 단위로 지역(region)을 지정 할 수 있고, 또 버킷에 포함된 모든 객체에 대해서 일괄적으로 인증과 접속 제한을 걸 수 있다.
버전관리: S3에 저장된 객체들의 변화를 저장.
예를들어 A라는 객체를 사용자가 삭제하거나 변경해도 각각의 변화를 모두 기록하기 때문에 실수를 만회할 수 있다.
BitTorrent: 분산된 파일 배포 시스템이라고 정의 할 수 있다.
여기서 분산이란 하나의 서버에서 파일을 배포하는 것이 아니라, 파일을 가지고 있는 컴퓨터들로부터 조금씩 파일을 다운받은 후에 이것을 붙여서 완전한 파일을 만드는 방식이다.
대용량의 파일을 배포할 때 BitTorrent를 사용하면 비용을 크게 절감 할 수 있다.
RSS(Reduced Redundancy Storage): 일반 S3 객체에 비해서 데이터가 손실될 확률이 높은 형태의 저장 방식.
대신에 가격이 저렴하기 때문에 복원이 가능한 데이터, 이를테면 섬네일 이미지와 같은 것을 저장하는데 적합하다.
그럼에도 불구하고 물리적인 하드 디스크 대비 400배 가량 안전하다는 것이 아마존의 주장
Glacier: 영어로는 빙하라는 뜻으로 매우 저렴한 가격으로 데이터를 저장 할 수 있는 아마존의 스토리지 서비스
2-1. AWS S3 Bucket 생성
- 버킷 이름 입력
- 리전 설정: 아시아 태평양(서울) 설정
- 객체 소유권: ACL 활성화 됨 설정
- 퍼블릭 액세스 차단 설정 해제
2-2. AWS S3 Bucket 권한 설정
- 생성된 버킷 권한 설정
- 버킷 클릭 -> 권한 탭 -> 버킷 정책 -> 편집 -> 정책 생성기
- principal:
- Actions: GetObject, PutObject
- ARN: 복사한 ARN + / 입력 (버킷 클릭 -> 속성 탭 -> ARN 복사)
- Add Statement 클릭 -> 확인 -> Generate Policy -> 복사 및 붙여넣기
- 버킷 클릭 -> 권한 탭 -> 버킷 정책 -> 편집 -> 정책 생성기
2-3. IAM 사용자 권한 추가
- IAM 서비스 -> 사용자 -> 사용자 추가
- 사용자 이름
- AWS 액세스 유형 선택: 액세스 키 - 프로그래밍 방식 액세스 -> 다음 클릭
- 권한 설정: 기존 정책 직접 연결 -> AmazonS3FullAccess 체크 -> 다음 클릭
- 사용자 만들기 -> .csv 파일 다운로드 및 보관 하기 (액세스 키, 비밀 액세스 키 소스 코드에서 사용)
2-4. Spring Boot로 파일 업로드
- 의존성 추가
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.0.1.RELEASE'
- aws.yml 작성
- application.yml에 작성해도 되지만, 저는 aws 설정들만 따로 관리하기 위해서 aws.yml 파일을 따로 만들었습니다.
위에 형식대로 자신의 IAM 키, 버킷 이름, 리전 등등을 입력해줍니다.
cloud:
aws:
credentials:
accessKey: IAM 사용자 엑세스 키
secretKey: IAM 사용자 비밀 엑세스 키
s3:
bucket: 버킷 이름
region:
static: ap-northeast-2
stack:
auto: false
- MainApplication
- application.yml과 aws.yml 두개의 파일 모두를 설정 파일로 읽어서 사용하겠다는 뜻
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
@SpringBootApplication
public class ImageApplication {
public static final String APPLICATION_LOCATIONS = "spring.config.location="
+ "classpath:application.yml,"
+ "classpath:aws.yml";
public static void main(String[] args) {
new SpringApplicationBuilder(ImageApplication.class)
.properties(APPLICATION_LOCATIONS)
.run(args);
}
}
- AmazonS3Config 작성
- 따로 config 디렉토리에서 설정 값을 넣기 위해서 AmazonS3Config 설정 클래스를 만들었습니다.
aws.yml 파일에 작성한 값들을 읽어와서 AmazonS3Client 객체를 만들어 Bean으로 주입하는 것입니다.
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AmazonS3Config {
@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 awsCreds = new BasicAWSCredentials(accessKey, secretKey);
return (AmazonS3Client) AmazonS3ClientBuilder.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(awsCreds))
.build();
}
}
- S3Uploader 작성 (파일 업로드는 하는 코드)
- convert() 메소드에서 로컬 프로젝트에 사진 파일이 생성되지만, removeNewFile()을 통해서 바로 지우고 있는 로직입니다.
- System.getProperty("user.dir"): 현재 프로젝트의 절대 경로를 꺼내올 수 있습니다.
=> 즉, 저는 프로젝트 루트 경로 아래에 제가 업로드한 사진 파일도 생성이 됩니다.
그리고 바로 removeNewFile를 통해서 로컬에 있는 파일은 삭제를 합니다.
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.model.CannedAccessControlList;
import com.amazonaws.services.s3.model.PutObjectRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Optional;
import java.util.UUID;
@Slf4j
@RequiredArgsConstructor
@Component
public class S3Uploader {
private final AmazonS3Client amazonS3Client;
@Value("${cloud.aws.s3.bucket}")
public String bucket; // S3 버킷 이름
public String upload(MultipartFile multipartFile, String dirName) throws IOException {
File uploadFile = convert(multipartFile) // 파일 변환할 수 없으면 에러
.orElseThrow(() -> new IllegalArgumentException("error: MultipartFile -> File convert fail"));
return upload(uploadFile, dirName);
}
// S3로 파일 업로드하기
private String upload(File uploadFile, String dirName) {
String fileName = dirName + "/" + UUID.randomUUID() + uploadFile.getName(); // S3에 저장된 파일 이름
String uploadImageUrl = putS3(uploadFile, fileName); // s3로 업로드
removeNewFile(uploadFile);
return uploadImageUrl;
}
// S3로 업로드
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()) {
log.info("File delete success");
return;
}
log.info("File delete fail");
}
// 로컬에 파일 업로드 하기
private Optional<File> convert(MultipartFile file) throws IOException {
File convertFile = new File(System.getProperty("user.dir") + "/" + file.getOriginalFilename());
if (convertFile.createNewFile()) { // 바로 위에서 지정한 경로에 File이 생성됨 (경로가 잘못되었다면 생성 불가능)
try (FileOutputStream fos = new FileOutputStream(convertFile)) { // FileOutputStream 데이터를 파일에 바이트 스트림으로 저장하기 위함
fos.write(file.getBytes());
}
return Optional.of(convertFile);
}
return Optional.empty();
}
}
- Controller 작성
- 파일 업로드를 할 때는 MultipartFile을 사용합니다.
@RequiredArgsConstructor
@RestController
public class HelloController {
private final S3Uploader s3Uploader;
@PostMapping("/images")
public String upload(@RequestParam("images") MultipartFile multipartFile) throws IOException {
s3Uploader.upload(multipartFile, "static"); // 두 번째 매개변수의 이름에 따라 S3 Bucket 내부에 해당 이름의 디렉토리가 생성
return "test";
}
}
-
PostMan 테스트
-
S3 확인하기
3. 실제 구현 시 참고사항
- BoardController
- @RequestPart: 파일 업로드, JSON 데이터 같이 쓸 때 사용
- (value = "multipartFile" : 프론트 form-data 이름 맞춰야 함
- , required = false) : default 값 true, 사진 반드시 첨부할 필요 없으므로 false 설정
@RequiredArgsConstructor
@RestController
public class BoardController {
private final BoardService boardService;
@PostMapping("/api/boards")
public BoardResponseDto BoardUpload(
// @AuthenticationPrincipal UserDetailsImpl userDetails,
@RequestPart(value = "multipartFile", required = false) MultipartFile multipartFile,
@RequestPart(value = "data") BoardRequestDto boardRequestDto
) throws IOException {
// return boardService.saveBoard(userDetails.getUser(), multipartFile, boardRequestDto);
return boardService.saveBoard(multipartFile, boardRequestDto);
}
}
- BoardService
- 클라이언트에서 파일을 첨부(multipartFile)하면 url을 응답한다.
@RequiredArgsConstructor
@Service
public class BoardService {
private final BoardRepository boardRepository;
private final S3Uploader s3Uploader;
private final String imageDirName = "static"; // S3 폴더 경로
@Transactional
// public BoardResponseDto saveBoard(User user, MultipartFile multipartFile, BoardRequestDto boardRequestDto) {
public BoardResponseDto saveBoard(
MultipartFile multipartFile,
BoardRequestDto boardRequestDto
) throws IOException {
String imgUrl = "";
if(multipartFile.getSize() != 0) { // 이미지 첨부 있으면 S3 파일 업로드
imgUrl = s3Uploader.upload(multipartFile, imageDirName);
}
Board board = Board.builder()
.title(boardRequestDto.getTitle())
.content(boardRequestDto.getContent())
// .nickname(user.getNickname())
.img(imgUrl)
.build();
boardRepository.save(board);
return BoardResponseDto.builder()
.board_id(board.getBoard_id())
.title(board.getTitle())
.content(board.getContent())
// .nickname(board.getUser().getNickname())
.img(board.getImg())
.build();
}
}
- postman 사용법 (파일 업로드, JSON)
- KEY: controller 설정한 value, File 인지 Text인지 설정
- File: 파일 업로드
- Text: JSON
- VALUE
- 파일 첨부
- JSON 형식
- CONTENT TYPE: JSON 형식이면 application/json 추가
- @RequestPart: 파일 업로드, JSON 데이터 같이 쓸 때 사용
- (value = "multipartFile" : 프론트 form-data 이름 맞춰야 함
- , required = false) : default 값 true, 사진 반드시 첨부할 필요 없으므로 false 설정
@RequiredArgsConstructor
@RestController
public class BoardController {
private final BoardService boardService;
@PostMapping("/api/boards")
public BoardResponseDto BoardUpload(
// @AuthenticationPrincipal UserDetailsImpl userDetails,
@RequestPart(value = "multipartFile", required = false) MultipartFile multipartFile,
@RequestPart(value = "data") BoardRequestDto boardRequestDto
) throws IOException {
// return boardService.saveBoard(userDetails.getUser(), multipartFile, boardRequestDto);
return boardService.saveBoard(multipartFile, boardRequestDto);
}
}
- 클라이언트에서 파일을 첨부(multipartFile)하면 url을 응답한다.
@RequiredArgsConstructor
@Service
public class BoardService {
private final BoardRepository boardRepository;
private final S3Uploader s3Uploader;
private final String imageDirName = "static"; // S3 폴더 경로
@Transactional
// public BoardResponseDto saveBoard(User user, MultipartFile multipartFile, BoardRequestDto boardRequestDto) {
public BoardResponseDto saveBoard(
MultipartFile multipartFile,
BoardRequestDto boardRequestDto
) throws IOException {
String imgUrl = "";
if(multipartFile.getSize() != 0) { // 이미지 첨부 있으면 S3 파일 업로드
imgUrl = s3Uploader.upload(multipartFile, imageDirName);
}
Board board = Board.builder()
.title(boardRequestDto.getTitle())
.content(boardRequestDto.getContent())
// .nickname(user.getNickname())
.img(imgUrl)
.build();
boardRepository.save(board);
return BoardResponseDto.builder()
.board_id(board.getBoard_id())
.title(board.getTitle())
.content(board.getContent())
// .nickname(board.getUser().getNickname())
.img(board.getImg())
.build();
}
}
- KEY: controller 설정한 value, File 인지 Text인지 설정
- File: 파일 업로드
- Text: JSON
- VALUE
- 파일 첨부- JSON 형식
- CONTENT TYPE: JSON 형식이면 application/json 추가
Author And Source
이 문제에 관하여(S3-2021.12.13), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@bellpro/S3-2021.12.13저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)