개발일지/2023_한이음

[Spring boot] s3 이미지 업로드 구현(초안)

기억지기 개발자 2023. 7. 26. 23:49

🏕️기존 상황

이미 S3 버킷은 만들어져 있는 상황이었고 모든 권한을 가진 iam 유저도 1명 있는 상태였다. 

그 상황에서 더 추가적으로 필요한 것들을 추가해서 개발하였다.

 

🔺S3연동을 위해 해당 권한만 별도로 가진 유저를 생성

실제 aws 화면

  • 노란색 박스처럼 S3 권한을 가진 전용 iam 유저를 [highWay_S3_user]라는 이름으로 생성.
  • 핑크색 박스의 권한을 부여하여 유저를 생성. (S3에 대한 전체 권한을 허용하는 것이기 때문에 추후 더 보안에 신경 쓸 수 있는 권한을 도입예정)

🔺build.gradle

//aws
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'

🔺application.properties

# S3
cloud.aws.credentials.accessKey= **개별정보**
cloud.aws.credentials.secretKey=**개별정보**
cloud.aws.s3.bucket=**개별정보**
cloud.aws.region.static=ap-northeast-2
cloud.aws.stack.auto-=false

 

🔺Image

@Getter
@Setter
@ToString
@NoArgsConstructor
@Entity(name = "image_TB")
public class Image {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;  // Image 객체 pk

    @Column
    private String imageUrl;

    public Image(String imageUrl) {
        this.imageUrl = imageUrl;
    }
}

🔺ImageServiceImpl

@Service
public class ImageServiceImpl {

    // 의존성 주입
    private final JpaDiaryRepository diaryRepository;

    @Autowired
    private S3Uploader s3Uploader;

    public ImageServiceImpl(JpaDiaryRepository diaryRepository) {
        this.diaryRepository = diaryRepository;
    }

   @Transactional
   public Long keepImage(MultipartFile image, Image entity) throws IOException {
       if (!image.isEmpty()) {
           try {
               String storedFileName = s3Uploader.upload(image, "images");
               entity.setImageUrl(storedFileName);
           } catch (IOException e) {
               // 업로드 오류 처리
               e.printStackTrace();
               // 또는 로그로 기록: log.error("S3 파일 업로드에 실패했습니다. {}", e.getMessage());
               throw new IllegalStateException("S3 파일 업로드에 실패했습니다.");
           }
       }
       System.out.println("-------------"+entity.getImageUrl()+"---------------");
       Image savedImage = diaryRepository.save(entity);
       return savedImage.getId();
   }
}

🔺S3Config

@Configuration
public class S3Config {
    @Value("${cloud.aws.credentials.accessKey}")
    private String accessKey;

    @Value("${cloud.aws.credentials.secretKey}")
    private String secretKey;

    @Value("${cloud.aws.region.static}")
    private String region;

    @Bean
    public AmazonS3 amazonS3Client() {
        AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);

        return AmazonS3ClientBuilder
                .standard()
                .withCredentials(new AWSStaticCredentialsProvider(credentials))
                .withRegion(region)
                .build();
    }
}

🔺S3Controller

@Controller
public class S3Controller {
    private final ImageServiceImpl service;

    public S3Controller(ImageServiceImpl service) {
        this.service = service;
    }

    @ResponseBody
    @PostMapping(value="/image", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public Long saveDiary(HttpServletRequest request, @ModelAttribute Image diary, @RequestParam(value="image") MultipartFile image) throws IOException {
        Long ImageId = service.keepImage(image, diary);
        return ImageId;
    }
}

🔺S3Uploader

@Slf4j
@RequiredArgsConstructor    // final 멤버변수가 있으면 생성자 항목에 포함시킴
@Component
@Service
public class S3Uploader {
    private final AmazonS3Client amazonS3Client;

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

    // MultipartFile을 전달받아 File로 전환한 후 S3에 업로드
    public String upload(MultipartFile multipartFile, String dirName) throws IOException {
        // 메타데이터 설정
        File uploadFile = convert(multipartFile)
                .orElseThrow(() -> new IllegalArgumentException("MultipartFile -> File 전환 실패"));
        System.out.println("변환된 이미지 파일 이름: " + uploadFile.getName());
        return upload(uploadFile, dirName);
    }


    private String upload(File uploadFile, String dirName) {
        String fileName = dirName + "/" + UUID.randomUUID() + "." + uploadFile.getName();
        String uploadImageUrl = putS3(uploadFile, fileName);

        removeNewFile(uploadFile);  // 로컬에 생성된 File 삭제 (MultipartFile -> File 전환 하며 로컬에 파일 생성됨)

        return uploadImageUrl;      // 업로드된 파일의 S3 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();
    }

    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);
    }

}

🔺성공화면

test222이미지를 전송하고, 버킷에 저장된 화면

💚후기

  • 내가 일일이 작성한 코드는 아니고 블로그에 검색해서 나온 코드를 내 필요에 맞게 변경한 것이다. 
    (근데 흥미로운 것은 대부분 코드가 다 비슷비슷하다는 것이다. )
  • 그렇게 복잡한 코드는 아닐지라도 성공하는데 거의 2~3일은 소모한 거 같다... 
  • 중간에 멘토님께도 질문하고, 성공하지 못하는 다양한 원인을 생각해 보고 그에 상응하는 해결책들을 검색해 보느라 오래 걸리기도 했다.
  • 오류가 중간에 좀 있었는데 해결하느라 정리는 하나도 못했다!!! 
  • 그렇게 복잡하지 않은 코드들로 인터넷에 로컬에 있는 이미지가 올라가는 게 신기하다! (더 공부해야지...)

🧡앞으로 할 일들

  1. 자세한 코드 분석과 작동원리 공부하기
  2. 일단 성공시키느라 보안에 대해선 크게 신경 쓰지 못했기 때문에 권한이나 접근에 대해 더 고민하여 S3, iam을 변경하기
  3. 업로드 이외에 읽기/삭제 기능 구현하기
  4. 게시판 코드와 도킹(?)하기 ㅎㅎ
  5. 다중 업로드에 대해서 추가 기능 개발하기