Blog
와 Member
를 생성할 때 이미지 파일을 업로드하지 않으면 AWS S3
에 저장된 기본 썸네일 이미지를 사용한다.Blog
와 Member
를 생성하거나, 수정할 때 이미지 파일을 업로드하면 기존에 DB에 저장된 이미지 URL을 변경하고 AWS S3
에 저장한다....
cloud:
aws:
s3:
bucket: ${S3_BUCKET}
endpoint: ${S3_ENDPOINT}
region:
static: ${S3_REGION}
...
애플리케이션에서 해당 S3 버킷을 사용할 수 있도록 application.yml
에 매핑해주었다.
@Entity
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ImageFile {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long imageFileId;
@Column(nullable = false)
private String imageFileName;
@Column(nullable = false)
private String imageFileUrl;
@OneToOne
@JoinColumn(name = "member_id")
private Member member;
@OneToOne
@JoinColumn(name = "blog_id")
private Blog blog;
public void setMember(Member member) {
this.member = member;
}
public void setBlog(Blog blog) {
this.blog = blog;
}
}
이미지 파일은 S3 버킷에 저장해놓지만 각 Blog
, Member
에 해당하는 이미지가 무엇인지 구별할 수 있도록 URL 이 담긴 ImageFile
엔티티를 사용해 DB에서 관리할 수 있도록 하였다.
Blog
-ImageFile
: 일대일 관계로 매핑
Member
-ImageFile
: 일대일 관계로 매핑
@RestController
@RequestMapping("/s3")
@RequiredArgsConstructor
public class ImageFileController {
private final ImageFileService imageFileService;
private final MemberService memberService;
private final ImageFileMapper mapper;
// 멤버 프로필 이미지 업로드
@PostMapping("/member")
public ResponseEntity uploadMemberImg(@RequestParam("image")MultipartFile multipartFile,
@AuthenticationPrincipal Member loginMember) throws IOException {
Member findMember = memberService.findMember(loginMember.getMemberId());
ImageFile imageFile = imageFileService.uploadMemImg(findMember, multipartFile);
ImageFileResponseDto response = mapper.imageFileToImageFileResponseDto(imageFile);
return new ResponseEntity<>(response, HttpStatus.OK);
}
// blog 타이틀 이미지 업로드
@PostMapping("/blog")
public ResponseEntity uploadBlogTitleImg (@RequestParam("image")MultipartFile multipartFile,
@AuthenticationPrincipal Member loginMember) throws IOException {
Member findMember = memberService.findMember(loginMember.getMemberId());
ImageFile imageFile = imageFileService.uploadBlogTitleImg(multipartFile, findMember);
ImageFileResponseDto response = mapper.imageFileToImageFileResponseDto(imageFile);
return new ResponseEntity<>(response, HttpStatus.OK);
}
}
@Service
@Slf4j
@RequiredArgsConstructor
public class ImageFileService {
private final ImageFileRepository imageFileRepository;
@Value("${S3_BUCKET}")
private String s3Bucket;
@Value("${S3_ENDPOINT}")
private String s3EndPoint;
private final AmazonS3Client amazonS3Client;
public ImageFile uploadMemImg(Member member, MultipartFile multipartFile) throws IOException {
String fileType = multipartFile.getContentType();
String imageFileName = UUID.randomUUID() + "-" + multipartFile.getOriginalFilename();
// 업로드할 파일의 사이즈
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentLength(multipartFile.getInputStream().available());
// s3에 업로드
try {
amazonS3Client.putObject(s3Bucket + "/member", imageFileName, multipartFile.getInputStream(), objectMetadata);
} catch (Exception e) {
log.error("Error uploading to AWS S3, Exception: {}",e.getMessage());
}
String imgUrl = s3EndPoint + "/member/" + imageFileName;
ImageFile imageFile = ImageFile.builder()
.imageFileName(imageFileName)
.imageFileUrl(imgUrl)
.member(member)
.build();
member.setProfileImageUrl(imgUrl);
member.setImageFile(imageFile);
return imageFileRepository.save(imageFile);
}
public ImageFile uploadBlogTitleImg(MultipartFile multipartFile, Member member) throws IOException {
String fileType = multipartFile.getContentType();
String imageFileName = UUID.randomUUID() + "-" + multipartFile.getOriginalFilename();
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentLength(multipartFile.getInputStream().available());
try {
amazonS3Client.putObject(s3Bucket + "/blog", imageFileName, multipartFile.getInputStream(), objectMetadata);
} catch (Exception e) {
log.error("Error uploading to AWS S3, Exception: {}",e.getMessage());
}
String imgUrl = s3EndPoint + "/blog/" + imageFileName;
ImageFile imageFile = ImageFile.builder()
.imageFileName(imageFileName)
.imageFileUrl(imgUrl)
.member(member)
.build();
return imageFileRepository.save(imageFile);
}
// 기본 멤버 프로필 이미지 가져오기
public String getDefaultMemImgUrl() {
String imageFileName = "default-member.png";
String imageFileUrl = s3EndPoint + "/member/" + imageFileName;
return imageFileUrl;
}
// 기본 썸네일 이미지 가져오기
public String getDefaultTitleImgUrl() {
String imageFileName = "default-title.png";
String imageFileUrl = s3EndPoint + "/blog/" + imageFileName;
return imageFileUrl;
}
}
AmazonS3Client는 AWS S3와 상호 작용하기 위한 메서드와 기능을 제공하는 클래스로, 객체 업로드, 다운로드, 삭제 등 다양한 작업을 할 수 있다.
이미지 파일을 저장하고 관리하고 S3 버킷에 이미지를 업로드하기 위해 AmazonS3Client를 주입해주었다.
public String getDefaultTitleImgUrl() {
String imageFileName = "default-title.png";
String imageFileUrl = s3EndPoint + "/blog/" + imageFileName;
return imageFileUrl;
}
미리 AWS S3 버킷에 저장된 파일명을 사용하여, imageFileUrl
을 생성하고 반환한다.
getDefaultTitleImgUrl()
는 BlogService
에서 Blog를 생성하는 로직에 사용된다.
public void createBlog(Blog blog) {
if(blog.getTitleImageUrl().isEmpty()) {
blog.setTitleImageUrl(imageFileService.getDefaultTitleImgUrl());
}
blogRepository.save(blog);
}
위 로직에서 유저가 Blog Title 이미지를 업로드 하지 않으면, imageFileService.getDefaultTitleImgUrl()
메서드를 호출해 기본 썸네일 이미지 URL를 Blog 인스턴스에 입력한다.
실제 서비스에서 블로그를 작성할 때 썸네일 이미지를 등록하지 않은 경우
위 처럼 기본 이미지가 노출된다.
public ImageFile uploadBlogTitleImg(MultipartFile multipartFile, Member member) throws IOException {
String fileType = multipartFile.getContentType();
String imageFileName = UUID.randomUUID() + "-" + multipartFile.getOriginalFilename();
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentLength(multipartFile.getInputStream().available());
try {
amazonS3Client.putObject(s3Bucket + "/blog", imageFileName, multipartFile.getInputStream(), objectMetadata);
} catch (Exception e) {
log.error("Error uploading to AWS S3, Exception: {}",e.getMessage());
}
String imgUrl = s3EndPoint + "/blog/" + imageFileName;
ImageFile imageFile = ImageFile.builder()
.imageFileName(imageFileName)
.imageFileUrl(imgUrl)
.member(member)
.build();
return imageFileRepository.save(imageFile);
}
uploadBlogTitleImg()는 유저가 등록한 이미지 파일(multipartFile)과 해당 유저의 정보를 파라미터로 갖는다.
String imageFileName = UUID.randomUUID() + "-" + multipartFile.getOriginalFilename();
이미지 파일의 이름이 겹칠 수 있다는 것을 인지하고 있었기 때문에 기존의 이름 앞에 UUID.randomUUID()
값을 부여하여 고유한 이름을 가질 수 있도록 하였다.
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentLength(multipartFile.getInputStream().available());
ObjectMetadata
는 S3에 객체를 업로드할 때 추가적인 메타데이터를 설정하는데 사용되는 클래스로 여기서는 multipartFile.getInputStream().available()
를 사용하여 업로드할 파일의 크기를 설정한다.
try {
amazonS3Client.putObject(s3Bucket + "/blog", imageFileName, multipartFile.getInputStream(), objectMetadata);
} catch (Exception e) {
log.error("Error uploading to AWS S3, Exception: {}",e.getMessage());
}
amazonS3Client
를 사용하여 S3에 이미지를 업로드한다.
bucketName : 미리 설정한 s3Bucket + 추가 경로(/blog, /member)
key : 고유성을 부여한 이미지 파일 이름
input : multipartFile.getInputStream()
metadata : 위에 생성한 objectMetadata
업로드 시, Exception이 발생할 상황을 고려하여 try-catch문을 사용하여 error log를 받을 수 있도록 하였다.
String imgUrl = s3EndPoint + "/blog/" + imageFileName;
ImageFile imageFile = ImageFile.builder()
.imageFileName(imageFileName)
.imageFileUrl(imgUrl)
.member(member)
.build();
return imageFileRepository.save(imageFile);
실제 서비스에서 썸네일을 등록하고 글을 작성해보았다.
정상적으로 썸네일이 등록되었다.
정상적으로 저장된 것을 확인할 수 있다.