0. AWS 스토리지 서비스
- 스토리지
- AWS의 퍼블릭 클라우드에 데이터 저장 가능
- AWS 스토리지 서비스 종류 (파일 저장 목적에 따라 나뉨)
- 객체 스토리지 S3 (Simple Storage Service)
- 블록 스토리지 EBS (Elastic Block Store)
- 파일 스토리지 EFS (Elastic File Store)
1. AWS S3
- AWS의 객체 스토리지 서비스
- 데이터 저장, 관리 및 분석을 안전하고 확장 가능하게 해 줌
- 특징
- 객체 스토리지 : 파일을 객체 형태로 저장함
- 각 객체는 데이터, 메타데이터, 고유한 식별자로 구성됨
- 내구성 : 99.999%의 내구성 제공함
- 가용성 : 데이터는 여러 가용 영역 (AZ)에 자동으로 복제됨 ⇒ 데이터 손실 위험 최소화
- 확장성 : 거의 무한한 저장 용량 제공, 데이터 양에 관계없이 성능 유지됨
- 사용자가 데이터 저장, 삭제 할 때 자동으로 확장, 축소됨!
- 객체 스토리지 : 파일을 객체 형태로 저장함
1-1. S3 구성 요소
- 버킷 (Bucket)
- S3에서 데이터를 저장하는 기본 단위, 하나의 버킷은 여러 객체를 포함 가능
- 각 버킷은 전 세계적으로 고유한 이름을 가져야 함
- 각 버킷은 특정 리전(지역)에 생성됨
- 객체 (Object)
- S3에 저장되는 데이터의 단위
- 파일(데이터)과 해당 메타데이터로 구성됨
- 메타데이터 : 객체에 대한 정보를 담고 있는 데이터
- ex) 컨텐츠 유형, 크기, 생성 날짜 등
- 각 객체는 고유한 키(이름)를 가지며, 최대 5TB까지 저장 가능
- 버킷 이름 + 객체 키 조합하여 식별자로 사용됨
2. AWS S3 사용 이미지 업로드 예제
2-1. S3 버킷 생성
- 버킷 만들기 클릭
2. 생성 정보 기입
- 일반 구성
- 버킷 이름 : 대문자 사용 불가, 전세계에서 고유한 이름으로 생성해야 함
- 객체 소유권
- ACL 활성화
- 객체 소유권 : 버킷 소유자 선호
- 퍼블릭 액세스 차단 설정
- 모든 퍼블릭 액세스 차단 선택 해제
- 아래의 고지 사항 확인 후 체크
- 나머지 설정은 기본값으로 유지
2-2. IAM (Identity and Access Management)
- 검색창에서 IAM 검색
2. 사용자 생성 클릭
3. 사용자 세부 정보 지정
- 사용자 이름 작성
4. 권한 설정
- 직접 정책 연결 선택
- S3 검색 후 AmazonS3FullAccess 선택
5. 검토 및 생성
- 내용 확인하고 사용자 생성 클릭
6. 액세스 키 생성
- 생성된 사용자 이름 클릭
- 액세스 키 만들기 클릭
- 사용 사례를 본인의 상황에 맞게 선택
- 설명 태그 작성 (선택)
- 액세스 키 만들기 클릭
- 액세스 키가 생성됨
- 액세스 키는 이 페이지를 나가는 순간 다시 확인 불가능함 ⇒ 복사해서 저장해두거나 csv 파일로 다운로드 해놓을 것!
3. S3로 구현한 이미지 업로드 예제
2024.09.02 - [Spring Boot] - [Spring Boot] 스프링 이미지 업로드 예제 (0) - 예시 상황, 공통 코드
- 이전에 올린 이미지 업로드 예제와 상황을 유사하게 설정함
- 위의 글을 읽고 진행하면 수월할 듯
3-1. 의존 라이브러리
- build.gradle
dependencies {
// Spring Cloud AWS 라이브러리
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
}
- application.yml
spring:
servlet:
multipart:
enabled: true
max-file-size: 128MB # 단일 파일의 최대 크기 설정
max-request-size: 128MB # 전체 요청의 최대 크기 설정
cloud:
aws:
credentials:
accessKey: (2-2에서 발급받은 액세스 키)
secretKey: (2-2에서 발급받은 비밀 액세스 키)
s3:
bucketName: (S3 버킷 이름)
region:
static: ap-northeast-2 # 리전 설정
stack:
auto: false # 자동으로 스택 생성하지 않게 설정
3-2. S3Config 생성
- S3Config
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3;
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 S3Config {
// AWS 자격 증명을 application.yml에서 읽어오기 위한 필드
@Value("${cloud.aws.credentials.accessKey}")
private String accessKey;
@Value("${cloud.aws.credentials.secretKey}")
private String secretKey;
@Value("${cloud.aws.region.static}")
private String region;
@Bean
// AmazonS3 클라이언트를 생성하는 메서드
public AmazonS3 amazonS3() {
// AWS 자격 증명을 기본 자격 증명 클래스를 사용하여 생성
AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);
// AmazonS3 클라이언트 빌더를 사용하여 S3 클라이언트 구성
return AmazonS3ClientBuilder
.standard() // 기본 클라이언트 빌더 사용
.withCredentials(new AWSStaticCredentialsProvider(credentials)) // 인증 정보 제공
.withRegion(region) // 사용할 AWS 리전 설정
.build();
}
}
3-3. S3ImageService 구현
- S3ImageService
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.CannedAccessControlList;
import com.amazonaws.services.s3.model.PutObjectRequest;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@Service
@RequiredArgsConstructor
public class S3ImageService {
// Amazon S3 클라이언트를 주입받음
private final AmazonS3 amazonS3;
// S3 버킷 이름을 application.yml에서 읽어옴
@Value("${cloud.aws.s3.bucketName}")
private String bucketName;
// 이미지 파일을 S3에 업로드하는 메서드
@Transactional
public List<String> uploadImages(List<MultipartFile> imageList) throws IOException {
// 업로드한 파일 리스트가 null인 경우, null을 반환
if (imageList == null) return null;
// 업로드 파일이 이미지 형식인지 확인
if (!isImageFile(imageList)) throw new IllegalArgumentException("사진 이외의 파일은 업로드 불가능합니다!");
// S3에 업로드된 이미지 URL을 저장할 리스트 생성
List<String> imageUrlList = new ArrayList<>();
// 각 이미지 파일을 S3에 업로드
for (MultipartFile image : imageList) {
// 원본 파일 이름 가져오기
String originalFileName = image.getOriginalFilename();
// 중복을 피하기 위해 UUID를 추가한 새로운 파일 이름 생성
String modifiedFileName = UUID.randomUUID() + "_" + originalFileName;
// S3에 업로드할 PutObjectRequest 생성
PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, modifiedFileName,
image.getInputStream(), null) // InputStream과 메타데이터(null)를 사용하여 요청 생성
.withCannedAcl(CannedAccessControlList.PublicRead); // 파일을 공개적으로 읽을 수 있도록 설정
// S3에 파일 업로드
amazonS3.putObject(putObjectRequest);
// 업로드된 파일의 URL을 리스트에 추가
imageUrlList.add(String.valueOf(amazonS3.getUrl(bucketName, modifiedFileName)));
}
// 업로드된 이미지의 URL 리스트 반환
return imageUrlList;
}
// 파일이 이미지 형식인지 검사하는 메서드
private static boolean isImageFile(List<MultipartFile> imageList) {
// 각 이미지 파일의 MIME 타입을 검사
for (MultipartFile image : imageList) {
String contentType = image.getContentType();
// MIME 타입이 null이거나 'image/'로 시작하지 않으면 false 반환
if (contentType == null || !contentType.startsWith("image/")) return false;
}
// 모든 파일이 이미지 형식이라면 true 반환
return true;
}
}
3-4. 나머지 코드 작성
3-4-1. entity
- Post
@Entity
@Getter
@Setter
@NoArgsConstructor
public class Post {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "post_id")
private Long id;
private String title;
private String content;
@ElementCollection
private List<String> imageUrlList;
public Post(String title, String content) {
this.title = title;
this.content = content;
}
}
3-4-2. dto
- PostDto
@Data
public class PostDto {
private String title;
private String content;
// 다수의 이미지 (리스트) 입력받기 위해 추가
private List<MultipartFile> imageList;
}
3-4-3. repository
- PostRepository
public interface PostRepository extends JpaRepository<Post, Long> {
}
3-4-4. service
- PostService
@Service
@RequiredArgsConstructor
public class PostService {
private final PostRepository postRepository;
private final S3ImageService s3ImageService;
public Post findPost(Long id) {
return postRepository.findById(id).orElse(null);
}
@Transactional
public void save(PostDto postDto) {
Post post = new Post(postDto.getTitle(), postDto.getContent());
// 이미지 저장 로직 추가
try {
List<String> imageUrlList = s3ImageService.uploadImages(postDto.getImageList());
post.setImageUrlList(imageUrlList);
} catch (IOException e) {
throw new RuntimeException(e);
}
postRepository.save(post);
}
}
3-4-5. controller
- PostController
@Controller
@RequiredArgsConstructor
@RequestMapping("/s3/post")
public class PostController {
private final PostService postService;
// 게시글 단건 조회
@GetMapping("/{postId}")
public String getPost(@PathVariable("postId") Long postId, Model model) {
model.addAttribute("post", postService.findPost(postId));
return "home";
}
// 게시글 작성
@ResponseBody
@PostMapping(consumes = "multipart/form-data")
public ResponseEntity<Void> savePost(@ModelAttribute PostDto postDto) {
postService.save(postDto);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
}
3-4-6. html
- home.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org" >
<h1>제목: [[${post.title}]]</h1>
<h1>내용: [[${post.content}]]</h1>
<div th:each="imageUrl : ${post.imageUrlList}">
<img th:src="${imageUrl}" style="width: 200px"/>
</div>
</html>
3-5. 실행 결과
3-5-1. 이미지 첨부 게시글 작성 (POST localhost:8080/s3/post)
- 이미지 파일 2개와 함께 게시글이 작성됨을 확인
3-5-2. AWS S3 버킷에 이미지 업로드 됨을 확인
- POST 요청을 보낼 때 포함되어 있던 2개의 이미지가 업로드 됨을 확인
3-5-3. MySQL 확인
- Post의 imageUrlList 에 S3 버킷에 업로드 된 url이 저장됨을 확인
3-5-4. 이미지 출력 확인(http://localhost:8080/s3/post/1)
- 업로드 한 2개의 이미지가 출력됨을 확인