Infra

몽수의 AWS S3 기능을 이용해서 이미지 업로드를 해보자!!!

기몽수 2024. 7. 14. 16:01

AWS S3란?

AWS S3(Simple Storage Service)의 약자로 주로 파일 서버로 사용된다.

 

 

AWS S3의 이점

확장성(Scalability)

  • 파일 서버는 트래픽이 증가함에 따라 서버 인프라 및 용량 계획을 변경해야 하는데, S3가 확장 및 성능 부분을 대신 처리해준다.

내구성(Durability)

  • 여러 영역에 여러 데이터 복사본을 저장하므로 한 영역이 다운되더라도 데이터를 사용할 수 있고, 복구가 가능하다.

 

 

어떻게 이미지 업로드를 할 수 있을까?

  1. 클라이언트에게 MultipartFile로 이미지 파일을 받는다.
  2. 이를 S3로 업로드하고, 이 S3에서는 이미지를 접근할 수 있는 public URL을 반환해준다.
  3. 이 URL을 통해 이미지 어디서나 접근하고 다운로드 할 수 있다.
  4. 이 URL을 DB에 저장하여 필요할 때 이 URL로 이미지 데이터를 사용하는 것

 

활용 방안 > 농사 관련 프로젝트에서 활용 할 수 있는 방법

  1. 사용자 프로필 이미지
  2. 다이어리에 농작물 사진 업로드
  3. 물물 교환에 사용자가 교환 하는 농작물 사진등을 업로드

→ 즉, 우리 프로젝트 Key Factor에 대한 기능 구현을 위해서는 이미지 업로드는 기본으로 구현되어있어야한다.

 

  


 

실제 구현

  1. AWS 생성
  2. 스프링 연동
  3. 스프링 설정
  4. Controller 구현해서 테스트

  

1. AWS 생성

 

1-1) AWS - 버킷 생성

버킷 : 다수의 객체를 관리하는 컨테이너 즉, 파일 시스템

  • 버킷 만들기 클릭

   

  • 버킷 이름 설정 → 고유해야하므로 yongsoo-ssafy-common-project로 설정

   

  • 다른 사용자가 접근 할 수 있도록 모든 퍼블릭 액세스 차단 해제해준다. 그외 설정은 기본값으로 해도됨

   

1-2) AWS - 사용자 생성

  • 상단 검색에서 IAM 검색

   

  • 그 후 사용자로 이동

   

  • 이름 설정

   

  • 권한 설정 → 직접 정책 연결
  • 우리는 S3에 대한 모든 권한이 필요하니까 AmazonS3FullAccess를 선택하고 다음을 눌러 생성한다.

   

1-3) IAM 액세스 키 생성

  • 방금 만든 사용자를 클릭한 후 액세스 키를 만들어준다.
  • 그 후 액세스 키 만들기
  • 사용 사례 나오는건 아무거나 클릭하고 넘어간다.
  • 그 다음 설명 태그는 선택사항이니까 써도 되고 안써도 됨

   

  • 액세스 키 값이 발급 된 경우다. → 딱 한번만 발급되니까 저장하기, 외부 유출되면 안됨
  • 두 키 모두 저장하기
  • 저는 글 포스팅 후 삭제 할 예정입니다.

   

  • 소유권까지 주면 끝
  • 버킷 소유자가 아닌 사람도 쓰기 권한을 얻어야 함

 

2. 스프링 연동

 

2-1) 스프링 연동하기 - 의존성 추가   

build.gradle implementation(의존성 추가)

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

→ aws 라이브러리를 사용하기 위한 의존성 추가

 

  

2-2) 스프링 연동하기 - application.properties에 설정 정보 추가

cloud.aws.region.static=ap-northeast-2 // 지역 코드 위에서 나라 뒤에 나온 코드
cloud.aws.stack.auto=false 
/*
EC2에서 Spring Cloud 실행 시키면 기본으로 CloudFormation 구성
우리는 설정한게 따로 없기 때문에 프로젝트 시작이 안되니까 False
*/
cloud.aws.s3.bucket=방금 만든 버킷이름(yongsoo-~~~)
cloud.aws.credentials.accessKey=방금 받은 액세스 키
cloud.aws.credentials.secretKey=방금 받은 시크릿 키

 

 

 

3. 스프링 설정

 

3-1) 스프링 설정 - Config 생성해주기

 

S3Config.java

// 해당 클래스가 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}")
    private String region;

    @Bean // Spring 컨텍스트에 Bean 등록
    public AmazonS3 amazonS3() {
        // AWS 접근키와 비밀키를 이용해서 AWSCredentials 객체 생성
        AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);
        // AmazonS3ClientBuilder 사용해서 Amazon S3 클라이언트 생성
        // 이 빌더를 통해 자격 증명과 지역을 설정할 수 있다.
        return AmazonS3ClientBuilder.
                standard()
                .withCredentials(new AWSStaticCredentialsProvider(credentials))
                //  -> AWS 자격 증명 설정 위에서 생성한 객체를 이용
                .withRegion(region)
                // -> S3 클라이언으가 사용할 지역 설정
                .build();
    }
}

    

3-2) 스프링 설정 - Service 생성

package com.project.aws.aws.service;

import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.CannedAccessControlList;
import com.amazonaws.services.s3.model.PutObjectRequest;
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.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Optional;
import java.util.UUID;

@Slf4j
@Service
@RequiredArgsConstructor
public class S3Service {

    private final AmazonS3 amazonS3;

    // application.properties 파일에서 받아 온 S3 버킷 이름
    @Value("${cloud.aws.s3.bucket}")
    private String bucket;

    // MultipartFile을 받아 S3에 업로드하는 메서드
    public String upload(MultipartFile multipartFile, String dirName) throws IOException {
        // MultipartFile을 File 객체로 변환
        File uploadFile = convert(multipartFile)
                .orElseThrow(() -> new IllegalArgumentException("MultipartFile -> File 변환 실패"));
        // 변환된 파일을 S3에 업로드하고 URL을 반환
        return upload(uploadFile, dirName);
    }

    // File 객체를 S3에 업로드하고 URL을 반환하는 메서드
    private String upload(File uploadFile, String dirName) {
        // 업로드할 파일의 경로 설정
        String fileName = dirName + "/" + uploadFile.getName();
        // S3에 파일 업로드
        String uploadImageUrl = putS3(uploadFile, fileName);
        // 로컬 서버에서 임시 파일 삭제
        removeNewFile(uploadFile);
        // 업로드된 파일의 URL 반환
        return uploadImageUrl;
    }

    // 로컬 서버에서 임시 파일을 삭제하는 메서드
    private void removeNewFile(File uploadFile) {
        if(uploadFile.delete()){
            log.debug("서버 파일 삭제");
        }else{
            log.debug("서버 파일 삭제 실패");
        }
    }

    // 파일을 S3에 업로드하고 URL을 반환하는 메서드
    private String putS3(File uploadFile, String fileName) {
        // S3에 파일 업로드 요청
        amazonS3.putObject(new PutObjectRequest(bucket, fileName, uploadFile)
                .withCannedAcl(CannedAccessControlList.PublicRead));
        // 업로드된 파일의 URL 반환
        return amazonS3.getUrl(bucket, fileName).toString();
    }

    // MultipartFile을 File 객체로 변환하는 메서드
    private Optional<File> convert(MultipartFile file) throws IOException {
        // 원본 파일 이름 가져옴
        String originalFilename = file.getOriginalFilename();
        // UUID 생성
        String uuid = UUID.randomUUID().toString();
        // UUID와 원본 파일 이름을 조합하여 고유한 파일 이름 생성 -> 공백은 _로 변환
        String uuidFileName = uuid + "_" + originalFilename.replaceAll("\\s", "_");

        // 변환 할 파일 객체 생성
        File convertFile = new File(uuidFileName);
        // 새로운 파일 생성 시도
        if (convertFile.createNewFile()) {
            try (FileOutputStream fos = new FileOutputStream(convertFile)) {
                fos.write(file.getBytes());
            }
            return Optional.of(convertFile);
        }
        return Optional.empty();
    }
}

   

간단한 코드 순서

  1. MultipartFile 전달 받음
  2. S3에 전달할 수 있도록 MultiPartFile을 File로 전환 -> S3에 MultipartFile 타입은 전송 되지 않는다.
  3. 전환된 File을 S3에 Public 읽기 권한으로 put -> 외부에서 정적 파일을 읽을 수 있도록 하기 위함
  4. 로컬에 생성된 File 삭제
  5. 업로드된 파일의 S3 URL 주소 반환

   


4. 테스트

4) Controller 구현해서 테스트

@RequiredArgsConstructor
@RestController
@Slf4j
public class S3Controller {

    private final S3Service s3Service;

    @PostMapping("/upload")
    public String upload(@RequestParam("data") MultipartFile file) throws IOException {
        return s3Service.upload(file, "static");
    }
}
  • 테스트를 위한 컨트롤러

   

테스트 요청

  • url이 리턴값으로 넘어온다.   

 

   

  • 실제 s3에 올라간 걸 볼 수 있음

 

   


+ 삭제

@DeleteMapping("/delete")
    public String delete(@RequestParam("url") String url) {
        s3Service.delete("static/" + url.substring(url.lastIndexOf("/") + 1));
        return "ok";
    }
    public void delete(String fileName){
        amazonS3.deleteObject(bucket, fileName);
    }

- 삭제에 대한 간단한 요청이다.

- aws에서만 삭제하는 것이기 때문에 DB에서도 제거 해줘야한다.

 

 

  


마지막으로

S3 이미지 업로드 기능을 활용하기 위해서 그대로 사용하면 이미지 URL에 버킷이 그대로 노출됩니다.

버킷이 노출되면 다른 사람들이 AWS

AWS S3자체에서 CloudFront 기능을 이용해서 버킷을 가릴 수 있도록 해야합니다.

다음 블로그 글로 작성하겠습니당.