221030 TIL Spring Boot AWS S3 연동해서 파일 업로드 하기 (S3 Bucket 생성부터 연동까지)

이번 주 스프린트의 핵심 게시글 작성할 때 이미지 업로드하는 기능을 오늘 구현했다.

어제 AWS 가입이 되지 않아 S3를 이용해 이미지를 업로드하는 것을 포기하려 했지만 오늘 일어나서 메일함을 보니 어제 그렇게 기다리던 가입 완료 메일이 새벽 5시에 정상적으로 도착한 것을 확인할 수 있었다.

S3를 이용해서 이미지 업로드할 수 있다는 기쁨을 느꼈다. (이미지 업로드를 위해 수많은 삽질을 할 나의 미래를 모른 채..)

 

게시글에 이미지 업로드를 위한 spring에 AWS S3 연동방법

1. AWS S3 버킷 생성하기

S3 서비스로 이동 후 버킷 만들기로 이동한다.

그다음 버킷 이름(소문자)을 입력한다.

 

 

모든 퍼블릭 액세스 차단 체크를 풀어준 다음 밑에 두 개만 다시 체크해준다.

 

첫 번째 ACL(액세스 제어 목록)을 통해 부여된 버킷 및 객체에 대한 퍼블릭 액세스 차단을 해제하면 객체를(이미지 파일) 업로드할 수 있다.

두 번째 의의 ACL(액세스 제어 목록)을 통해 부여된 버킷 및 객체에 대한 퍼블릭 액세스 차단을 해제하지 않으면 각각 파일 업로드시, 업로드된 파일에 접근할 시에 403 Access Denied를 만나게 되기 때문에 해제를 풀어준다.

 

2. IAM(Identity and Access Management) 생성하기

AWS 환경이 아닌 로컬 환경에서 S3에 접근하기 위해서는 IAM 사용자에게 S3 접근 권한을 주어야 한다.

 

IAM 서비스 -> 사용자 -> 사용자 추가로 이동후 사용자 이름을 입력하고 엑세스키 - 프로그래밍 방식 엑세스을 체크해준다.

 

 

기존 정책 직접 연결을 클릭하고, S3를 검색하여 AmazonS3FullAccess을 선택한다. 다음 태그는 선택사항이라 입력해도 좋고 입력하지 않아도 괜찮다.

 

사용자 생성이 완료되면, Access-key와 Secret-key를 발급해주는데, 생성 직후에만 볼 수 있는 화면이라 csv파일로 다운 받아서 잘 저장해서 보관해야 한다!!!!

 

3. Spring 설정

1. build.gradle 

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

build.gradle에 dependency를 추가해준다.

 

2. application.properties

cloud.aws.region.static=ap-northeast-2
cloud.aws.stack.auto=false
cloud.aws.s3.bucket=본인 bucket명
cloud.aws.credentials.access-key= 발급받은 access키
cloud.aws.credentials.secret-key= 발급받은 secret키

spring.servlet.multipart.max-file-size: 10MB
spring.servlet.multipart.max-request-size: 10MB

파일 사이즈를 제한한 이유는 aws 과금 방지를 위해..

그리고 제일 중요한거!!!! access key와 secret key가 노출되면 안되기 때문에 github에 코드를 올릴때 application.properties는 gitignore에 추가하고 올리자.

 

3. S3설정 등록

@Configuration
public class AwsS3Config {
  @Value("${cloud.aws.credentials.access-key}")
  private String accessKey;
  @Value("${cloud.aws.credentials.secret-key}")
  private String secretKey;
  @Value("${cloud.aws.region.static}")
  private String region;

  @Bean
  public AmazonS3Client amazonS3Client() {
    BasicAWSCredentials awsCredentials = new BasicAWSCredentials(accessKey,secretKey);
    return (AmazonS3Client) AmazonS3ClientBuilder.standard()
        .withRegion(region)
        .withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
        .build();
  }
}

 

4. S3Uploader

@Component
public class S3Uploader {
  private final AmazonS3Client amazonS3Client;

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

  public S3Uploader(AmazonS3Client amazonS3Client) {
    this.amazonS3Client = amazonS3Client;
  }

  public String uploadFiles(
      MultipartFile multipartFile, String dirName) throws IOException {
    File uploadFile = convert(multipartFile).orElseThrow(() ->
            new IllegalArgumentException("error: MultipartFile -> File convert fail"));
    return upload(uploadFile, dirName);
  }

  public String upload(File uploadFile, String filePath) {
    String fileName = filePath + "/" + UUID.randomUUID() + uploadFile.getName();
    String uploadImageUrl = putS3(uploadFile, fileName);
    removeNewFile(uploadFile);
    return uploadImageUrl;
  }

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

  private void removeNewFile(File targetFile) {
    if (targetFile.delete()) {
      System.out.println("File delete success");
      return;
    }
    System.out.println("File delete fail");
  }

  private Optional<File> convert(MultipartFile file) throws IOException {
    File convertFile = new File(System.getProperty("user.dir") + "/" + file.getOriginalFilename());
    if (convertFile.createNewFile()) {
      try (FileOutputStream fileOutputStream = new FileOutputStream(convertFile)) {
        fileOutputStream.write(file.getBytes());
      }
      return Optional.of(convertFile);
    }
    return Optional.empty();
  }
}

동작 순서

  1. 이미지를 업로드 하는 컨트롤러에서 uploadFiles 메서드를 요청받은 MultipartFile을 전달하면서 호출하면 uploadFiles메서드는 S3에 전달할 수 있도록 MultiPartFile을 File로 전환(convert 메서드)하는 작업을 실행한다. (S3에 Multipartfile 타입이 전송이 되지 않아서 해줘야 한다고 한다.)
  2. 변환된 File의 이름을 UUID를 이용해 랜덤하게 변경시켜준다 (파일 이름 중복 방지를 위해) 그리고 putS3메서드를 이용해 S3에 put 한다.
  3. 이후 Multipartfile-> File로 전환되면서 로컬에 파일 생성된것을 삭제한다.
  4. S3에 업로드된 파일의 URL 주소를 받아서 DB에 url만 저장할 수 있다.

5. 이미지 업로드 Controller

@PostMapping("/upload")
public String upload(MultipartFile multipartFile) throws IOException {
  return s3Uploader.uploadFiles(multipartFile, "bucket폴더명");
}
  •  

 

value too long for column 에러..

react에서 axios.post 요청을 이용해 이미지를 업로드 후에 해당 이미지를 게시글 상세페이지에 나오게 하기 위해서 생각한 방법은 S3에 올라간 이미지의 url을 받아와 그 url을 Post DB에 저장하는것이였다.

ImageUrl column을 생성하고 S3Uploader클래스에서 upload메서드에서 리턴받는 S3 이미지 url을 그냥 DB에 넣어주면 끝나는 문제라고 생각했지만 DB에 url을 넣으려고 하니 value too long for column "image_url character varying(255)" 에러 메세지가 발생했다. 내가 저장하려는 image의 url이 DB의 최대 저장 길이를 초과해서 저장 할 수 없는 상황이였던것이였다.

 

그래서 문제를 해결하기 위해 구글링을 해봤는데 너무나도 쉽게 해결할수 있는 문제였다. 길이가 초과하는 column의 길이를 늘리는 방법이 대부분이였고 나도 entity에 적용을 해보았다.

길이를 늘리고 다시 파일을 저장해보는데 똑같이 value too long for column 에러가 발생했고 이 방법이 맞지 않는건가..? 생각하고 구글링을 다시 했는데 아무리 찾아봐도 모두가 위 방법처럼 column의 길이를 늘리는 해결방안을 제시하고 있었다.

근데 길이를 늘려도 계속 value too long for column문제가 발생하고 있었다.

column의 길이가 늘어나지 않은것 같다고 생각이되서 column의 최대 사이즈를 확인해봤는데 역시나 255로 변하지 않은 것을 확인할 수 있었다.

 

확인하고 싶은 table의 데이터 타입과 최대 길이 확인하는 쿼리문 

 

select column_name, data_type, character_maximum_length    
  from information_schema.columns  
 where table_name = 'table명'

 

일단은 문제상황이 확인되었다. column의 길이가 늘어나지 않아 이미지 url이 저장되지 않았고 문제를 해결하기 위해서는 길이를 늘리면 되는데 서버를 다시 시작해도 늘어나지 않았다.

 

그래서 table을 삭제하고 다시 시작해서 길이를 확인해보니 내가 설정한 2048만큼 늘어난것을 확인할 수 있었다.

 

table삭제 쿼리문

drop table table명

 

다시 이미지를 저장해보니 데이터베이스에 url이 잘 저장되어 이미지 업로드 기능을 마무리할 수 있었다.!

 

내가 작성한 글과 이미지가 잘 저장된것을 확인할 수 있다.

게시글 상세 페이지 화면

참조 

https://jojoldu.tistory.com/300

https://velog.io/@yyong3519/S3-%EB%B2%84%ED%82%B7-%EC%84%A4%EC%A0%95