AWS

AWS - S3 이미지 업로드(SpringBoot)

DJ.Kang 2024. 11. 1. 19:19

□ 버켓 생성

  1. Create Bucket 클릭
  2. Bucket Name 작성
  3. 객체 소유권 설정(ACL) → ACLs enabeld 선택
    ※ ACL(Access Control List) : 접근 제어 목록
    ACL 을 통해 S3 버킷 및 버킷의 오브젝트에 대한 접근을 관리할 수 있다.
    버킷과 개별 객체들의 권한은 독립적이다.객체는 객체가 속한 버킷으로부터 권한을 상속받지 않는다.
    - ACLs disabled : 내가 접속한 계정만 해당 버킷을 소유
    - ACLs enabled : 다른 AWS계정에서도 소유권을 갖거나 접속제어가 가능
  4. Block Public Access
    • 새 ACL(액세스 제어 목록)을 통해 부여된 버킷 및 객체에 대한 퍼블릭 액세스 차단
    • 임의의 ACL(액세스 제어 목록)을 통해 부여된 버킷 및 객체에 대한 퍼블릭 액세스 차단
    • 새 퍼블릭 버킷 또는 액세스 지점 정책을 통해 부여된 버킷 및 객체에 대한 퍼블릭 액세스 차단
    • 임의의 퍼블릭 버킷 또는 액세스 지점 정책을 통해 부여된 버킷 및 객체에 대한 퍼블릭 액세스 차단
  5.  나머지는 기본값으로 두고 생성

□ I AM 유저 생성

  1. Create User 클릭
  2. 권한 정책 부여(AmazonS3FullAccess)
  3. root계정에서 만든 I AM계정으로 들어가 Security credentials - Create access key 클릭

□ SpringBoot 적용

  1. 의존성 추가
    implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
  2. properties설정
    # AWS S3
    cloud.aws.credentials.accessKey=${aws-access-key} // 엑세스 키
    cloud.aws.credentials.secretKey=${aws-secret-key} // 시크릿 키
    cloud.aws.region.static=ap-northeast-2 // 서울
    cloud.aws.s3.bucket=${bucket-name} // 버킷 이름
    spring.servlet.multipart.enabled=true // 멀티파트 파일 업로드를 지원하도록 활성화


  3. S3Config
    import com.amazonaws.auth.AWSCredentials;
    import com.amazonaws.auth.AWSStaticCredentialsProvider;
    import com.amazonaws.auth.BasicAWSCredentials;
    import com.amazonaws.services.s3.AmazonS3;
    import com.amazonaws.services.s3.AmazonS3ClientBuilder;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @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 amazonS3() {
            AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);
    
            return AmazonS3ClientBuilder
                    .standard()
                    .withCredentials(new AWSStaticCredentialsProvider(credentials))
                    .withRegion(region)
                    .build();
        }
    }


  4. S3Service
    import com.amazonaws.services.s3.AmazonS3;
    import com.amazonaws.services.s3.model.CannedAccessControlList;
    import com.amazonaws.services.s3.model.DeleteObjectRequest;
    import com.amazonaws.services.s3.model.ObjectMetadata;
    import com.amazonaws.services.s3.model.PutObjectRequest;
    import com.play.hiclear.common.exception.CustomException;
    import com.play.hiclear.common.exception.ErrorCode;
    import lombok.RequiredArgsConstructor;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Service;
    import org.springframework.web.multipart.MultipartFile;
    
    import java.io.IOException;
    import java.io.InputStream;
    import java.util.UUID;
    
    @Service
    @RequiredArgsConstructor
    public class AwsS3Service {
        @Value("${cloud.aws.s3.bucket}")
        private String bucket;
    
        private final AmazonS3 amazonS3;
    
        public String uploadFile(MultipartFile multipartFile){
    
            if (multipartFile == null || multipartFile.isEmpty()) {
                return null;
            }
    
            String fileName = createFileName(multipartFile.getOriginalFilename());
            ObjectMetadata objectMetadata = new ObjectMetadata();
            objectMetadata.setContentLength(multipartFile.getSize());
            objectMetadata.setContentType(multipartFile.getContentType());
    
            try(InputStream inputStream = multipartFile.getInputStream()){
                amazonS3.putObject(new PutObjectRequest(bucket, fileName, inputStream, objectMetadata)
                        .withCannedAcl(CannedAccessControlList.PublicRead));
            } catch (IOException e){
                throw new CustomException(ErrorCode.IO_ERROR);
            }
    
            return fileName;
        }
    
        // 파일명을 난수화하기 위해 UUID 를 활용하여 난수를 돌린다.
        public String createFileName(String fileName){
            return UUID.randomUUID().toString().concat(getFileExtension(fileName));
        }
    
        //  "."의 존재 유무만 판단
        private String getFileExtension(String fileName){
            try{
                return fileName.substring(fileName.lastIndexOf("."));
            } catch (StringIndexOutOfBoundsException e){
                throw new CustomException(ErrorCode.IMAGE_BAD_REQUEST);
            }
        }
    
    
        public void deleteFile(String fileName){
            amazonS3.deleteObject(new DeleteObjectRequest(bucket, fileName));
            System.out.println(bucket);
        }
    }
  5. 사진을 업로드 및 삭제하는 Controller
    @RestController
    @RequiredArgsConstructor
    public class AwsS3Controller {
    	
        private final AwsS3Service s3Service;
    
    	// 파일 업로드
        @PostMapping("/images")
        public ResponseEntity<String> updateImage(
                @RequestParam("image") MultipartFile image) {
            return ResponseEntity.ok(s3service.uploadFile(image));
        }
    
        // 파일 삭제
        @DeleteMapping("/images")
        public ResponseEntity<String> deleteImage(
                @RequestParam String fileName) {
            return ResponseEntity.ok(s3Service.deleteFile(fileName));
        }
    }

◎ S3Service 로직 분석

- 파일 생성

String fileName = createFileName(multipartFile.getOriginalFilename());

- 메타데이터 설정

ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentLength(multipartFile.getSize());
objectMetadata.setContentType(multipartFile.getContentType());

- S3에 파일 업로드

try(InputStream inputStream = multipartFile.getInputStream()){
    amazonS3.putObject(new PutObjectRequest(bucket, fileName, inputStream, objectMetadata)
            .withCannedAcl(CannedAccessControlList.PublicRead));
} catch (IOException e){
    throw new CustomException(ErrorCode.IO_ERROR);
}

 

  • multipartFile.getInputStream()을 호출하여 파일 내용을 입력 스트림으로 가져온다, 이 입력 스트림을 사용하여 PutObjectRequest를 생성
  • amazonS3.putObject() 메서드를 통해 S3에 파일을 업로드,
    withCannedAcl(CannedAccessControlList.PublicRead)를 사용하여 업로드된 파일을 공개적으로 읽을 수 있도록 설정
  • try-with-resources 구문을 사용하여 입력 스트림을 자동으로 닫는다.
    IOException이 발생하면 커스텀 예외 CustomException을 발생
    // 파일명을 난수화하기 위해 UUID 를 활용하여 난수를 돌린다.
    public String createFileName(String fileName){
        return UUID.randomUUID().toString().concat(getFileExtension(fileName));
    }

    //  "."의 존재 유무만 판단
    private String getFileExtension(String fileName){
        try{
            return fileName.substring(fileName.lastIndexOf("."));
        } catch (StringIndexOutOfBoundsException e){
            throw new CustomException(ErrorCode.IMAGE_BAD_REQUEST);
        }
    }