[TIL] AWS S3로 썸네일 이미지 저장

·2023년 3월 11일
0

NerdNest

목록 보기
4/6
post-thumbnail

요구사항

  1. 유저가 BlogMember를 생성할 때 이미지 파일을 업로드하지 않으면 AWS S3에 저장된 기본 썸네일 이미지를 사용한다.
  2. 유저가 BlogMember를 생성하거나, 수정할 때 이미지 파일을 업로드하면 기존에 DB에 저장된 이미지 URL을 변경하고 AWS S3에 저장한다.

AWS S3 버킷에 기존 이미지 저장

AWS S3 기본 설정

...
cloud:
  aws:
    s3:
      bucket: ${S3_BUCKET}
      endpoint: ${S3_ENDPOINT}
    region:
      static: ${S3_REGION}
...

애플리케이션에서 해당 S3 버킷을 사용할 수 있도록 application.yml에 매핑해주었다.

ImageFile Entity

@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 : 일대일 관계로 매핑

Controller

@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

@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

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를 생성하는 로직에 사용된다.

BlogService - createBlog()

public void createBlog(Blog blog) {

        if(blog.getTitleImageUrl().isEmpty()) {
            blog.setTitleImageUrl(imageFileService.getDefaultTitleImgUrl());
        }
        blogRepository.save(blog);
    }

위 로직에서 유저가 Blog Title 이미지를 업로드 하지 않으면, imageFileService.getDefaultTitleImgUrl() 메서드를 호출해 기본 썸네일 이미지 URL를 Blog 인스턴스에 입력한다.


실제 서비스에서 블로그를 작성할 때 썸네일 이미지를 등록하지 않은 경우

위 처럼 기본 이미지가 노출된다.

uploadBlogTitleImg()

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 objectMetadata = new ObjectMetadata();
objectMetadata.setContentLength(multipartFile.getInputStream().available());

ObjectMetadata는 S3에 객체를 업로드할 때 추가적인 메타데이터를 설정하는데 사용되는 클래스로 여기서는 multipartFile.getInputStream().available()를 사용하여 업로드할 파일의 크기를 설정한다.

S3에 이미지 업로드

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를 받을 수 있도록 하였다.

DB에 ImageFile 인스턴스 저장

String imgUrl = s3EndPoint + "/blog/" + imageFileName;

        ImageFile imageFile = ImageFile.builder()
                .imageFileName(imageFileName)
                .imageFileUrl(imgUrl)
                .member(member)
                .build();

        return imageFileRepository.save(imageFile);

썸네일 이미지 테스트

실제 서비스에서 썸네일을 등록하고 글을 작성해보았다.

정상적으로 썸네일이 등록되었다.

AWS S3

MySQL DB

정상적으로 저장된 것을 확인할 수 있다.

profile
🧑‍💻백엔드 개발자, 조금씩 꾸준하게

0개의 댓글