[AWS] 이미지 저장을 위한 S3 버킷 생성 및 SpringBoot(Gradle) 에 연동하기

Noh Jihyeon·2023년 9월 27일
4

AWS

목록 보기
1/2
post-thumbnail

🔸 오늘의 목표: 이미지 저장을 위한 AWS S3 구현하기

SpringBoot: 3.1.3
Build: Gradle
Java: 17
OS: Window




📌 미니 프로젝트를 진행하면서 이미지 저장이 필요하게 되었다.
이미지는 S3에 저장하고 저장된 URL만 DB에 저장해서 불러오기, 수정, 삭제를 구현할 수 있도록 S3를 만들어보고 적용까지 시도해봤다.

S3 버킷 생성 방법이 기록된 여러 블로그를 보고 S3를 생성 및 적용을 했는데 AWS 웹사이트가 업데이트 주기가 짧은지건지 설명과 안맞는 부분과 부족한 부분이 있어서 상당히 힘들었다.

🙋‍♀ 그래서 나는 나중에 이 블로그를 방문하는 사람들이 어느 부분이 달라졌고 달라진 부분의 설정을 어떻게 찾아보면 되는지 비교할 수 있도록 현제 버전에서의 자세한 내용을 기록해보려고 한다.




[1] AWS S3 개요

1) S3란?

👉 S3는 Simple Storage Service의 약자로 주로 파일을 저장하는데 주로 사용된다.


2) S3를 사용하는 이유



확장성(Scalability)

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

내구성(Durability)

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




3) S3 관련 용어



객체(object)

파일과 파일정보로 구성된 저장단위로 그냥 파일이라 생각하면 된다.

버킷(Bucket)

다수의 객체를 관리하는 컨테이너로 파일시스템이라 보면된다.




4) S3 무료사용 기간과 용량

⭐ Amazon S3는 계정 가입일을 기준을 12개월 동안 무료로 사용이 가능하며, 제한 범위는 아래와 같으니 과금방지를 위해 참고하자.






[2] 버킷 생성하기

1) AWS Console > S3 > 버킷 > 버킷 만들기 클릭

  • S3 접속 후 버킷 만들기 클릭



⛔ 버킷 이름은 중복설정 불가


🔽🔽🔽




  • 버킷 이름 : 원하는 이름으로 설정
  • 리전: 서울 선택
    -> 리전은 더 빠른 속도로 사용하기 위해 보통 버킷이 많이 사용될 지역을 선택해주면 된다.



2) 버킷 생성 정보의 객체 소유권 선택


S3를 설정할 때 가장 일반적인 설정 방법을 선택했다.
사용자에 따라 액세스 차단 설정을 통해 필요한 항목에 대한 액세스를 제한하거나 Spring Security를 사용하여 파일 조작 권한을 부여할 수 있는데 아직 그 부분을 공부하지 않았으므로 나는 이 방법을 선택했다 🌝


남은 설정은 이렇게 기본값으로 설정하고 버킷 만들기 를 클릭해서 S3 버킷을 생성하면 된다.





[3] IAM 사용자 생성하기

  • S3에 접근하기 위해서는 IAM 사용자에게 S3접근 권한을 부여하고 해당 사용자의 엑세스 키, 시크릿키를 사용해야 한다.

따라서 S3용 IAM을 생성하고 엑세스 키값을 반드시 저장해야한다!!

1) AWS console > IAM > 엑세스 관리 > 사용자 > 사용자 생성 클릭

사용자 이름을 입력하고 다음을 누른다




권한 옵션 : 직접 정책 연결
권한 정책 : AmazonS3FullAccess


으로 설정하고

다음을 눌러서 생성까지 완료한다.




📌 23.09.28 기준)
다른 사이에서는 IAM 생성시 엑세스 키-값이 자동으로 생성된다고 나왔지만 현재 기준으로는 IAM 생성 후 엑세스 키를 직접 발급해줘야 한다.
다른 블로그의 설명에서 아무리 찾아봐도 엑세스키가 자동발급이 안되서 헤맸는데 아주 간단하게 발급이 가능하니 아래의 방법으로 진행하면 된다.




2) IAM 엑세스 키 생성하기

엑세스 키 만들기 클릭




사용 사례를 선택하게 나오는데 말 그대로 사용 사례와 대안을 띄워주는 기능만 하기때문에 뭘 선택해도 상관 없다.

참고로 나는 이걸 선택했다.
다음 클릭


이 부분은 선택사항이라 빈칸으로 넘어가도 된다.
나는 AWS의 다른 기능들을 추가하면서 이 사용자가 어디에 연결되어 있는지 헷갈리는 경우가 많았던지라 왠만하면 구분이 가능하게 태그를 추가하는 편이다.



엑세스 키 값이 발급되었다.
꼭 파일로 저장하자!!





[4] 스프링에 연동하기

1) build.gradle에 의존성 추가

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

2) application.properties 에 설정 정보 추가

cloud.aws.credentials.accessKey=IAM의 엑세스 키
cloud.aws.credentials.secretKey=IAM의 시크릿 키
cloud.aws.region.static=ap-northeast-2
cloud.aws.stack.auto=false
cloud.aws.s3.bucket=S3버킷이름(test-bucket-9876543)



3) config > S3Config.java 생성



@Configuration
public class S3Config {
    @Value("${cloud.aws.credentials.access-key}")
    private String accessKey;

    @Value("${cloud.aws.credentials.secret-key}")
    private String accessSecret;
    @Value("${cloud.aws.region.static}")
    private String region;

    @Bean
    public AmazonS3 s3Client() {
        AWSCredentials credentials = new BasicAWSCredentials(accessKey, accessSecret);
        return AmazonS3ClientBuilder.standard()
                .withCredentials(new AWSStaticCredentialsProvider(credentials))
                .withRegion(region).build();
    }
}

4) service > S3Uploader.java 생성


@Slf4j
@Service
public class S3Uploader {

    private final AmazonS3 amazonS3;
    private final String bucket;

    public S3Uploader(AmazonS3 amazonS3, @Value("${cloud.aws.s3.bucket}") String bucket) {
        this.amazonS3 = amazonS3;
        this.bucket = bucket;
    }
    public String upload(MultipartFile multipartFile, String dirName) throws IOException {
        // 파일 이름에서 공백을 제거한 새로운 파일 이름 생성
        String originalFileName = multipartFile.getOriginalFilename();

        // UUID를 파일명에 추가
        String uuid = UUID.randomUUID().toString();
        String uniqueFileName = uuid + "_" + originalFileName.replaceAll("\\s", "_");

        String fileName = dirName + "/" + uniqueFileName;
        log.info("fileName: " + fileName);
        File uploadFile = convert(multipartFile);

        String uploadImageUrl = putS3(uploadFile, fileName);
        removeNewFile(uploadFile);
        return uploadImageUrl;
    }

    private File convert(MultipartFile file) throws IOException {
        String originalFileName = file.getOriginalFilename();
        String uuid = UUID.randomUUID().toString();
        String uniqueFileName = uuid + "_" + originalFileName.replaceAll("\\s", "_");

        File convertFile = new File(uniqueFileName);
        if (convertFile.createNewFile()) {
            try (FileOutputStream fos = new FileOutputStream(convertFile)) {
                fos.write(file.getBytes());
            } catch (IOException e) {
                log.error("파일 변환 중 오류 발생: {}", e.getMessage());
                throw e;
            }
            return convertFile;
        }
        throw new IllegalArgumentException(String.format("파일 변환에 실패했습니다. %s", originalFileName));
    }

    private String putS3(File uploadFile, String fileName) {
        amazonS3.putObject(new PutObjectRequest(bucket, fileName, uploadFile)
                .withCannedAcl(CannedAccessControlList.PublicRead));
        return amazonS3.getUrl(bucket, fileName).toString();
    }

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

    public void deleteFile(String fileName) {
        try {
            // URL 디코딩을 통해 원래의 파일 이름을 가져옵니다.
            String decodedFileName = URLDecoder.decode(fileName, "UTF-8");
            log.info("Deleting file from S3: " + decodedFileName);
            amazonS3.deleteObject(bucket, decodedFileName);
        } catch (UnsupportedEncodingException e) {
            log.error("Error while decoding the file name: {}", e.getMessage());
        }
    }

    public String updateFile(MultipartFile newFile, String oldFileName, String dirName) throws IOException {
        // 기존 파일 삭제
        log.info("S3 oldFileName: " + oldFileName);
        deleteFile(oldFileName);
        // 새 파일 업로드
        return upload(newFile, dirName);
    }
}

이렇게 S3의 설정을 마무리 한 후 entity와 서비스 로직은 필요에 맞게 설정하면 된다.




🔽 실행결과





📔 S3 예시파일(Github)

🔎 위의 내용을 참고로 entity와 service, controller가 포함된 파일을 올렸다.
어떤식으로 동작하는지 파악하고 필요에 따라 커스텀하는 용도로 사용하면 좋을것 같다.

https://github.com/jihyeon4956/S3_Example

profile
꼭꼭 씹어서 소화시키는 맛있는 코딩

1개의 댓글

comment-user-thumbnail
2023년 9월 28일

안녕하세요. 정성이 넘치는 글 잘 보았습니다.
특히 과금 방지를 위한 내용이 글 상단에 올라와 있다는 점이
대단히 인상적이었습니다.
이후 S3 사용 시 꼭 참고하도록 하겠습니다.

답글 달기