Spring Boot를 사용한 프로젝트를 진행하던 중 이미지 저장하는 기능이 필요했고, 이미지를 저장하기위해 이미지 저장서버로 S3를 구축했다.
AWS에서 S3서버 구축하는것은 쉬웠으나, 이를 Spring Boot코드에 연동해서 파일 업로드와 삭제 기능을 구현하는것은 무척 어려웠다..
나는 vscode를 사용해서 작업했는데, 처음에 며칠을 헤맸다.
물론 그 며칠동안 이 작업만 한건 아니지만, 이게 안되니 이 코드를 붙잡고있기가 점점 싫어졌다..
다른 사람들의 블로그를 참고했을때에 모든 사람들이 너무나 쉽게 아무렇지않게 설명하는데, 왜 나는 안되는지 의문이었다.
내가 헤맨부분은 아래와 같다.(gradle 이용)
import com.amazonaws.services.s3~~
를 하는데 com.amazonaws가 없다면서 import가 되지않는 에러가 떴다.마우스 우클릭 - Reloads Projects
을 해주면 된다해서 그렇게했는데, 아무리해도 해결이 안됐다.그래서 작업한 코드를 다 지운후, 차근차근 다시 로직을 파악하며 접근했다.
그래서 현재는 성공!
성공하면 아래 사진처럼 JAVA PROJECTS의 외부 의존성리스트에 잘 들어가있는것을 볼 수 있다.
// 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를 하자!
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) 했기 때문에 더이상 이 키가 외부에 공개될것을 걱정하지않아도된다.
해당 파일의 구조는 다음과 같다.
사용할 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 을 호출한다.
public static final String APPLICATION_LOCATIONS = "spring.config.location="
+ "classpath:application.yml,"
+ "classpath:application-dev.yml";
해당 파일의 위치는 다음과 같다.
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();
}
}
}
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}")
// 메서드 작성
}
}
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);
}
}
}
파일 삭제를 구현할 때 이것저것 많이 봤는데, 결국 공식문서대로 썼더니 문제없이 작동했다.
https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/examples-s3-objects.html
이것 또한 쿼리를 사용하는것이 전부이기 때문에, 기존에 사용하는 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);
}
//기능 생성
}