[Spring Boot] S3 연동과 파일 업로드 및 삭제

Benjamin·2022년 11월 14일
0

Spring

목록 보기
2/5

내가 겪은 일에 대한 이야기

Spring Boot를 사용한 프로젝트를 진행하던 중 이미지 저장하는 기능이 필요했고, 이미지를 저장하기위해 이미지 저장서버로 S3를 구축했다.

AWS에서 S3서버 구축하는것은 쉬웠으나, 이를 Spring Boot코드에 연동해서 파일 업로드와 삭제 기능을 구현하는것은 무척 어려웠다..
나는 vscode를 사용해서 작업했는데, 처음에 며칠을 헤맸다.
물론 그 며칠동안 이 작업만 한건 아니지만, 이게 안되니 이 코드를 붙잡고있기가 점점 싫어졌다..
다른 사람들의 블로그를 참고했을때에 모든 사람들이 너무나 쉽게 아무렇지않게 설명하는데, 왜 나는 안되는지 의문이었다.

내가 헤맨부분은 아래와 같다.(gradle 이용)

  • build.gradle파일에 해당 implementation을 추가하고, Service에서 이를 이용하기위해 import com.amazonaws.services.s3~~를 하는데 com.amazonaws가 없다면서 import가 되지않는 에러가 떴다.
    -> vscode는 build.gradle에 추가한 내용으로 적용시켜주기위해 마우스 우클릭 - Reloads Projects을 해주면 된다해서 그렇게했는데, 아무리해도 해결이 안됐다.
    JAVA PROJECTS에서 외부 의존성 추가된 리스트를 확인해봤는데, 이에 관한 항목이 안보여서 구글링을 통해 필요한 리스트를 수집한 후 수동으로 추가했다. 그랬더니 몇가지 import는 됐으나, 일부가 작동하지않아서 이렇게하면 언젠가 문제가 터질거라는 생각이들었다.

그래서 작업한 코드를 다 지운후, 차근차근 다시 로직을 파악하며 접근했다.
그래서 현재는 성공!
성공하면 아래 사진처럼 JAVA PROJECTS의 외부 의존성리스트에 잘 들어가있는것을 볼 수 있다.


프로젝트 설정

  • Mac OS
  • Java 11
  • Spring boot 2.4.2
  • Gradle
  • MySql
  • Visual Studio Code

작성코드

1. gradle.build파일에 아래 코드 추가 : 의존성 추가

// Use ConfigurationProperties
annotationProcessor ('org.springframework.boot:spring-boot-configuration-processor')
//S3
implementation ('org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE')

ConfigurationProperties을 지정하면 application.yml의 값을 가져와서 멤버변수에 자동으로 할당한다. 이 의존성이 추가되어있다면 S3에 관련한 두번째 의존성만 추가하면된다.

-> vscode를 사용한다면 추가후 꼭 Reload Projects를 하자!

2. application-dev.yml

application.yml 파일이 아니다!

cloud:
  aws:
    region:
      static: ap-northeast-2 //s3설정 확인
    s3:
      bucket: //버킷명
      # dir: S3 디렉토리 이름 # ex) /gyunny
    credentials:
      access-key: //키 값
      secret-key: //키 값
    stack:
      auto: false

stack.auto 는 Spring Cloud 실행 시, 서버 구성을 자동화하는 CloudFormation이 자동으로 실행되는데 이를 사용하지 않겠다는 설정입니다.
해당 설정을 안해주면 아래의 에러가 발생합니다.

이렇게 따로 파일을 생성해서 코드를 작성한 이유는 보안을 위해서이다.
기본적으로 사용해야하는 application.yml에는 노출돼도 상관없는 설정내용을 작성한다.
나는 아래와같은 내용만 넣어두었다.

+참고
application.yml

#### Default ###
spring:
  application:
    name: //이름 설정

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

  profiles:
    active: dev //이 설정은 application-dev.yml을 부른다.

노출되면 안되는 키 값이 들어있는 application-dev.yml을 .gitignore에 추가하자.

Github에 올라가게되면 언제든 다른 사람이 가져갈 수 있기 때문에 git에서 관리되지 않는 파일에서 별도로 관리하는 것이다.
Git 관리 항목에서 제외 (.gitignore) 했기 때문에 더이상 이 키가 외부에 공개될것을 걱정하지않아도된다.

3. ImageApplication.java 파일 생성

해당 파일의 구조는 다음과 같다.

사용할 resource폴더내에 이 파일을 생성해준다. (나는 많은 resource중에서 post에서만 사용)

import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;

@SpringBootApplication
public class ImageApplication {

    public static final String APPLICATION_LOCATIONS = "spring.config.location="
            + "classpath:application.yml";

    public static void main(String[] args) {
        new SpringApplicationBuilder(com.example.demo.src.post.ImageApplication.class)
                .properties(APPLICATION_LOCATIONS)
                .run(args);
    }
}

현재 프로젝트에서 application.yml만 사용하면, 해당 파일이 application-dev.yml 을 호출한다.

  • 또 다른 방법으로는 APPLICATION_LOCATIONS에 application-dev.yml을 추가해주는것도 있다.
public static final String APPLICATION_LOCATIONS = "spring.config.location="
            + "classpath:application.yml,"
            + "classpath:application-dev.yml";

4. S3Config.java

해당 파일의 위치는 다음과 같다.

s3 설정코드를 작성한다.

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;

public class S3Config {
    @Configuration
public class AmazonS3Config {

    @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 awsCreds = new BasicAWSCredentials(accessKey, secretKey);
        return (AmazonS3Client) AmazonS3ClientBuilder.standard()
                .withRegion(region)
                .withCredentials(new AWSStaticCredentialsProvider(awsCreds))
                .build();
    }
}
    
}

5. ~Controller.java 에서 사용

import java.io.IOException;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.example.demo.config.BaseException;
import com.example.demo.config.BaseResponse;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@RestController
@RequestMapping("/app/image")
public class postController {

    @Autowired
    private final PostService postService;
    
    public PostController(PostService postService){
        this.postService = postService;
    }

    @PostMapping("/s3-upload/{post-id}")
    // 메서드 작성 
    }
    
}

6. S3Service.java 파일 생성

MultipartFile을 사용하긴하지만, Controller는 각 resource마다 S3의 사용용도가 다양하므로 각 메소드들을 하나의 파일에 묶는것보다 S3를 사용하는 각 리소스에서 개별적으로 ImageApplicationr과 Controller의 코드를 작성해준다.

해당 파일의 위치는 다음과 같다.

import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.example.demo.config.BaseException;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.util.UUID;
import static com.example.demo.config.BaseResponseStatus.*;
import com.amazonaws.AmazonServiceException;

@RequiredArgsConstructor
@Service
public class S3Service {
    final Logger logger = LoggerFactory.getLogger(this.getClass());
    
    // application.yml
    @Value("${cloud.aws.s3.bucket}")    
    private String bucket;

    @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;

    private final AmazonS3 amazonS3;


    /** 파일 업로드 (1개) **/
    public String fileUpload(MultipartFile multipartFile) throws BaseException {
        String fileName = UUID.randomUUID() + "-" + multipartFile.getOriginalFilename();

        ObjectMetadata objMeta = new ObjectMetadata();
        objMeta.setContentLength(multipartFile.getSize());
        objMeta.setContentType(multipartFile.getContentType());

        try {
            // 파일 업로드 후 URL 저장
            amazonS3.putObject(bucket, fileName, multipartFile.getInputStream(), objMeta);
        } catch (Exception e){
            logger.error("S3 ERROR", e);
            // 이미지 업로드 에러
            throw new BaseException(S3_UPLOAD_ERROR);
        }

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

    //파일 삭제
    public void fileDelete(String fileUrl) throws BaseException {
        try{
            String fileKey = fileUrl.substring(58);
            String key = fileKey; // 폴더/파일.확장자
            final AmazonS3 s3 = AmazonS3ClientBuilder.standard().withRegion(region).build();

            try {
                s3.deleteObject(bucket, key);
            } catch (AmazonServiceException e) {
                System.err.println(e.getErrorMessage());
                System.exit(1);
            }

            System.out.println(String.format("[%s] deletion complete", key));

        } catch (Exception exception) {
            throw new BaseException(S3_DELETE_ERROR);
        }
    }
}
  • 파일 업로드
    파일명이 동일한 동일이미지의 중복저장을 가능하게하기위해, 생성되는 파일명은 랜덤값을 추가한 값으로 지정한다.
    파일을 S3 버킷에 업로드 한 후, 생성된 url을 반환한다.(반환된 url을 DB에 저장하기위해)
  • 파일 삭제
    버킷명과 S3의 각 파일에 생성되어있는 key값을 사용해야한다.
    키 값만 가져오는법을 모르겠어서, url에서 가장 앞부분을 구성하는 값은 버킷명에 관한 값으로 동일하기때문에 해당 값들을 제거시키는 방법으로했다. 이 부분은 이후 더 좋은 방법으로 수정이 필요해보인다.

파일 삭제를 구현할 때 이것저것 많이 봤는데, 결국 공식문서대로 썼더니 문제없이 작동했다.
https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/examples-s3-objects.html

7. ~Dao에서 사용

이것 또한 쿼리를 사용하는것이 전부이기 때문에, 기존에 사용하는 resource의 Dao파일에 추가한다.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import javax.sql.DataSource;

@Repository
public class S3Dao {
    private static JdbcTemplate jdbcTemplate;

    @Autowired
    public void setDataSource(DataSource dataSource){
        jdbcTemplate = new JdbcTemplate(dataSource);
    }

//기능 생성
    
}

0개의 댓글