Spring Boot -> AWS S3 이미지 업로드 및 삭제 해보기

J_log·2024년 12월 23일
0

오늘은 Spring Boot에서 AWS S3에 이미지를 업로드하고 삭제하는 방법을 정리해보려고 한다. 먼저 AWS에서 해줘야 할 것들이 있다.

AWS S3 버킷 생성

버킷 만들기를 클릭해준다.

버킷 이름을 지어주고 객체 소유권은 ACL 활성화됨으로 설정해주어야 Spring Boot 어플리케이션과 통신이 가능하다.

퍼블릭 액세스 차단 설정도 디폴트로 '차단' 되어 있는데 이 부분도 체크 해제해준다.
그 아래 설정들은 디폴트로 하고 '버킷 만들기' 버튼을 눌러 버킷을 생성해준다.

버킷 권한 설정

생성된 버킷으로 들어가면 권한 탭을 볼 수 있다.

아랫쪽으로 조금 내리다 보면 버킷 정책이 보인다. 편집 버튼을 누르고

우측 상단에 보이는 '정책 생성기'로 정책을 생성하고 나오는 JSON을 붙여넣어도 되고, S3 버킷 정책은 구글링해도 바로 나올 만큼 검색 결과가 많아서, 찾아서 복사해 와도 괜찮지만 일단은 '정책 생성기'로 진행해봤다.

위 쪽에 정책 타입을 'S3 Bucket Policy'로 선택해준다.

Principal에는 모두 허용한다는 의미로 '*'를 입력해주고 Actions에서 GetObject 옵션을 추가해 주었다. 다른 권한이 필요하다면 찾아보고 추가해주면 된다.

그리고 아래에 ARN에는 만든 버킷의 ARN을 넣어주면 된다.

여기까지 하고 Step3의 'Generate Policy'를 누르게 되면 JSON을 받을 수 있다.

이 부분을 복사하여 좀 전의 버킷 정책에 붙여넣는다. 이렇게 하면 AWS S3의 설정은 끝이났다. 아직 스프링 부트 프로젝트에서 S3를 연동하고 코드를 작성해줘야 할 부분이 남아있다.


Spring Boot 설정

build.gradle에 AWS 관련된 의존성을 추가해줘야 한다.

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

다음은 application.yml or application.properties 파일 설정이다.

loud:
  aws:
    credentials: # IAM으로 생성한 시크릿키 정보를 입력한다.
      access-key: 
      secret-key: 
    S3:
      bucket: # bucket 이름을 설정한다.
    region:
      static: ap-northeast-2 # bucket이 위치한 AWS 리전을 설정한다.
    stack:
      auto: false # 자동 스택 생성 기능 사용여부를 설정한다. (자동 스택 생성: 애플리케이션이 실행, 배포될 때 인프라 리소스를 자동으로 생성하고 설정하는 것)

여기서 access-key와 secret-key는 절대로 외부에 공개되면 안된다. 환경 변수로 설정해서 가져오거나 .gitignore로 해당 파일을 제외하고 깃에 올려야 한다.

만약 EC2를 사용하고 있다면
AWS에서 권장하는 방법은 환경 변수를 직접 설정하지 않고 IAM Role을 사용하는 것이다.

  • EC2 인스턴스에 필요한 최소 권한(AmazonS3FullAccess 등)을 가진 IAM Role을 생성
  • EC2에 IAM Role 연결
  • SpringBoot 설정

AWS SDK는 EC2에서 실행 중인 경우 자동으로 IAM Role의 자격 증명을 사용한다.

@Bean
public S3Client s3Client() {
    return S3Client.builder()
            .region(Region.of("your-region")) // 리전을 설정하세요
            .build(); // IAM Role 자동 사용
}

EC2가 아닌 로컬환경이라면 AWS CLI를 설치한 후 자격 증명 파일을 설정해주면 된다.

  • AWS CLI 설치
  • AWS 자격 증명 파일 생성

    aws configure

프롬프트에 따라 다음 정보를 입력한다.

  • AWS Access Key ID:엑세스키
  • AWS Secret Access Key:시크릿키
  • Default region name:지역

자격 증명 정보는 ~/.aws/credentials 파일에 저장된다.
Spring Boot는 Default Credentials Provider Chain을 사용하므로 환경에 따라 자동으로 자격 증명을 선택한다.

  • EC2 환경 : EC2 Instance Profile을 자동으로 사용.
  • 로컬 환경 : 환경 변수 또는 ~/.aws/credentials 파일을 자동으로 사용.

이렇게 설정해주면 EC2, 로컬 모두 코드는 위의 코드와 동일하게 사용할 수 있다.

참고 : https://velog.io/@chrkb1569/AWS-S3-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0-IAM-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%84%A4%EC%A0%95

코드 작성

설정이 끝났다면 S3에 이미지를 업로드하고 삭제할 수 있도록 코드를 작성해주면 된다.
먼저 @Configuration을 통해 AmazonS3를 빈으로 등록해줘야 한다.

package com.jhpark.awss3test.config;

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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AwsS3Config {

    @Bean
    public AmazonS3 amazonS3() {
        BasicAWSCredentials awsCreds = new BasicAWSCredentials("my-accessKey", "my-secretKey"); // 엑세스키, 시크릿키
        return AmazonS3ClientBuilder.standard()
                .withRegion("ap-northeast-2")
                .withCredentials(new AWSStaticCredentialsProvider(awsCreds))
                .build();
    }
}
  • Controller
@RestController
@RequestMapping("/api/s3")
@RequiredArgsConstructor
public class S3Controller {

    private final S3ImageService s3ImageService;

    @PostMapping("/upload")
    public ResponseEntity<String> uploadImage(@RequestPart(value = "image") MultipartFile image) {
        String imageUrl = s3ImageService.uploadImage(image);
        return ResponseEntity.ok(imageUrl);
    }

    @DeleteMapping("/delete")
    public ResponseEntity<Void> deleteImage(@RequestParam String imageUrl) {
        s3ImageService.deleteImage(imageUrl);
        return ResponseEntity.ok().build();
    }
}
  • Service
@Service
@RequiredArgsConstructor
public class S3ImageService {

    private final AmazonS3 amazonS3;
    private final String bucketName = "bucketName"; //버킷 이름

    public String uploadImage(MultipartFile image) {
        if (image.isEmpty()) {
            throw new IllegalArgumentException("File is empty");
        }

        String originalFilename = image.getOriginalFilename();
        String extension = originalFilename.substring(originalFilename.lastIndexOf(".") + 1);
        String fileName = "image/" + UUID.randomUUID().toString() + "." + extension;

        try {
            byte[] bytes = toByteArray(image.getInputStream());
            ObjectMetadata metadata = new ObjectMetadata();
            metadata.setContentType(image.getContentType());
            metadata.setContentLength(bytes.length);

            amazonS3.putObject(new PutObjectRequest(bucketName, fileName, new ByteArrayInputStream(bytes), metadata)
                    .withCannedAcl(CannedAccessControlList.PublicRead));
        } catch (IOException e) {
            throw new RuntimeException("Failed to upload image", e);
        }

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

    public void deleteImage(String imageUrl) {
        String key = imageUrl.substring(imageUrl.indexOf("image/"));
        amazonS3.deleteObject(new DeleteObjectRequest(bucketName, key));
    }

    private byte[] toByteArray(InputStream input) throws IOException {
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        byte[] data = new byte[1024];
        int nRead;
        while ((nRead = input.read(data, 0, data.length)) != -1) {
            buffer.write(data, 0, nRead);
        }
        return buffer.toByteArray();
    }
}

0개의 댓글