많은 사용자가 접속을 해도 이를 감당하기 위해서 시스템적인 작업을 하지 않아도 된다.
(아마존 인프라가 대행)
저장할 수 있는 파일 수의 제한이 없다.
최소 1바이트에서 최대 5TB의 데이터를 저장하고 서비스 할 수 있다.
파일에 인증을 붙여서 무단으로 엑세스 하지 못하도록 할 수 있다.
HTTP와 BitTorrent 프로토콜을 지원한다.
REST, SOAP 인터페이스를 제공한다.
데이터를 여러 시설에서 중복으로 저장해 데이터의 손실이 발생할 경우 자동으로 복원한다.
(하드디스크 저장보다 안전)
버전관리 기능을 통해서 사용자에 의한 실수도 복원이 가능하다.
정보의 중요도에 따라서 보호 수준을 차등 할 수 있고, 이에 따라서 비용을 절감 할 수 있다. (RSS)
객체(object): AWS는 S3에 저장된 데이터 하나 하나를 객체라고 명명하는데, 하나 하나의 파일이라고 생각하면 된다.
버킷(bucket): 객체가 파일이라면 버킷은 연관된 객체들을 그룹핑한 최상위 디렉토리라고 할 수 있다.
버킷 단위로 지역(region)을 지정 할 수 있고, 또 버킷에 포함된 모든 객체에 대해서 일괄적으로 인증과 접속 제한을 걸 수 있다.
버전관리: S3에 저장된 객체들의 변화를 저장.
예를들어 A라는 객체를 사용자가 삭제하거나 변경해도 각각의 변화를 모두 기록하기 때문에 실수를 만회할 수 있다.
BitTorrent: 분산된 파일 배포 시스템이라고 정의 할 수 있다.
여기서 분산이란 하나의 서버에서 파일을 배포하는 것이 아니라, 파일을 가지고 있는 컴퓨터들로부터 조금씩 파일을 다운받은 후에 이것을 붙여서 완전한 파일을 만드는 방식이다.
대용량의 파일을 배포할 때 BitTorrent를 사용하면 비용을 크게 절감 할 수 있다.
RSS(Reduced Redundancy Storage): 일반 S3 객체에 비해서 데이터가 손실될 확률이 높은 형태의 저장 방식.
대신에 가격이 저렴하기 때문에 복원이 가능한 데이터, 이를테면 섬네일 이미지와 같은 것을 저장하는데 적합하다.
그럼에도 불구하고 물리적인 하드 디스크 대비 400배 가량 안전하다는 것이 아마존의 주장
Glacier: 영어로는 빙하라는 뜻으로 매우 저렴한 가격으로 데이터를 저장 할 수 있는 아마존의 스토리지 서비스
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.0.1.RELEASE'
cloud:
aws:
credentials:
accessKey: IAM 사용자 엑세스 키
secretKey: IAM 사용자 비밀 엑세스 키
s3:
bucket: 버킷 이름
region:
static: ap-northeast-2
stack:
auto: false
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);
}
}
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();
}
}
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();
}
}
@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 확인하기
@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);
}
}
@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();
}
}