본문 바로가기

개발공부/AWS

[AWS] 이미지 AWS S3에 업로드하기

💡 코드가 보이지 않으시다면 드래그 혹은 오른쪽 아래 🌜 아이콘을 눌러 테마 색을 변경해주세요.

 

 

안녕하세요!

키크니 개발자 입니다. 🦒

 

이미지를 AWS S3에 업로드하는 기능을 맡아서 구현한 김에 기록삼아 작성하였습니다!

 

DB 저장 로직을 제외한 S3에 이미지 업로드하는 예시입니다.

(DB 저장을 할 경우에는 이미지 업로드 후 데이터를 저장해주면 됩니다.)

 

아무나 S3에 파일을 업로드 하지 못하게 막으려면 IAM에서 accessKey와 secretKey를 발급 받아야 합니다.만약 누구나 업로드가 가능하게 하려면 S3 권한 > 버킷 정책에서 해당 정책을 생성한 후 적용해 주어야 합니다.

 

application.properties

# S3 Bucket
cloud.aws.credentials.accessKey=accessKey 입력
cloud.aws.credentials.secretKey=secretKey 입력
cloud.aws.stack.auto=false

# AWS S3 Service bucket
cloud.aws.s3.bucket=bucket name 입력
cloud.aws.region.static=region 입력

# AWS S3 Bucket URL
cloud.aws.s3.bucket.url=bucket 주소 입력

# multipart 사이즈 설정
spring.http.multipart.max.file.size=1024MB
spring.http.multipart.max.request.size=1024MB

💡 cloud.aws.stack.auto=false 안할 경우 아래와 같은 에러가 발생합니다.

 

발생 원인

AWS S3를 연동한 스프링부트 프로젝트를 aws 서버에 배포할 때 에러 발생합니다.

(로컬에서는 문제 없음) 프로젝트 배포시 기본으로 CloudFormation 구성을 시작하기 때문에 설정한 CloudFormation이 없으면 프로젝트 실행이 되지 않습니다.

해당 기능을 False로 설정해야합니다.

org.springframework.beans.factory.BeanCreationException: 
Error creating bean with name 'org.springframework.cloud.aws.core.env.ResourceIdResolver.BEAN_NAME': 
Invocation of init method failed; nested exception is org.springframework.beans.factory.BeanCreationException: 
Error creating bean with name 'stackResourceRegistryFactoryBean' defined in class path resource 
[org/springframework/cloud/aws/autoconfigure/context/ContextStackAutoConfiguration$StackAutoDetectConfiguration.class]: 
Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.cloud.aws.core.env.stack.config.StackResourceRegistryFactoryBean]: 
Factory method 'stackResourceRegistryFactoryBean' threw exception; nested exception is com.amazonaws.services.cloudformation.model.AmazonCloudFormationException: User: arn:aws:iam::xxxxxxxxx: is not authorized to perform: 
cloudformation:DescribeStackResources (Service: AmazonCloudFormation; Status Code: 403; Error Code: AccessDenied; Request ID: )

 

build.gradle

# S3
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
implementation platform('com.amazonaws:aws-java-sdk-bom:1.12.239')

# FilenameUtils Class 사용
implementation 'commons-io:commons-io:2.6'

implementation platform('com.amazonaws:aws-java-sdk-bom:1.12.239') 대신

implementation 'com.amazonaws:aws-java-sdk:1.11.404'을 사용하는 방법도 있습니다.

 

AwsConfig에 사용할 AmazonS3Client는 현재 deprecated가 되었습니다.

AmazonS3Client를 대신할 AmazonS3를 사용하겠습니다.

/**
 * Constructs a new Amazon S3 client using the specified AWS credentials to
 * access Amazon S3.
 *
 * @param awsCredentials
 *            The AWS credentials to use when making requests to Amazon S3
 *            with this client.
 *
 * @see AmazonS3Client#AmazonS3Client()
 * @see AmazonS3Client#AmazonS3Client(AWSCredentials, ClientConfiguration)
 * @deprecated use {@link AmazonS3ClientBuilder#withCredentials(AWSCredentialsProvider)}
 */
@Deprecated
public AmazonS3Client(AWSCredentials awsCredentials) {
    this(awsCredentials, configFactory.getConfig());
}

AwsConfig

@Configuration
public class AWSConfig {

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

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

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

    @Bean
    public AWSCredentials amazonAWSCredentials() {
        return new BasicAWSCredentials(amazonAWSAccessKey, amazonAWSSecretKey);
    }

    @Bean
    public AmazonS3 amazonS3() {
        return AmazonS3ClientBuilder.standard()
                .withCredentials(new AWSStaticCredentialsProvider(amazonAWSCredentials()))
                .withRegion(this.region)
                .build();
    }
}

Contorller

@Slf4j
@RequiredArgsConstructor
@RequestMapping("/user")
@RestController
public class UserController {

		private final UserService userService;
	    private final String BUCKET_NAME = "user_profile/";	


        @PostMapping("/image/upload/{userId}")
        public ResponseEntity<List<UserImageDTO>> uploadUserImage(@PathVariable String userId,
                                                            @RequestPart(value = "image") List<MultipartFile> images) throws IOException {
            log.info("user image upload to aws s3. userId={}, images={}", userId, images);

            return ResponseEntity.ok(
                    userService.uploadUserImage(BUCKET_NAME + userId, images)
            );
        }
    }

@RequestPart : MultipartFile 타입을 바인딩 해줍니다. (파일 업로드 시 주로 씁니다.)

저는 필수값이기 때문에 다른 옵션을 주지 않았지만, 혹시나 필수값이 아닐 경우에는 @RequestPart(required= false) 설정을 해주면 됩니다. 여러 이미지를 받기 위해서는 List<MultipartFile> 사용해야 합니다.

 

Service

@Slf4j
@RequiredArgsConstructor
@Service
public class UserProfileService {

    private final AmazonS3 amazonS3;

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

		// bucket 안에 폴더에 이미지를 업로드 합니다.
		public List<UserImageDTO> uploadUserImage(String userId, List<MultipartFile> images) throws IOException {
		        return uploadS3Images(makeS3FolderName(userId, BucketFolderType.PICTURE), images);
		    }
		
		
		// bucket 내부에서 /를 하면 folder로 분리되어집니다. (기존에 해당 folder가 없을 경우에는 생성이 되고, 있을 경우에는 해당 folder로 이미지가 업로드 됩니다.)
		private static String makeS3FolderName(String userId, BucketFolderType folderType) {
		        return delegateId + "/" + folderType;
		    }
		
		
		private List<UserImageDTO> uploadS3Images(String folderName, String childDelegateId, List<MultipartFile> images) {
		        List<UserImageDTO> results = new ArrayList<>();
		
		        images.forEach(image -> {
		            ObjectMetadata om = new ObjectMetadata();
		            om.setContentLength(image.getSize());
					om.setContentType(image.getContentType());
		
		            String extension = FilenameUtils.getExtension(image.getOriginalFilename());    // (1)
		            String now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));    // (2)
		            String imageUrl = folderName + String.format("/%s-%s.%s", now, RandomStringUtils.randomAlphabetic(4), extension);    // (3)
		
		            try {
		                amazonS3.putObject(
		                        new PutObjectRequest(S3_BUCKET_USER, imageUrl, image.getInputStream(), om));    // (4)
		                BufferedImage imageBuffer = ImageIO.read(image.getInputStream());
		                int width = imageBuffer.getWidth();
		                int height = imageBuffer.getHeight();
		
		                results.add(UserImageDTO.from(userId, imageUrl, width, height));
		
		            } catch (IOException e) {
						// custom Exception, 어떤 이미지가 에러를 발생시켰는지 기존 이름 로그 
		                log.info("Fail S3 Upload image. folderName={}, originImageName={}", folderName, image.getOriginalFilename());
		                throw new FailFileUploadException();
		            }
		        });
		
		        return results;
		    }

}

S3에 저장할 객체 메타데이터를 지정해줍니다.

(객체 메타데이터는 두가지 종류가 있습니다. 이중 시스템 정의 객체 메타데이터를 지정해줍니다.)

더 자세한 S3 객체 메타데이터는 참고 해주세요

contentType을 지정하지 않을 경우 Fild upload시 octet-stream으로 동작하여 특정 이미지 포멧의 경우 에러가 발생할 수 있습니다.

 

(1) : 이미지(파일) 확장자를 추출합니다.

(2) : 오늘날짜를 yyyyMMdd로 format 합니다. (ex : 220916)

(3) : 파일명을 현재날짜 + 랜덤 4글자 + 확장자를 붙여 해당 bucket folder url을 붙여줍니다.

(4) : S3 buckt에 이미지를 업로드 합니다.

 

BucketType

@Getter
@RequiredArgsConstructor
public enum BucketFolderType {
  PROFILE("프로필사진"),
  PICTURE("사진");
  
  private final String value;
}

 

Response

{
    "status": 200,
    "data": [
        {
            "userId": "QBSCZr6HK2J",
            "url": "/user_profile/QBSCZr6HK2J/PICTURE/20220905-rauJ.png",
            "width": 210,
            "height": 210
        },
        {
            "userId": "QBSCZr6HK2J",
            "url": "/user_profile/QBSCZr6HK2J/PICTURE/20220905-JZLB.jpeg",
            "width": 225,
            "height": 225
        }
    ]
}

포스트맨으로 확인해볼때에는 아래와 같이 form-data로 설정한 뒤 controller에 설정해둔 image value에 이미지를 등록시키면 됩니다.

 

 

 

References

https://docs.aws.amazon.com/ko_kr/AmazonS3/latest/userguide/UsingMetadata.html#object-key-guidelines

https://icthuman.tistory.com/entry/AWS-Java-SDK-S3-File-upload-2

https://www.notion.so/S3-456e8d942e0c423fabee6a6c12cb51ac#89772ab3493941e88bfdc5da73059968

반응형