공부/AWS

AWS S3 이용하여 이미지 업로드 구현하기

son_i 2024. 8. 24. 22:17
728x90

S3 이미지 업로드 개념

 1. Spring Boot Project에서 클라이언트에게 MultipartFile로 이미지 파일을 받는다.

 2. 이 파일을 S3에 업로드하고 S3에 어디서나 접근 가능한 public url을 반환한다.

 3. public url을 통해 이미지에 어디서나 접근/다운이 가능하다.

 4. url을 DB에 저장하여 필요할 때 url로 이미지 데이터를 사용할 수 있다.

 

구현과정

1. S3 버킷 생성

* 퍼블릭 액세스 차단을 해제 해야함

 

나머지는 기본 설정으로 두고 버킷 생성

 

버킷 정책 설정

이 작업을 안 해주면 추후 URL이 나왔을 때 들어가보려고 하면 아래와 같은 오류가 난다.

 

생성한 버킷 - 권한 - 버킷 정책 편집

 

아래와 같이 작성해준다.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicReadGetObject",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::<버킷명>/*"
        },
        {
            "Sid": "DenyOtherAccess",
            "Effect": "Deny",
            "Principal": "*",
            "Action": "s3:PutObject",
            "NotResource": [
                "arn:aws:s3:::<버킷명>/*.jpg",
                "arn:aws:s3:::<버킷명>/*.png",
                "arn:aws:s3:::<버킷명>/*.jpeg",
                "arn:aws:s3:::<버킷명>/*.gif"
            ]
        }
    ]
}

 

2. IAM 사용자 생성

이름 지정 

직접 정책 연결 -> AmazonS3FullAccess 체크

 

생성한 IAM 사용자에 들어가서 액세스 키 만들기

액세스 키 모범 사례 및 대안 이 나오는데 아무거나 골라도 된다. 나는 CLI 선택

 

나오는 키를 저장해둔다.

비밀 액세스 키는 다시 볼 수 없으므로 복사해두거나 csv 파일로 저장한다.

 

 

3. yml 구성

cloud:
  aws:
    credentials:
      accessKey: {위에서 발급한 key}
      secretKey: {위에서 발급한 key}
    s3:
      bucketName: {버킷 이름}
    region:
      static: ap-northeast-2
    stack:
      auto: false

 

4. S3 config 구성

package com.sj.Petory.config;

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

AmazonS3ClientBuilder를 이용해 Credentials과 Region을 등록해주고

AmazonS3 객체를 @Bean으로 등록한다.

 

5. AmazonS3Service

package com.sj.Petory.domain.member.service;

import com.amazonaws.SdkClientException;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.AmazonS3Exception;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.sj.Petory.exception.S3Exception;
import com.sj.Petory.exception.type.ErrorCode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.UUID;

import static com.sj.Petory.exception.type.ErrorCode.IMAGE_UPLOAD_FAIL;

@RequiredArgsConstructor
@Slf4j
@Service
public class AmazonS3Service {
    private final AmazonS3 amazonS3;

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

    public String upload(MultipartFile image) {
        if (image.isEmpty() || Objects.isNull(image.getOriginalFilename())) {
            throw new S3Exception(ErrorCode.FILE_EMPTY);
        }
        return this.uploadImage(image);
    }
    private String uploadImage(MultipartFile image) {
        String randomFilename = generateRandomFilename(image);

        ObjectMetadata metadata = new ObjectMetadata();
        metadata.setContentLength(image.getSize());
        metadata.setContentType(image.getContentType());

        log.info("File Upload Started : " + randomFilename);
        try {
            amazonS3.putObject(bucketName, randomFilename, image.getInputStream(), metadata);
        } catch (AmazonS3Exception e) {
            log.error("Amazon S3 error while uploading file: " + e.getMessage());
            throw new S3Exception(IMAGE_UPLOAD_FAIL);
        } catch (SdkClientException e) {
            log.error("AWS SDK client error while uploading file: " + e.getMessage());
            throw new S3Exception(IMAGE_UPLOAD_FAIL);
        } catch (IOException e) {
            log.error("IO error while uploading file: " + e.getMessage());
            throw new S3Exception(IMAGE_UPLOAD_FAIL);
        }

        log.info("File upload Success : " + randomFilename);

        return amazonS3.getUrl(bucketName, randomFilename).toString();
    }

    private String generateRandomFilename(MultipartFile image) {
        String originName = image.getOriginalFilename();
        String fileExtension = validateFileExtension(originName);

        return UUID.randomUUID() + "." + fileExtension;
    }

    private String validateFileExtension(String originName) {
        String fileExtension = originName.substring(originName.lastIndexOf(".") + 1);
        log.info(fileExtension);
        List<String> allowedExtensions = Arrays.asList("jpg", "png", "gif", "jpeg");

        if (!allowedExtensions.contains(fileExtension)) {
            throw new S3Exception(ErrorCode.FILE_EXTENSION_NOT_ALLOWED);
        }
        return fileExtension;
    }
}

 

메소드 별로 설명하겠다.

 

upload(MultipartFile image)

우리가 사용하는 서비스에서 호출될 메소드로 image 파일이 비어있는 지, 파일이름이 null 인지를 검사한다.

정상 파일이면 해당 클래스의 uploadImage() 메소드를 호출한다.

 

uploadImage(MultipartFile image)

실제 s3에 이미지를 업로드하는 메소드이다.

 

generateRandomFilename() 메소드를 통해 filename을 랜덤으로 생성한다.

 

metadate를 설정해주고

amazonS3.putObject() 메소드를 이용해 s3에 업로드한다. 

 

generateRandomFilename(MultipartFile image) 

이미지의 원래 파일 명에서 validateFileExtension()을 통해 유효한 확장자인지 (이미지 파일인지)를 검사하고 파일 확장자를 뽑아온다.

 

반환값으로 랜덤 파일명.파일확장자를 리턴한다.

 

validateFileExtension(String originName) 

파일 이름에서 . 뒷부분 확장자를 추출해서 List에 저장해둔 확장자가 아니면 예외를 터트린다.

유효한 확장자일 경우 확장자 문자열을 리턴한다.

 

 

6. 컨트롤러

@PatchMapping("/image")
    public ResponseEntity<?> s3ImageUpload(
            @AuthenticationPrincipal MemberAdapter memberAdapter
            , @RequestPart(value = "image", required = false) MultipartFile image) {

        return ResponseEntity.ok(memberService.imageUpload(memberAdapter, image));
    }

이미 가입된 회원에 이미지를 변경하는 느낌이므로 PATCH를 사용한다.

MultipartFile 타입으로 이미지를 받는다.

 

7. 서비스

@Transactional
    public String imageUpload(
            final MemberAdapter memberAdapter, final MultipartFile image) {

        Member member = getMemberByEmail(memberAdapter.getEmail());

        String imageUrl = amazonS3Service.upload(image);

        member.updateImage(imageUrl);

        return imageUrl;
    }

5에서 작성한 amazonS3Service클래스에 upload 메소드를 호출한다.

이미 가입된 회원에 이미지 필드를 수정하는 것이므로 회원정보 수정처럼 더티체킹을 이용해야한다.

서비스 메소드에 @Transactional을 붙여 변경을 감지할 수 있도록 해준다.

 

간단하게 s3에 이미지 업로드가 완료됐다 !

 

여기에 리턴값으로 내려오는 imageUrl로 접속하면 S3에 업로드한 이미지를 확인 할 수 있다.

 

간단하게 구현해봤지만

이제 이 이미지 url을 사용자 엔티티에 추가로 넣어야 한다. - O 완료

또한 이미지 삭제, 여러 개의 이미지 업로드 시 저장 로직(커뮤니티 게시글 용)을 구현해야 한다.

 

 

 

Trouble Shooting

버킷 정책을 아래처럼 모든 곳에서 접속할 수 있게 작성하지 않으면 오류가 난다 !

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicReadGetObject",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::<버킷명>/*"
        },
        {
            "Sid": "DenyOtherAccess",
            "Effect": "Deny",
            "Principal": "*",
            "Action": "s3:PutObject",
            "NotResource": [
                "arn:aws:s3:::<버킷명>/*.jpg",
                "arn:aws:s3:::<버킷명>/*.png",
                "arn:aws:s3:::<버킷명>/*.jpeg",
                "arn:aws:s3:::<버킷명>/*.gif"
            ]
        }
    ]
}

 

 


참고

 

버킷 생성, IAM 사용자 생성 참고

https://innovation123.tistory.com/198

 

[AWS/S3] Spring boot project 이미지 업로드를 위해 S3 버켓 만들기

Amazon S3 버킷 만들기 IAM 만들기 생성 완료 IAM accessKey, secretKey 얻기 IAM - 사용자 - 보안 자격 증명 액세스 키 만들기 CLI 선택 accessKey, secretKey 저장 저 두 가지 Key를 저장해 뒀다가 spring properties에 등

innovation123.tistory.com

 

버킷 정책 설정 참고 & 전체적인 로직 참고

https://velog.io/@nyoung215/Spring-S3-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%97%85%EB%A1%9C%EB%93%9C%ED%95%98%EA%B8%B0#aws-identity-and-access-management%EB%9E%80

 

[Spring] S3 이미지 업로드하기

사진, 동영상 등 파일을 저장하기 위해 사용하는 파일 서버 서비스. 일반적인 파일서버는 트래픽이 증가함에 따라서 장비를 증설하는 작업을 해야 하는데 S3는 이와 같은 것을 대행한다.

velog.io

 

전체적인 로직 참고

https://innovation123.tistory.com/197

 

[Spring / S3] SpringBoot 프로젝트 - S3 이미지 업로드

이전 글에서 S3 bucket과 IAM을 생성하고 SpringBoot project에서 S3 접근에 사용할 accessKey와 secretKey를 얻는 것까지 다뤘다. 2024.01.21 - [DevOps] - [AWS/S3] Spring boot project 이미지 업로드를 위해 S3 버켓 만들기 [

innovation123.tistory.com

 

다중 파일 업로드 

https://ksh-coding.tistory.com/67

 

[Spring] Postman multipart/form-data 여러 개 파일 보내기

프로젝트를 진행하다가, multipart/form-data로 파일을 보낼 일이 생겼었다. 컨트롤러 코드를 다음과 같이 구현했다. 이미지, 영상 첨부하는 PostController @PostMapping("/save") public BaseResponse save( @RequestPart P

ksh-coding.tistory.com

 

728x90