AWS S3
- AWS Simple Storeage Service의 줄임말로 파일 서버의 역할을 하는 서비스
- 프로젝트 개발 중 파일을 저장하고 불러오는 작업이 필요한 경우에 프로젝트 내부 폴더에 저장할 수 있지만, AWS S3를 사용하여 파일을 관리할 수도 있음
AWS S3의 장점
- 무제한 용량(하나의 파일에 대한 용량 제한은 있지만, 전체 용량은 무제한)
- 파일 저장에 최적화 (개발자가 따로 용량을 추가하거나 성능을 높이는 작업을 하지 않아도 됨)
- 99.999%라는 높은 내구도(파일이 유실될 가능성이 낮음)
- 이 외에도 저렴한 비용, 높은 객체 가용성, 뛰어난 보안성 등의 장점이 있음
AWS S3 생성
- 객체(Object) : 파일과 파일정보로 구성된 저장단위로 파일이라 생각하면 됨
- 버킷(Bucket) : 저장된 객체 대한 컨테이너
- 버킷은 최대 100개 생성 가능하며, 버킷에 저장할 수 있는 객체 수는 제한이 없음
Bucket 생성
- AWS Console 접속 후 S3 서비스 선택
- 버킷 만들기 클릭
- 원하는 버킷 이름 입력
- AWS 리전 선택(아시아 태평양(서울) ap-northeast-2)
- 객체 소유권 선택(ACL 비활성화)
- '모든 퍼블릭 액세스 차단' 해제
- 나머지도 Default
사용자 생성
- AWS Console 접속 후 IAM 서비스 선택
- 액세스 관리 → 사용자 → 사용자 추가
- 원하는 이름 입력 후 다음
- 직접 정책 연결 → AmazonS3FullAccess 선택 후 다음
- 사용자 생성
액세스 키 생성
- 방금 생성한 사용자 선택 후 보안 자격 증명 탭 클릭
- 액세스 키 만들기 클릭
- 기타 선택 후 다음
- 원하는 설명 태그 입력 후 액세스 키 만들기
- 액세스 키를 만들면 아래와 같이 액세스 키와 비밀 액세스 키를 확인할 수 있는데 , 이 값들을 저장후 완료
버킷 정책 변경
- AWS Console에서 생성한 버킷으로 이동
- 권한 → 버킷 정책 → 편집
- 정책이 비어있으면 '+ 새 문 추가' 클릭
- 정책 내용을 아래와 같이 변경
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Statement1",
"Principal": "*",
"Effect": "Allow",
"Action": "s3:*",
"Resource": "arn:aws:s3:::<버킷 이름>/*"
}
]
}
Spring 프로젝트와 연동
라이브러리 추가(build.gradle dependency)
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
application.yml 설정
cloud:
aws:
s3:
bucket: <S3 버킷 이름>
credentials:
access-key: <저장해놓은 액세스 키>
secret-key: <저장해놓은 비밀 액세스 키>
region:
static: ap-northeast-2
auto: false
stack:
auto: false
S3Config.java 생성
@Configuration
public class S3Config {
@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 credentials = new BasicAWSCredentials(accessKey, secretKey);
return (AmazonS3Client) AmazonS3ClientBuilder
.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(credentials))
.build();
}
}
파일 업로드 구현
@Service
@RequiredArgsConstructor
public class S3UploadService {
private final AmazonS3 amazonS3;
@Value("${cloud.aws.s3.bucket}")
private String bucket;
public String saveFile(MultipartFile multipartFile) throws IOException {
String originalFilename = multipartFile.getOriginalFilename();
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(multipartFile.getSize());
metadata.setContentType(multipartFile.getContentType());
amazonS3.putObject(bucket, originalFilename, multipartFile.getInputStream(), metadata);
return amazonS3.getUrl(bucket, originalFilename).toString();
}
}
- putObject() 메서드가 파일을 저장해주는 메서드
- getURI()을 통해 파일이 저장된 URL을 return 해주고, 이 URL로 이동 시 해당 파일이 오픈됨(버킷 정책 변경을 하지 않았으면 파일은 업로드 되지만 해당 URL로 이동 시 accessDenied 됨)
- 만약 MultipartFile에 대해 잘 모르거나 웹 페이지에서 form으로 파일을 입력받고 싶다면 [Spring Boot] 파일 업로드 참고
파일 다운로드 구현
public ResponseEntity<UrlResource> downloadImage(String originalFilename) {
UrlResource urlResource = new UrlResource(amazonS3.getUrl(bucket, originalFilename));
String contentDisposition = "attachment; filename=\"" + originalFilename + "\"";
// header에 CONTENT_DISPOSITION 설정을 통해 클릭 시 다운로드 진행
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition)
.body(urlResource);
}
- 로컬 파일 다운로드 할 때는 UrlResource()메서드에 "file:" + 로컬 파일 경로를 넣어주면 로컬파일이 다운로드 됨
- S3에 올라간 파일은 위와 같이 amazonS3.getUrl(버킷이름, 파일이름)을 통해 파일 다운로드를 할 수 있음
HTML 파일에서 이미지 미리보기 구현
String url = amazonS3.getUrl(bucket, filename).toString();
- 위에서 구한 amazonS3.getUrl(bucket, filename).toString();
- 아래와 같이 Thymeleaf를 이용해 URL을 받고 src에 넣어주면 화면에서 미리보기 가능
<img th:src="${s3ImageUrl}"/>
파일 삭제 구현
public void deleteImage(String originalFilename) {
amazonS3.deleteObject(bucket, originalFilename);
}
실제 프로젝트 적용 예시
클론 코딩 프로젝트 일부
설정 파일
package global AWS config : S3Config
package com.sparta.market.global.aws.config;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.client.builder.AwsClientBuilder;
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 S3Config {
private final String endPoint = "https://kr.object.ncloudstorage.com";
@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 credentials = new BasicAWSCredentials(accessKey, secretKey);
return (AmazonS3Client) AmazonS3ClientBuilder
.standard()
.withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(endPoint, region))
.withCredentials(new AWSStaticCredentialsProvider(credentials))
.build();
}
}
package global AWS config : MultipartJackson2HttpMessageConverter
package com.sparta.market.global.aws.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter;
import org.springframework.stereotype.Component;
import java.lang.reflect.Type;
@Component
public class MultipartJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter {
/**
* "Content-Type: multipart/form-data" 헤더를 지원하는 HTTP 요청 변환기
* Swagger에서 multipart/form-data 를 확인하기 위해 추가
*/
public MultipartJackson2HttpMessageConverter(ObjectMapper objectMapper) {
super(objectMapper, MediaType.APPLICATION_OCTET_STREAM);
}
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return false;
}
@Override
public boolean canWrite(Type type, Class<?> clazz, MediaType mediaType) {
return false;
}
@Override
protected boolean canWrite(MediaType mediaType) {
return false;
}
}
서비스 파일
package global AWS service : S3UploadService
package com.sparta.market.global.aws.service;
import com.amazonaws.AmazonServiceException;
import com.amazonaws.SdkClientException;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.ObjectMetadata;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
@Slf4j(topic = "AWS S3 로그")
@Service
@RequiredArgsConstructor
public class S3UploadService {
private final AmazonS3 amazonS3;
@Value("${cloud.aws.s3.bucket}")
private String bucket;
public String saveFile(MultipartFile multipartFile, String filename) throws IOException {
// String filename -> UUID 추가된 filename 저장 (중복 방지)
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(multipartFile.getSize());
metadata.setContentType(multipartFile.getContentType());
amazonS3.putObject(bucket, filename, multipartFile.getInputStream(), metadata);
// 한글 깨짐 방지
return URLDecoder.decode(amazonS3.getUrl(bucket, filename).toString(), StandardCharsets.UTF_8);
}
public void deleteFile(String originFilename) {
amazonS3.deleteObject(bucket, originFilename);
}
}
Application 컨트롤러
package domain community controller : CommunityController 클래스 코드 일부
@Slf4j(topic = "Community Controller")
@Tag(name = "Community Controller", description = "커뮤니티 게시글 컨트롤러")
@RestController
public class CommunityController {
private final CommunityService communityService;
public CommunityController(CommunityService communityService) {
this.communityService = communityService;
}
@Operation(summary = "커뮤니티 게시글 등록", description = "커뮤니티 게시글을 등록합니다.")
@PostMapping(value = "/community", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE, MediaType.APPLICATION_JSON_VALUE})
/*@RequestPart 사용 이미지 업로드 추가 리팩토링 예정*/
public ResponseEntity<?> createCommunityPost(@RequestPart(value = "files", required = false)MultipartFile[] multipartFilesList,
@RequestPart(value = "CommunityRequestDto") CommunityRequestDto requestDto) throws IOException, java.io.IOException {
CommunityResponseDto responseDto = communityService.createCommunityPost(requestDto, multipartFilesList);
return ResponseEntity.ok().body(ResponseDto.success("커뮤니티 글 작성 성공", responseDto));
}
Application 서비스 로직
package domain community service : CommunityService 클래스 코드 일부
@Slf4j(topic = "Community Service")
@Tag(name = "Community Service", description = "커뮤니티 게시글 서비스 로직 클래스")
@Service
public class CommunityService {
private final CommunityRepository communityRepository;
private final UserRepository userRepository;
private final S3UploadService s3UploadService;
private final CommunityImageRepository communityImageRepository;
public CommunityService(CommunityRepository communityRepository, UserRepository userRepository,
S3UploadService s3UploadService, CommunityImageRepository communityImageRepository) {
this.communityRepository = communityRepository;
this.userRepository = userRepository;
this.s3UploadService = s3UploadService;
this.communityImageRepository = communityImageRepository;
}
/*커뮤니티 게시글 생성 로직*/
@Transactional
public CommunityResponseDto createCommunityPost(CommunityRequestDto requestDto,
MultipartFile[] multipartFileList) throws IOException {
/*유저 정보 검증*/
User user = getAuthenticatedUser();
/*Builder 사용 entity 객체 생성*/
Community community = Community.builder()
.title(requestDto.getTitle())
.content(requestDto.getContent())
.user(user)
.category(requestDto.getCategory())
.build();
/*DB에 Community Post 정보 저장*/
communityRepository.save(community);
/* 이미지 파일이 없을 경우*/
if (multipartFileList == null) {
/*Community Post 정보 반환*/
return new CommunityResponseDto(community);
}
/* 이미지 파일 정보 저장 리스트*/
List<String> createImageUrlList = new ArrayList<>();
List<String> createImageNameList = new ArrayList<>();
/* image file S3 save*/
saveImgToS3(multipartFileList, community, createImageUrlList, createImageNameList);
/*Community Post 정보 반환*/
return new CommunityResponseDto(community, createImageUrlList, createImageNameList);
}
Application 엔티티 클래스
package domain community Entity : Community 클래스 코드 일부
@EntityListeners(AuditingEntityListener.class)
public class Community {
...
@OneToMany(mappedBy = "community", cascade = CascadeType.ALL)
@Schema(name = "community post image id", description = "커뮤니티 게시글 이미지 ID 리스트")
private List<CommunityImage> communityImages;
...
}
Application Dto 클래스
package domain community Dto : CommunityResponseDto 클래스 코드 일부
@Getter
public class CommunityResponseDto {
...
private List<String> imageNameList;
private List<String> imageUrlList;
...
}
'항해 99 > Spring' 카테고리의 다른 글
MapStruct (0) | 2024.03.22 |
---|---|
대댓글 기능 구현 (1) | 2024.03.21 |
CORS 에러 해결 방법, CORS 보안 취약점 예방 가이드 (0) | 2024.03.19 |
CORS (1) | 2024.03.18 |
AWS로 HTTPS 연결 (0) | 2024.03.16 |