[AWS] AWS S3 이미지 업로드

Donghoon Jeong·2024년 6월 8일
0

AWS

목록 보기
2/4
post-thumbnail

이번 포스팅에서는 이미지를 저장하고 업로드하기 위해 AWS S3 버킷과 스프링 부트 프로젝트를 연동하여 사용하는 방법을 정리해 보겠습니다.


AWS S3

S3는 AWS에서 제공해 주는 파일을 업로드하고 다운로드하는 등의 스토리지 역할에 특화된 서비스입니다. S3는 분산된 스토리지를 사용하며 S3를 사용하는 클라이언트가 원하는 만큼의 용량을 제공해 줍니다.

AWS S3의 이점

  1. 높은 내구성과 가용성

    S3는 분산된 환경에 다중 복제를 해서 저장하기 때문에 어느 한 영역이 다운되더라도 데이터를 사용할 수 있고, 복구가 가능합니다. 그렇기 때문에 높은 내구성과 가용성을 제공합니다.

  2. 확장성

    파일 서버는 트래픽이 증가함에 따라 서버 인프라 및 용량 계획을 변경해야 되는데, S3는 내부적으로 스토리지에 대한 오토 스케일링 (부족하면 늘리고 남아돌면 줄임)을 할 수 있기 때문에 확장 및 성능 부분을 대신 처리해 줍니다.

  3. 보안

    S3는 자체적인 암호화 기능, 접근 허용 목록 등 강력한 보안 기능을 제공합니다. 따라서 편하게 업로드 한 리소스에 대한 접근을 제어 가능합니다.


AWS S3 설정

이제 본격적으로 S3를 사용하기 위한 초기 세팅을 하도록 하겠습니다.

버킷 생성

AWS Console → S3 → 버킷 → 버킷 만들기

버킷 이름을 입력하고 퍼블릭으로 설정하기 위해 액세스 차단 설정을 해제합니다.

버킷이 성공적으로 생성되었습니다.

폴더 생성

실제 프로젝트에서 파일을 업로드하는 상황이 여러 가지가 존재할 수 있습니다. (프로필 사진 업로드, 리뷰 사진 업로드 등)

따라서 각각에 대해서 컴퓨터의 파일을 정리하는 것과 비슷하게 파일을 만들어서 저장하면 관리가 더욱 용이해지기 때문에 파일에 사진을 저장하도록 하겠습니다.

리뷰와 관련된 사진을 저장할 파일을 생성하겠습니다.

사용자 생성

S3에 접근하기 위해서는 IAM 사용자에게 S3 접근 권한을 주고, 액세스 키와 비밀 액세스 키를 만들어 이를 통해 접근해야 합니다. 접근 권한이 있는 사용자를 생성하고 액세스 키와 비밀 키를 발급받는 과정을 진행하겠습니다.

AWS console → IAM → 액세스 관리 → 사용자 → 사용자 추가

사용자 이름을 입력하고 직접 정책 연결 → AmozonS3FullAccess 권한을 선택합니다.

사용자를 생성하고 난 후, 생성한 사용자의 보안 자격 증명에서 액세스 키를 생성하겠습니다.

사용할 사례에 맞게 선택하시면 됩니다. 스프링 부트에서 사용할 예정이기 때문에 AWS 외부에서 실행되는 애플리케이션을 선택하겠습니다.

액세스 키 생성 완료 화면에서 생성된 공개키와 비밀키를 확인할 수 있습니다. 추후에 해당 키값을 사용해야 하기 때문에 .csv 파일로 저장하거나 복사해서 따로 저장해둬야 합니다.


Spring Boot 설정

의존성 추가

build.gradle

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

이 의존성은 Spring Cloud AWS를 사용하여 AWS S3와의 통합을 지원합니다. AWS 서비스와의 상호작용을 간편하게 해주는 라이브러리입니다.

설정 정보 추가

cloud:
  aws:
    s3:
      bucket: myumcbucket
      path:
        review: reviews
    region:
      static: ap-northeast-2
    stack:
      auto: false
    credentials:
      accessKey: AKIASY54NL7QOATCUHZF
      secretKey: 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

위에서 발급받은 액세스 키와 비밀 키를 설정 정보 파일에 추가하고 application.yml에서 받아오고 싶은 대상에 org.springframework.beans.factory.annotation에서 제공하는 @Value 어노테이션을 붙여서 가져올 수 있습니다. 하지만 이렇게 평문으로 저장하여 github에 올릴 경우 큰일이 날 수 있기 때문에 로컬에서 환경 변수를 지정하는 작업을 진행하겠습니다. 배포된 환경에서 지정할 경우엔 따로 환경 변수 설정 작업을 해주셔야 합니다.

AWS_ACCESS_KEY_ID=AKIASY55ML7QOATCUHZP;AWS_SECRET_ACCESS_KEY=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;

이런 형식으로 각각의 환경 변수는 ;로 구분해서 지정해 주면 됩니다.

cloud:
  aws:
    s3:
      bucket: myumcbucket
      path:
        review: reviews
    region:
      static: ap-northeast-2
    stack:
      auto: false
    credentials:
      accessKey: ${AWS_ACCESS_KEY_ID}
      secretKey: ${AWS_SECRET_ACCESS_KEY}

해당 작업 후, AWS S3 버킷 정보와 자격 증명 정보를 환경 변수로 받아 설정합니다.
${AWS_ACCESS_KEY_ID}${AWS_SECRET_ACCESS_KEY}는 로컬에서 설정한 환경 변수를 통해 값을 읽어옵니다. 이외에도 버킷 이름, 리뷰 경로, 리전 정보 등을 포함하고 있습니다.

이미지 업로드

Uuid.java

@Entity
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class Uuid extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true)
    private String uuid;
}

이후 AWS S3에 업로드 시 UUID를 파일 이름에 붙여서 업로드해서 업로더가 지은 파일의 이름이 동일하더라도 각각의 파일이 식별 되도록 Uuid 클래스를 생성하겠습니다.

AmazonS3Manager.java

@Slf4j
@Component
@RequiredArgsConstructor
public class AmazonS3Manager {

    private final AmazonS3Client amazonS3Client;
    private final AmazonConfig amazonConfig;

    // MultipartFile 리스트를 전달받아 S3에 업로드
    public List<String> upload(List<MultipartFile> multipartFiles, String dirName, Uuid uuid) throws IOException {
        List<String> uploadImageUrls = new ArrayList<>();
        for (MultipartFile multipartFile : multipartFiles) {
            File uploadFile = convert(multipartFile)
                    .orElseThrow(() -> new IllegalArgumentException("MultipartFile -> File 전환 실패"));
            String uploadImageUrl = upload(uploadFile, dirName, uuid.getUuid());
            uploadImageUrls.add(uploadImageUrl);
        }
        return uploadImageUrls; // 업로드된 파일들의 S3 URL 주소 리스트 반환
    }

    private String upload(File uploadFile, String dirName, String uuid) {
        String fileName = dirName + "/" + uuid + 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(amazonConfig.getBucket(), fileName, uploadFile)
                        .withCannedAcl(CannedAccessControlList.PublicRead) // PublicRead 권한으로 업로드 됨
        );
        return amazonS3Client.getUrl(amazonConfig.getBucket(), fileName).toString();
    }

    private void removeNewFile(File targetFile) {
        if (targetFile.delete()) {
            log.info("파일이 삭제되었습니다.");
        } else {
            log.info("파일이 삭제되지 못했습니다.");
        }
    }

    private Optional<File> convert(MultipartFile file) throws IOException {
        File convertFile = new File(Objects.requireNonNull(file.getOriginalFilename()));
        if (convertFile.createNewFile()) {
            try (FileOutputStream fos = new FileOutputStream(convertFile)) {
                fos.write(file.getBytes());
            }
            return Optional.of(convertFile);
        }
        return Optional.empty();
    }
}

AmazonS3Manager 클래스는 upload() 메서드를 통해 여러 개의 MultipartFile을 받아서 S3에 업로드하고, 업로드된 파일들의 URL을 리스트로 반환합니다. 이때, 각 파일에는 고유한 UUID가 부여됩니다.

업로드 과정은 다음과 같습니다:

  1. upload() 메서드는 받은 MultipartFile들을 반복하면서 각 파일을 convert() 메서드를 통해 File 객체로 변환합니다.

  2. 변환된 파일은 upload() 메서드를 통해 S3에 업로드됩니다. 이때, 파일 이름은 지정된 디렉토리명과 UUID로 구성됩니다.

  3. 업로드가 완료되면 로컬에 생성된 파일은 removeNewFile() 메서드를 통해 삭제됩니다.

  4. 각 파일의 업로드가 완료되면 업로드된 파일들의 URL이 리스트에 추가되고, 이 리스트가 최종적으로 반환됩니다.

이러한 방식으로 AmazonS3Manager 클래스는 안전하고 효율적으로 파일을 AWS S3에 업로드하고, 업로드된 파일들의 URL을 반환합니다.

AmazonConfig.java

@Configuration
@Getter
public class AmazonConfig {

    private AWSCredentials awsCredentials;

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

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

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

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

    @Value("${cloud.aws.s3.path.review}")
    private String reviewPath;

    @PostConstruct
    public void init() {
        this.awsCredentials = new BasicAWSCredentials(accessKey, secretKey);
    }

    @Bean
    public AmazonS3Client amazonS3Client() {
        AWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey);

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

이 클래스는 AmazonS3Manager 클래스에서 AWS S3 클라이언트를 주입받아 사용하여 파일을 업로드하는 과정에서 필요한 설정 정보를 제공합니다. 설정 정보를 통해 안전하고 효율적으로 AWS S3에 접근하여 파일을 업로드할 수 있습니다.

Controller.java


@PostMapping(value = "/{storeId}/reviews", consumes = "multipart/form-data")
public ApiResponse<StoreResponseDTO.ReviewResponseDTO> createReview (
    @RequestPart("request") @Valid StoreRequestDTO.ReviewRequestDTO request,
    @RequestPart("images") List<MultipartFile> multipartFiles,
    @ExistStore @PathVariable(name = "storeId") Long storeId,
    @ExistMember @RequestParam(name = "memberId") Long memberId) throws IOException {

    Review review = storeCommandService.createReview(memberId, storeId, request, multipartFiles);
    return ApiResponse.onSuccess(StoreConverter.toReviewResponseDTO(review));
}

해당 코드는 가게 대한 리뷰와 리뷰 사진을 리스트 형태로 등록하는 컨트롤러입니다. 여기서 주의할 점은 매개변수를 받을 때, @RequestBody 어노테이션이 아닌 @RequestPart 어노테이션을 사용한다는 점입니다.

Service.java

@Override
public Review createReview(Long memberId, Long storeId, StoreRequestDTO.ReviewRequestDTO request, List<MultipartFile> multipartFiles) throws IOException {

	...

    // 리뷰 엔티티 생성
    Review review = StoreConverter.toReview(request);

    // UUID 생성 및 저장
    String uuid = UUID.randomUUID().toString();
    Uuid savedUuid = uuidRepository.save(Uuid.builder().uuid(uuid).build());

    // 이미지 파일 업로드 및 URL 리스트 반환
    List<String> pictureUrls = s3Manager.upload(multipartFiles, amazonConfig.getReviewPath(), savedUuid);

    // 리뷰 엔티티에 회원과 가게 정보 설정
    review.setMember(member);
    review.setStore(store);

    // 리뷰 저장
    Review savedReview = reviewRepository.save(review);

    // 업로드된 이미지 URL 리스트를 통해 ReviewImg 엔티티 리스트 생성 및 저장
    List<ReviewImg> reviewImgList = ReviewConverter.toReviewImgList(pictureUrls, savedReview);
    reviewImgRepository.saveAll(reviewImgList);

    return savedReview;
}

upload 메서드는 여러 개의 MultipartFile을 받아서 AWS S3에 업로드하고, 업로드된 파일들의 URL을 리스트로 반환합니다. 해당 메서드의 매개변수의 amazonConfig.getReviewPath()는 버킷에서 생성한 폴더 중 저장할 폴더의 정보를 적어주면 됩니다. 이 코드는 리뷰와 관련된 이미지를 저장하는 코드이기 때문에 reviews 폴더의 정보가 들어갑니다.

이 코드는 주어진 요청을 기반으로 리뷰를 생성하고, 해당 리뷰에 속한 이미지 파일을 AWS S3에 업로드한 뒤, 주어진 이미지 URL 리스트와 저장된 리뷰 정보를 바탕으로 ReviewImg 엔티티 리스트를 생성한 후, 이를 데이터베이스에 저장하는 작업을 수행합니다.

MultipartJackson2HttpMessageConverter.java

@Component
public class MultipartJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter {

    public MultipartJackson2HttpMessageConverter(ObjectMapper objectMapper) {
        super(objectMapper, MediaType.APPLICATION_OCTET_STREAM);
    }

    @Override
    public boolean canWrite(Class<?> clazz, MediaType mediaType) {
        return false;
    }

    @Override
    public boolean canWrite(Type type, Class<?> clazz, MediaType mediaType) {
        return false;
    }

    @Override
    protected boolean canWrite(MediaType mediaType) {
        return false;
    }
}
{
    "type": "about:blank",
    "title": "Unsupported Media Type",
    "status": 415,
    "detail": "Content-Type 'application/octet-stream' is not supported.",
    "instance": "/stores/2/reviews"
}

다음과 같은 오류를 해결하기 위해 MultipartJackson2HttpMessageConverter 클래스를 추가해 줍니다.


테스트

로그인은 아직 구현되어 있지 않은 상태이기 때문에 파라미터로 넘겨주고 리뷰에 대한 데이터와 이미지 정보를 담아 요청을 보냈을 때, 성공적으로 저장된 것을 확인할 수 있습니다.

업로드한 사진이 버킷의 reviews 폴더에도 등록된 것을 확인할 수 있습니다.

profile
정신 🍒 !

0개의 댓글