코딩과 결혼합니다

230901 - Spring 의 BufferedImage로 S3 이미지 리사이징하기 본문

코딩과 매일매일♥/Seoulvival

230901 - Spring 의 BufferedImage로 S3 이미지 리사이징하기

코딩러버 2023. 9. 1. 11:59
728x90

이미지 리사이징에 BufferedImage를 사용한 이유


기존에는 원본 이미지를 S3에 바로 업로드 하고, 프런트에서 해당 원본 이미지를 사용하였다. 그러나 디바이스에 보여지는 사진 대비 너무 큰 이미지를 호출하는건 비효율적이며, S3에도 굳이 큰 파일들을 모두 넣는것 보다는 리사이징 한 사진을 넣는게 공간을 덜 낭비할 것이라는 판단으로 이미지 리사이징을 하게 되었다.

이미지 리사이징을 하기 위해 BufferedImage
사용했다.
Java의 기본 라이브러리로 제공되므로 별도의 외부 라이브러리나 프레임워크 설치 없이 사용할 수 있고, resizeImage 메서드와 같이 크기를 변경하는 작업을 간단하게 구현할 수 있기 때문이다.

@Service
@RequiredArgsConstructor
@Transactional //(readOnly = true)
public class PostService {

    private final AmazonS3Service amazonS3Service;
    private final PostRepository postRepository;
    private final PostImgRepository postImgRepository;
    private final PostLikeRepository postLikeRepository;
    private final PostScrapRepository postScrapRepository;
    private final AlarmRepository alarmRepository;
    private final ReportRepository reportRepository;
    private final NotificationService notificationService;
    @PersistenceContext
    private EntityManager entityManager;

    @Value("${cloud.aws.s3.bucket}")
    private String bucketName;

    //게시물 생성 : 게시물을 업로드하고 이미지를 저장
    public MessageResponseDto upload(PostRequestDto requestDto, User user, List<MultipartFile> photos) throws IOException {
        requestDto.validateCategory();

        //본문에서 해시태그 추출
        List<String> hashtags = extractHashtagsFromContent(requestDto);

		//S3에 사진 업로드
        List<PostImg> postImgList = amazonS3Service.uploadPhotosToS3AndCreatePostImages(photos);

        Post post = new Post(requestDto, user);
        postRepository.save(post);

        associatePostImagesWithPost(post, postImgList);

        // 게시물에 연결된 해시태그를 구독하는 사용자 찾기
        for (String hashtag : hashtags) {
            List<User> subscribers = findSubscribersForHashtag(hashtag);

            for (User subscriber : subscribers) {
                // 로그인한 사용자와 게시물을 올린 사용자가 다를 때만 알림 생성
                if (!subscriber.getId().equals(user.getId())) {
                    AlarmEventType eventType = AlarmEventType.NEW_POST_WITH_HASHTAG; // 댓글에 대한 알림 타입 설정
                    Boolean isRead = false; // 초기값으로 미읽음 상태 설정
                    String notificationMessage = post.getContent(); // 알림 메시지 설정
                    LocalDateTime registeredAt = LocalDateTime.now(); // 알림 생성 시간 설정
                    String userImg = user.getProfileImageUrl();
                    String hashtagName = hashtag;

                    Alarm alarm = new Alarm(post, subscriber, eventType, isRead, notificationMessage, registeredAt, userImg, hashtagName);
                    alarmRepository.save(alarm);
                    notificationService.notifyAddEvent(subscriber, subscriber.isHashtagAlarm());
                }
            }
        }

        return new MessageResponseDto("파일 저장 성공");
    }

원래는 postService 단에서 S3업로드를 하는 메서드 까지 모두 다뤘는데 가독성이 너무 떨어져서 S3업로드와 관련된 메서드는 AmazonS3Service로 분리 하였다.

 

Post를 업로드 할 때 사진 파일을 S3에 저장하게 되는데, 저장하기 전에 이미지를 너비 500으로 리사이징 하고 가로 세로 너비가 줄어든 만큼 높이도 비율에 맞춰 저장할 것이다.

 


AmazonS3Service - 리사이징


@Service
@RequiredArgsConstructor
public class AmazonS3Service {

    private final AmazonS3Client amazonS3Client;

    @Value("${cloud.aws.s3.bucket}")
    private String bucketName;

    //이미지를 S3에 업로드하고, PostImg 객체를 생성하여 리스트에 추가
    public List<PostImg> uploadPhotosToS3AndCreatePostImages(List<MultipartFile> photos) throws IOException {
        List<PostImg> postImgList = new ArrayList<>();

        if (photos != null && !photos.isEmpty()) {
            for (MultipartFile photo : photos) {
                String fileName = uploadPhotoToS3AndGetFileName(photo);
                PostImg postImg = new PostImg(fileNameToURL(fileName), null);
                postImgList.add(postImg);
            }
        }
        return postImgList;
    }

하나의 메서드에 코드가 너무 많으면 가독성이 떨어지니 알아보기 쉽게 메서드를 분리해 주었다.

    //S3에 이미지를 업로드하고, 업로드된 파일의 이름을 반환
    private String uploadPhotoToS3AndGetFileName(MultipartFile photo) throws IOException {
        long size = photo.getSize();

        ObjectMetadata objectMetadata = new ObjectMetadata();
        objectMetadata.setContentType(photo.getContentType());
        objectMetadata.setContentLength(size);
        objectMetadata.setContentDisposition("inline");

        String prefix = UUID.randomUUID().toString();
        String fileName = prefix + "_" + photo.getOriginalFilename();
        String bucketFilePath = "photos/" + fileName;

        InputStream inputStream = photo.getInputStream();

        BufferedImage originalImage = ImageIO.read(inputStream);

        int targetWidth = 500; // 가로 길이를 500px로 설정

        // 이미지가 500px보다 크면 리사이즈
        if (originalImage.getWidth() > targetWidth) {
            BufferedImage resizedImage = resizeImage(originalImage, targetWidth);

            // 리사이즈된 이미지를 S3에 업로드
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            ImageIO.write(resizedImage, "jpg", outputStream);
            byte[] resizedImageBytes = outputStream.toByteArray();

            objectMetadata.setContentLength(resizedImageBytes.length);

            amazonS3Client.putObject(
                    new PutObjectRequest(bucketName, bucketFilePath, new ByteArrayInputStream(resizedImageBytes), objectMetadata)
                            .withCannedAcl(CannedAccessControlList.PublicRead)
            );
        } else {
            // 이미지가 500px 이하이면 그대로 S3에 업로드
            amazonS3Client.putObject(
                    new PutObjectRequest(bucketName, bucketFilePath, inputStream, objectMetadata)
                            .withCannedAcl(CannedAccessControlList.PublicRead)
            );
        }

        return fileName;
    }

이 코드는 S3 (Amazon Simple Storage Service)에 이미지를 업로드하고, 업로드된 파일의 이름을 반환하는 메소드이다.

  1. uploadPhotoToS3AndGetFileName 메소드는 MultipartFile 객체인 photo를 매개변수로 받는다. 이 객체는 클라이언트에서 전송된 이미지 파일이다.
  2. photo.getSize()를 통해 업로드할 이미지의 크기를 확인
  3. ObjectMetadata 객체를 생성하여 업로드할 파일의 메타데이터(크기, 유형 등)를 설정
  4. UUID 값을 생성하여 파일 이름에 사용할 prefix로 지정
  5. prefix + "_" + photo.getOriginalFilename()을 통해 원본 파일 이름과 prefix를 조합하여 새로운 파일 이름 생성
  6. S3 버킷 내에서 저장될 경로(bucketFilePath)와 실제 S3 버킷에 저장될 경로(photos/파일이름)를 설정
  7. photo.getInputStream()을 호출하여 MultipartFile에서 InputStream을 얻는다.
  8. InputStream으로부터 BufferedImage 객체인 originalImage를 읽어온다.
  9. 이미지의 가로 길이가 500px보다 큰 경우, resizeImage() 메소드를 사용하여 이미지 크기를 조정하고 500px보다 작으면 그대로 저장
    • resizeImage() 메소드는 원본 이미지와 타겟 가로 길이(targetWidth) 값을 받아 리사이즈된 BufferedImage 객체(resizedImage)를 반환
    • 리사이즈된 이미지(resizedImage)는 ByteArrayOutputStream에 쓰여진 뒤, 바이트 배열(resizedImageBytes) 형태로 변환된다.
    • objectMetadata.setContentLength()을 호출하여 리사이즈된 이미지의 크기도 설정
    • AmazonS3Client의 putObject() 메소드를 사용하여 리사이즈된 이미지와 objectMetadata 정보가 포함된 PutObjectRequest 객체를 전달하고, CannedAccessControlList.PublicRead ACL(Access Control List) 옵션으로 S3 버킷에 업로드
    public BufferedImage resizeImage(BufferedImage originalImage, int targetWidth) {
        double aspectRatio = (double) originalImage.getHeight() / originalImage.getWidth();
        int targetHeight= (int) (targetWidth * aspectRatio);

        BufferedImage scaledImge= new BufferedImage(targetWidth,targetHeight ,BufferedImage.TYPE_INT_RGB);

        Graphics2D g2d= scaledImge.createGraphics();

        g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
                RenderingHints.VALUE_INTERPOLATION_BILINEAR);
        g2d.drawImage(originalImage ,0 ,0 ,targetWidth,targetHeight,null );

        g2d.dispose();

        return scaledImge;
    }

    private String fileNameToURL(String fileName) {
        return "https://living-in-seoul.s3.ap-northeast-2.amazonaws.com/photos/" + fileName;
    }

 

1.resizeImage(BufferedImage originalImage, int targetWidth) 메소드

이 메소드는 원본 이미지의 크기를 변경하여 새로운 이미지를 반환하는 역할을 한다.

  • 먼저 원본 이미지의 가로 세로 비율(aspect ratio)을 계산
  • 그 다음, 이 비율을 사용하여 변경할 이미지의 높이(targetHeight)를 계산
  • 새로운 BufferedImage 객체(scaledImage)를 생성. 이 객체는 변경된 크기와 RGB 색상 모델을 가진다.
  • Graphics2D 객체(g2d)를 생성하고, Bilinear Interpolation(양선형 보간법) 방식으로 렌더링 힌트 설정을 한다. 이렇게 하면 이미지 확대/축소 시 부드러운 결과물을 얻을 수 있다.
  • drawImage 함수를 사용해 원본 이미지를 새롭게 계산된 너비와 높이에 맞게 그린다.
  •  g2d.dispose() 호출하여 그래픽 리소스들을 정리해주고, 리사이즈된 이미지(scaledImge)를 반환한다.

 

2.fileNameToURL(String fileName) 메소드

이 메소드는 파일 이름을 받아 해당 파일의 URL 주소 문자열로 변환

  • 주어진 파일 이름에 대한 S3 버킷 내 위치 URL 문자열("https://living-in-seoul.s3.ap-northeast-2.amazonaws.com/photos/" + fileName) 을 만들어 반환

📌 결과

 

리사이징 전

리사이징 후