[S3] s3에 이미지 업로드하기
S3에 이미지 업로드하기
1. gradle
에 dependency 추가
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
2. application.yml
작성
# ==== S3 파일 업로드 용량 설정 ==== #
spring:
servlet:
multipart:
max-file-size: 20MB
max-request-size: 20MB
# ==== S3 접근 관련 내용 설정 ==== #
cloud:
aws:
credentials:
access-key: IAM 사용자 엑세스 키
secret-key: IAM 사용자 비밀 엑세스 키
s3:
bucket: 버킷 이름
region:
static: ap-northeast-2
stack:
auto: false
이때 IAM 사용자 엑세스 키와 비밀 엑세스키는 IAM 사용자를 생성할 때 받는 csv 파일에서 복붙하면 된다.
또한, 기본적으로 multipart로 보낼 수 있는 max-file-size
와 max-request-size
의 값은 1MB이며, 나는 20MB로 변경했다.
3. AmazonS3Client 객체를 bean으로 등록할 configuration 파일 생성
config/AmazonS3Config.java
@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();
}
}
해당 클래스에서는 @Value
애노테이션을 통해 application.yml
에서 값을 읽어온다.
4. S3Uploader
클래스 생성
service/util/S3Uploader.java
@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, multipartFile.getOriginalFilename()); //📌 파일의 originalName을 바로 넘기도록 설정
}
// S3로 파일 업로드하기
private String upload(File uploadFile, String dirName, String originalName) { //📌입력 파라미터에 originalName 추가
String fileName = dirName + "/" + UUID.randomUUID()
+ originalName; // S3에 저장된 파일 이름 📌random 값 + 기존의 파일명 으로 설정. 기존의 파일명은 upload 메서드 당시 multipartFile 에서 바로 getOriginalFileName으로 가져와서 입력 파라미터로 받기
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") + "/" + UUID.randomUUID()); //📌 local에 저장할때도 randomUUID를 쓰도록 설정
if (convertFile.createNewFile()) { // 바로 위에서 지정한 경로에 File이 생성됨 (경로가 잘못되었다면 생성 불가능)
try (FileOutputStream fos = new FileOutputStream(
convertFile)) { // FileOutputStream 데이터를 파일에 바이트 스트림으로 저장하기 위함
fos.write(file.getBytes());
}
return Optional.of(convertFile);
}
return Optional.empty();
}
}
DB에 S3 이미지 경로 저장하기
5. Image Entity 만들기
domain/post/Image.java
@Entity
@Getter
@Setter
@NoArgsConstructor
public class Image {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "image_id")
private Long id;
String imgurl;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id")
private Post post;
}
6. Post Entity 수정
domain/post/Post.java
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL)
private List<Image> img = new ArrayList<>();
Image Entity는 post에 종속되는 entity이기 때문에 Post Entity 클래스에서 CASCADE를 사용하여 OneToMany 매핑을 한다.
7. Image Repository
repository/ImageRepository.java
@Repository
public interface ImageRepository extends JpaRepository<Image,Long> {
}
Repository는 단순한 기능만 활용할 수 있도록 구현했다.
8. Image Service 구현
service/ImageService.java
@Service
public class ImageService extends S3Uploader {
private final ImageRepository imageRepository;
public ImageService(AmazonS3Client amazonS3Client, ImageRepository imageRepository) {
super(amazonS3Client);
this.imageRepository = imageRepository;
}
public String saveImage(MultipartFile multipartFile, String dirName, Post post) throws IOException {
String uri = super.upload(multipartFile, dirName);
Image img = new Image();
img.setImgurl(uri);
img.setPost(post);
imageRepository.save(img);
return uri;
}
}
9 PostServiceImpl
클래스의 save 메서드 수정
service/PostServiceImpl.java
@Service
@RequiredArgsConstructor
@Slf4j
public class PostServiceImpl implements PostService {
private final PostRepository postRepository;
private final MemberRepository memberRepository;
@Override
@Transactional
public Post save(PostSaveReqDto requestDto) {
Optional<Member> oMember = memberRepository.findByUsername(SecurityUtil.getLoginUsername());
List<Tag> tags = new ArrayList<>(requestDto.getTag());
PostTag postTag = null;
List<PostTag> postTags = new ArrayList<>();
int tagListSize = 3;
if (oMember.isPresent()) {
if (tags.size() > tagListSize) {
throw new CustomException(TAG_LIST_SIZE_ERROR);
}
Member member = oMember.get();
for (Tag tag : tags) {
// 포스트태그 생성
postTag = PostTag.createPostTag(tag);
postTags.add(postTag);
}
// 게시글 생성
Post post = Post.createPost(member, requestDto.getTitle(), requestDto.getContent(), postTags);
postRepository.save(post);
return post;
} else {
throw new CustomException(MEMBER_NOT_FOUND);
}
}
}
기존의 리턴 값이 void였던 save
메서드의 리턴 값을 Post
로 수정했다.
마찬가지로 PostService
클래스의 리턴 값도 수정하였다.
10. 컨트롤러 작성
web/BoardController.java
@RequiredArgsConstructor
@RestController
@RequestMapping
("/board")
@Slf4j
public class BoardController {
private final PostService postService;
private final ImageService imageService;
// 이미지 포함 게시글 등록
@PostMapping(value = "/save/image", consumes = {MediaType.APPLICATION_JSON_VALUE,
MediaType.MULTIPART_FORM_DATA_VALUE})
public Response savePost(@RequestPart @Valid PostSaveReqDto requestDto, @RequestPart MultipartFile image) throws
IOException {
Post post = postService.save(requestDto);
imageService.saveImage(image, "test", post);
return new Response("OK", "게시글 등록에 성공했습니다");
}
}
파일을 업로드 할 때 api 통신을 통해 받아올 객체의 타입은 MultipartFile
이다.
saveImage
메소드의 두번째 파라미터 (위의 예시에서는 test
)의 이름에 따라 S3 bucket 내부에 이미지를 담을 해당 이름의 directory가 생성된다.
🌟 중요한 것은 Multipartfile은 DTO 안에 같이 필드로 들어가 있으면 안되고 따로 RequestPart 로 나눠서 api 통신시 받아와야 한다.
그리고 이렇게 하려면 @PostMapping(consumes={})
형태로 위와 같이 각 requestpart의 content type이 무엇인지 정의를 해 주어야 한다!
포스트맨 테스트
KEY
에는 컨트롤러의 savePost
의 파라미터 이름을 작성해주고,
VALUE
에는 게시글 내용(TEXT
)과 이미지 파일(FILE
)을 첨부한 뒤,
반드시 CONTENT TYPE
을 명시해주어야 한다!
(CONTENT TYPE
필드가 안보인다면 위 사진에 표시한 부분을 클릭하여 Content Type을 선택한다.)
나중에 안드로이드에서 보낼 때도 CONTENT TYPE
을 명확하게 해야할 것 같다..
Ref.
- https://velog.io/@eeheaven/SpringBoot-AndroidStudio-KnockKnock-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-0227-%EA%B2%8C%EC%8B%9C%EA%B8%80-%EC%9E%91%EC%84%B1-%EC%8B%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%84%A3%EA%B8%B0-Server-%EA%B0%9C%EB%B0%9C
- https://devlog-wjdrbs96.tistory.com/323
- https://ttl-blog.tistory.com/320
- https://velog.io/@jinho_pca/Spring-Boot-%ED%8C%8C%EC%9D%BC-%EC%97%85%EB%A1%9C%EB%93%9C-%EC%9A%A9%EB%9F%89%EC%A0%9C%ED%95%9C-%EC%84%A4%EC%A0%95: 파일 업로드 용량 제한 설정
💛 개인 공부 기록용 블로그입니다. 👻