AWS S3는 AWS에서 제공하는 Storage 서비스로서 아래와 같은 특징들이 있다
S3에 이미지 업로드를 하기위해서는 사전 작업이 필요하다
(AWS 계정이 있다는 전제하에 필요한 작업들이다)
주의할 점은 발급받은 Access key와 Secret key는 절대 유출이 되서는 안된다(과금 우려)
또한, 이미지 업로드가 되지 않는다면, 액세스 권한을 풀어줬는지 확인해야한다
특히 ACL이 활성화 되지 않았을 경우 postman에서 다음과 같은 error를 만날 수 있으니 꼭 권한을 잘 풀어줘야한다
File upload failed : AccessControlListNotSupported: The bucket does not allow ACLs
다른 방법이 있는지는 모르겠지만, 찾아본 바에 따르면 사진 한 장 올리는 것을 List에 담아내는 방식으로 구현을 했다
build.gradle애 아래 코드를 추가해 주어야
dependencies {
implementation group: 'org.springframework.cloud', name: 'spring-cloud-starter-aws', version: '2.2.5.RELEASE'
}
@Getter
@Entity
@NoArgsConstructor
public class Img {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String imgUrl;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id", nullable = false)
@OnDelete(action = OnDeleteAction.CASCADE)
private Post post;
public Img(String imgUrl, Post post) {
this.imgUrl = imgUrl;
this.post = post;
}
}
@Getter
@Entity
@NoArgsConstructor
public class Post extends Timestamped {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long Id;
@Column(nullable = false)
private String content;
@Transient
private final List<Img> imgList = new ArrayList<>();
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn
private Member member;
@OneToMany(mappedBy = "post")
private List<Comment> comment;
public Post(String content, Member member) {
this.content = content;
this.member = member;
}
public void updatePost(PostRequestDto res) {
this.content = res.getContent();
}
}
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class PostController {
private final PostService postService;
private final S3Service s3Service;
// 게시글 전체 조회
@GetMapping("/posts")
public Map<String, List<PostGetResponseDto>> getAllPost(){ return postService.getAllPost();}
// 메인 페이지 무한 스크롤
@GetMapping("/postsScroll")
public Map<String, List<PostGetResponseDto>> getPostSlice(
@RequestParam(required=false) Integer page,
@RequestParam(required=false) Integer size,
@RequestParam(required=false) String sortBy ,
@RequestParam(required=false) Boolean isAsc
) {
if (isNotNullParam(page, size, sortBy, isAsc)) {
page -= 1;
return postService.getAllPostSlice(page, size, sortBy, isAsc);
} else {
throw new PrivateException(Code.PAGING_ERROR);
}
}
private boolean isNotNullParam(Integer page, Integer size, String sortBy, Boolean isAsc) {
return (page != null) && (size != null) && (sortBy != null) && (isAsc != null);
}
// 게시글 상세 조회
@GetMapping("/post/{postId}")
public ExceptionResponseDto getPost(@PathVariable Long postId) {
PostGetResponseDto postGetResponseDto = postService.getPostOne(postId);
return new ExceptionResponseDto(Code.OK, postGetResponseDto);
}
// 게시글 작성
@PostMapping("/post")
public ExceptionResponseDto uploadPost(@RequestPart("content") PostRequestDto postRequestDto,
@RequestPart("imgUrl") List<MultipartFile> multipartFiles) {
if (multipartFiles == null) {
throw new PrivateException(Code.WRONG_INPUT_CONTENT);
}
List<String> imgPaths = s3Service.upload(multipartFiles);
System.out.println("IMG 경로들 : " + imgPaths);
postService.uploadPost(postRequestDto, imgPaths);
return new ExceptionResponseDto(Code.OK);
}
// 게시글 수정
@PutMapping("/post/{postId}")
public ExceptionResponseDto updatePost(@PathVariable Long postId,@RequestPart("content") PostRequestDto postRequestDto) {
PostUpdateResponseDto postUpdateResponseDto = postService.updatePost(postId, postRequestDto);
return new ExceptionResponseDto(Code.OK, postUpdateResponseDto);
}
// 게시글 삭제
@DeleteMapping("/post/{postId}")
public ExceptionResponseDto deletePost(@PathVariable Long postId) {
postService.deletePost(postId);
return new ExceptionResponseDto(Code.OK);
}
}
@Service
@RequiredArgsConstructor
public class PostService {
private final MemberRepository memberRepository;
private final PostRepository postRepository;
private final ImgRepository imgRepository;
private final S3Service s3Service;
// 게시글 작성
@Transactional
public void uploadPost(PostRequestDto res, List<String> imgPaths) {
postBlankCheck(imgPaths);
System.out.println("로그인한 username : " + SecurityUtil.getCurrentUsername());
String username = SecurityUtil.getCurrentUsername();
Member member = memberRepository.findMemberByUsername(username).orElseThrow(
() -> new PrivateException(Code.NOT_FOUND_MEMBER)
);
String content = res.getContent();
Post post = new Post(content, member);
postRepository.save(post);
List<String> imgList = new ArrayList<>();
for (String imgUrl : imgPaths) {
Img img = new Img(imgUrl, post);
imgRepository.save(img);
imgList.add(img.getImgUrl());
}
}
private void postBlankCheck(List<String> imgPaths) {
if(imgPaths == null || imgPaths.isEmpty()){ //.isEmpty()도 되는지 확인해보기
throw new PrivateException(Code.WRONG_INPUT_IMAGE);
}
}
}
cloud.aws.credentials.accessKey=****
cloud.aws.credentials.secretKey=****
cloud.aws.s3.bucket=bucket name
cloud.aws.stack.auto=false
cloud.aws.region.static=ap-northeast-2
@Service
@RequiredArgsConstructor
public class S3Service {
private final AmazonS3 s3Client;
@Value("${cloud.aws.credentials.accessKey}")
private String accessKey;
@Value("${cloud.aws.credentials.secretKey}")
private String secretKey;
@Value("${cloud.aws.s3.bucket}")
private String bucket;
@Value("${cloud.aws.region.static}")
private String region;
@PostConstruct
public AmazonS3Client amazonS3Client() {
BasicAWSCredentials awsCreds = new BasicAWSCredentials(accessKey, secretKey);
return (AmazonS3Client) AmazonS3ClientBuilder.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(awsCreds))
.build();
}
public List<String> upload(List<MultipartFile> multipartFile) {
List<String> imgUrlList = new ArrayList<>();
// forEach 구문을 통해 multipartFile로 넘어온 파일들 하나씩 fileNameList에 추가
for (MultipartFile file : multipartFile) {
String fileName = createFileName(file.getOriginalFilename());
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentLength(file.getSize());
objectMetadata.setContentType(file.getContentType());
try(InputStream inputStream = file.getInputStream()) {
s3Client.putObject(new PutObjectRequest(bucket+"/post/image", fileName, inputStream, objectMetadata)
.withCannedAcl(CannedAccessControlList.PublicRead));
imgUrlList.add(s3Client.getUrl(bucket+"/post/image", fileName).toString());
} catch(IOException e) {
throw new PrivateException(Code.IMAGE_UPLOAD_ERROR);
}
}
return imgUrlList;
}
// 이미지파일명 중복 방지
private String createFileName(String fileName) {
return UUID.randomUUID().toString().concat(getFileExtension(fileName));
}
// 파일 유효성 검사
private String getFileExtension(String fileName) {
if (fileName.length() == 0) {
throw new PrivateException(Code.WRONG_INPUT_IMAGE);
}
ArrayList<String> fileValidate = new ArrayList<>();
fileValidate.add(".jpg");
fileValidate.add(".jpeg");
fileValidate.add(".png");
fileValidate.add(".JPG");
fileValidate.add(".JPEG");
fileValidate.add(".PNG");
String idxFileName = fileName.substring(fileName.lastIndexOf("."));
if (!fileValidate.contains(idxFileName)) {
throw new PrivateException(Code.WRONG_IMAGE_FORMAT);
}
return fileName.substring(fileName.lastIndexOf("."));
}
}
정상적으로 통신이 되서 Custom Exception으로 설정해두었던 데이터로 body에 잘 내려보내진 것을 확인할 수 있다
s3에서 파일 삭제는 어떻게 하셨나요?