본문 바로가기

AWS

S3 버킷 생성과 스프링부트로 S3에 이미지 저장하기 구현

버킷 : 저장소

객체 : 버킷에 업로드된 파일

S3 버킷 생성하기

퍼블릭 엑세스 차단 해제

 

그리고 그대로 생성하면 됨

 

버킷에 정책 ( 권한(Permission)을 정의하는 JSON 문서)추가

여기서 정책 추가

 

s3 -> gerobject

 

리소스 추가하기

 

S3에 파일 업로드 할 수 있도록 IAM에서 액세스 키 발급받기

백엔드 서버가 S3에 접근해서 파일을 업로드할 수 있어야 한다. S3에 접근할 수 있는 권한을 받기 위해 IAM이라는 곳에서 권한을 부여받아야 한다.

 

사용자 생성

 

 

정책 생성

 

엑세스 키 만들기

 

발급 완료, 저 두 키는 따로 저장해 놔야 함

 

스프링부트로 S3에 이미지 저장하기

  • users 테이블에 imageUrl필드를 추가하여 저장된 프로필 이미지의 url을 저장하여 user 조회시 함께 조회
  • 이미지 제목이 중복이 될 수 있기 때문에 원래 원본파일명 + UUID를 붙여서 저장
  • "jpg", "jpeg", "png", "gif"인 파일만 저장할 수 있도록 검증

 

이미지 저장 시도 중 문제 발생 : AccessControlListNotSupported

com.amazonaws.services.s3.model.AmazonS3Exception: The bucket does not allow ACLs (Service: Amazon S3; Status Code: 400; Error Code: AccessControlListNotSupported;

 

S3 버킷이 ACL(Access Control List)을 지원하지 않아서 발생하는 문제라고 한다

 

ACL

  • 사용자(User) 또는 그룹(Group)에 대해 특정 권한을 부여하는 방식
  • 버킷 정책(Bucket Policy) 및 IAM 정책보다 간단한 권한 관리 방식
  • 현재는 AWS는 ACL을 비활성화하는 것을 기본값(Default) 으로 변경

이미 위에서 버킷 정책을 적용했으니 ACL을 사용하는 코드를 제거해서 해결하면 된다

 

기존 코드

PutObjectRequest putObjectRequest =
    new PutObjectRequest(bucketName, s3FileName, byteArrayInputStream, metadata)
        .withCannedAcl(CannedAccessControlList.PublicRead); //ACL 사용
amazonS3.putObject(putObjectRequest);

 

수정된 코드

PutObjectRequest putObjectRequest =
    new PutObjectRequest(bucketName, s3FileName, byteArrayInputStream, metadata);
amazonS3.putObject(putObjectRequest);

 

스프링부트 코드

poroperties 파일 수정

aws.access.key=
aws.secret.access.key=
aws.s3.bucket=
aws.region.static=ap-northeast-2
aws.stack.auto-=false

 

라이브러리 추가

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

 

S3Config 생성

@Configuration
public class S3Config {
    @Value("${aws.access.key}")
    private String accessKey;
    @Value("${aws.secret.access.key}")
    private String secretKey;
    @Value("${aws.region.static}")
    private String region;

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

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

}

 

이미지 업로드 및 삭제 컨트롤러

// 사용자 프로필 이미지 업로드
    @PostMapping("/users/image")
    public ResponseEntity<UserProfileResponse> uploadImage(@AuthenticationPrincipal AuthUser authUser, @RequestPart MultipartFile image) throws IOException {
        UserProfileResponse user = userService.uploadImage(authUser.getId(), image);
        return ResponseEntity.ok(user);
    }

    // 사용자 프로필 이미지 삭제
    @DeleteMapping("/users/image")
    public void imageDelete(@AuthenticationPrincipal AuthUser authUser, @RequestParam String imageUrl) {
        userService.deleteImage(authUser.getId(), imageUrl);
    }

 

이미지 업로드 검증 메서드

// 이미지 업로드 검증
    private static void validImage(MultipartFile image) {
        // 이미지가 비어있는지 확인
        if(image.isEmpty() || Objects.isNull(image.getOriginalFilename())){
            throw new InvalidRequestException("이미지 처리중 오류가 발생했습니다.");
        }
        int lastDotIndex = image.getOriginalFilename().lastIndexOf(".");
        if (lastDotIndex == -1) {
            throw new InvalidRequestException("이미지 처리중 오류가 발생했습니다.");
        }

        // 확장자 검증
        String extention = image.getOriginalFilename().substring(lastDotIndex + 1).toLowerCase();
        List<String> allowedExtentionList = Arrays.asList("jpg", "jpeg", "png", "gif");

        if (!allowedExtentionList.contains(extention)) {
            throw new InvalidRequestException("이미지 처리중 오류가 발생했습니다.");
        }
    }

 

이미지 업로드 서비스

// 이미지 업로드
    @Transactional
    public UserProfileResponse uploadImage(long userId, MultipartFile image) throws IOException {
        User user = userRepository.findById(userId).orElseThrow(() -> new InvalidRequestException("User not found"));
        validImage(image);

        String originalFilename = image.getOriginalFilename(); //원본 파일 명
        String extention = originalFilename.substring(originalFilename.lastIndexOf(".")); //확장자 명

        String s3FileName = UUID.randomUUID().toString().substring(0, 10) + originalFilename; //변경된 파일 명

        InputStream is = image.getInputStream();
        byte[] bytes = IOUtils.toByteArray(is);

        ObjectMetadata metadata = new ObjectMetadata();
        metadata.setContentType("image/" + extention);
        metadata.setContentLength(bytes.length);
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);

        try{
            PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, s3FileName, byteArrayInputStream, metadata);
            amazonS3.putObject(putObjectRequest); // put image to S3
        }catch (Exception e){
            throw new InvalidRequestException("이미지 처리중 오류가 발생했습니다.");
        }finally {
            byteArrayInputStream.close();
            is.close();
        }

        String imageUrl = amazonS3.getUrl(bucketName, s3FileName).toString();
        user.updateImageUrl(imageUrl);
        User svaedUser = userRepository.findById(userId).orElseThrow(() -> new InvalidRequestException("User not found"));
        return new UserProfileResponse(svaedUser.getId(), svaedUser.getEmail(), svaedUser.getNickName(), svaedUser.getImageUrl());
    }

 

이미지 삭제 서비스

@Transactional
    public void deleteImage(Long userId, String imageUrl) {
        User user = userRepository.findById(userId).orElseThrow(() -> new InvalidRequestException("User not found"));
        try{
            URL url = new URL(imageUrl);
            String decodingKey = URLDecoder.decode(url.getPath(), "UTF-8");
            String key = decodingKey.substring(1); // 맨 앞의 '/' 제거
            amazonS3.deleteObject(new DeleteObjectRequest(bucketName, key));
        }catch (MalformedURLException | UnsupportedEncodingException e){
            throw new InvalidRequestException("이미지 처리중 오류가 발생했습니다.");
        }
        user.updateImageUrl(null);
    }

 

이미지 저장 및 삭제 테스트

저장테스트

 

삭제테스트

 

잘 삭제된다