개발일지/2023_한이음

[spring boot] S3 이미지 처리_업로드 구조 변경(api 분리)

기억지기 개발자 2023. 8. 26. 13:08

<< 구조 >>

  1.  게시글에 이미지를 올린다.
  2.  그럼 프런트 단에서 이벤트 감지 기능으로 api를 호출한다.
     (유효성 + 사이즈 검사를 진행 후 S3 임시 경로에 이미지들을 업로드한다.)
  3. 임시 경로가 포함된 해당 이미지의 URL을 반환한다.
  4. 사용자가 게시글까지 작성 후에 [등록하기]를 누르면 게시글은 게시글 DB에 저장, 이미지는 임시경로 🔜 정식경로로 이동시킨 후에 정식 경로로 변경된 url을 다시 반환받는다.

 

public class imageService {
    @Autowired
    private AmazonS3 amazonS3;

    private final AmazonS3Client amazonS3Client;
    private final imageRepository repository;

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

    // MultipartFile을 전달받아 File로 전환한 후 S3에 업로드
    @Transactional
    public List<String> upload(List<MultipartFile> multipartFileList) throws IOException {
        List<String> uploadedFileNames = new ArrayList<>();

        for (MultipartFile multipartFile : multipartFileList) {
            if (!multipartFile.isEmpty()) {
                if (!isValidImage(multipartFile.getInputStream())) { //유효한 이미지인지 검사
                    throw new IllegalArgumentException("Invalid image format");
                }

                long maxSizeInBytes = 5 * 1024 * 1024; // 10MB

                if (!isImageSizeValid(multipartFile, maxSizeInBytes)) {  //용량체크
                    throw new IllegalArgumentException("Image size exceeds the limit");
                }

                File uploadFile = convert(multipartFile)
                        .orElseThrow(() -> new IllegalArgumentException("MultipartFile -> File 전환 실패"));

                log.info("변환된 이미지 파일 이름: " + uploadFile.getName());

                String url = upload(uploadFile, "temporary"); //dirName = 이미지가 저장될 s3 폴더명

                uploadedFileNames.add(url);
            }
        }
        return uploadedFileNames;
    }

    private String upload(File uploadFile, String dirName) {
        String fileName = dirName + "/" + UUID.randomUUID() + "." + uploadFile.getName(); //이름 중복 방지를 위한 렌덤 코드를 추가(UUID.randomUUID())
        String uploadImageUrl = putS3(uploadFile, fileName);
        removeNewFile(uploadFile);
        return uploadImageUrl; //이미지 url 반환
    }

    private String putS3(File uploadFile, String fileName) {
        amazonS3Client.putObject(
                new PutObjectRequest(bucket, fileName, uploadFile)
                        .withCannedAcl(CannedAccessControlList.PublicRead)	// PublicRead 권한으로 업로드 됨
        );
        return amazonS3Client.getUrl(bucket, fileName).toString();
    }

    public boolean isValidImage(InputStream inputStream) {
        try {
            BufferedImage image = ImageIO.read(inputStream);
            if (image == null) {
                return false; // 이미지가 올바르지 않을 경우
            }
            return true;
        } catch (IOException e) {
            return false;
        }
    }

    public boolean isImageSizeValid(MultipartFile imageFile, long maxSizeInBytes) {
        return imageFile.getSize() <= maxSizeInBytes;
    }

    private void removeNewFile(File targetFile) {
        if(targetFile.delete()) {
            log.info("파일이 삭제되었습니다.");
        }else {
            log.info("파일이 삭제되지 못했습니다.");
        }
    }

    private Optional<File> convert(MultipartFile file) {
        File convertFile = new File(file.getOriginalFilename());
        try (FileOutputStream fos = new FileOutputStream(convertFile)) {
            fos.write(file.getBytes());
        } catch (IOException e) {
            // 변환에 실패하면 예외 처리
            convertFile.delete();
            // 예외 메시지와 스택 트레이스를 로그로 출력
            log.error("MultipartFile 변환에 실패했습니다: {}", e.getMessage(), e);
            return Optional.empty();
        }

        return Optional.of(convertFile);
    }

    public void deleteFile(String fileName) {
        DeleteObjectRequest request = new DeleteObjectRequest(bucket, fileName);
        amazonS3Client.deleteObject(request);
    }
//-------------------------------------------------------------------------------------
    public List<String> moveImagesToFinalLocation(List<String> imageUrls, Long boardId) { //정식 폴더로 옮기는 메인 메소드
        List<String> finalImageUrls = new ArrayList<>();
        for (String imageUrl : imageUrls) {
            String imageName = getImageNameFromUrl(imageUrl);
            // 임시 폴더에서 이미지를 가져와서 정식 폴더로 이동
            finalImageUrls.add(moveImageToFinalFolder(imageName));
        }
        saveImagesToDatabase(finalImageUrls, boardId);
        return finalImageUrls;
    }

    public void saveImagesToDatabase(List<String> finalImageUrls, Long boardId) { //정식 폴더로 이동할 이미지들은 db에 저장
        for (String imageUrl : finalImageUrls) {
            Image imageEntity = new Image();
            imageEntity.setImageUrl(imageUrl);
            imageEntity.setBoardId(boardId);
            repository.save(imageEntity);
        }
    }

    private String getImageNameFromUrl(String imageUrl) { // 이미지 URL에서 파일 이름 추출
        // 이미지 URL에서 파일 이름 추출
        return imageUrl.substring(imageUrl.lastIndexOf("/") + 1);
    }

    private String moveImageToFinalFolder(String imageName) { // 이미지를 임시 폴더에서 정식 폴더로 이동하는 로직 구현
        String sourceKey = "temporary/" + imageName;
        String destinationKey = "final/" + imageName;

        // Amazon S3의 copyObject 메소드를 활용하여 이미지를 임시 폴더에서 정식 폴더로 복사
        amazonS3.copyObject(bucket, sourceKey, bucket, destinationKey);

        // 복사가 완료되면 임시 폴더의 이미지 삭제
        amazonS3.deleteObject(bucket, sourceKey);

        // 정식 폴더에 저장된 이미지의 URL 생성 및 반환
        String finalImageUrl = amazonS3.getUrl(bucket, destinationKey).toString();

        return finalImageUrl;
    }
}

핵심이 되는 동작 코드들은 다 이 클래스에 있다.

이거 성공하고 감동의 눈물을 쪼끔 흘릴 뻔했다..ㅎㅎ 엄청난 기능은 아닐지 몰라도 난 며칠을 꼬박 걸려 만든 소중한 기능이다...

 


<< 성공한 상황들 >>

 

💚최대 용량을 3MB로 제한해두고 3MB가 넘는 이미지를 업로드 했을 때 성공적으로 나타나는 오류 메세지~~

console 화면

 

💚임시경로에서 정식경로로 바뀐 URL

임시경로
정식경로