-
[Spring Boot] Amazon S3로 파일 업로드 및 삭제Spring Boot/기타 2023. 1. 27. 17:34반응형
Amazon Simple Storage Service(Amazon S3) S3란?
확장성, 데이터 가용성, 보안 및 성능을 제공하는 객체 스토리지 서비스
참조 - https://docs.aws.amazon.com/ko_kr/AmazonS3/latest/userguide/Welcome.html
Amazon S3 설정1. 의존성 추가
Amazon S3를 사용하기 위해 spring-cloud-starter-aws 의존성을 추가하세요.
build.gradle
plugins { id 'java' id 'org.springframework.boot' version '2.7.7' id 'io.spring.dependency-management' version '1.0.15.RELEASE' } group = 'com.example' version = '0.0.1-SNAPSHOT' sourceCompatibility = '11' configurations { compileOnly { extendsFrom annotationProcessor } } repositories { mavenCentral() } dependencies { // Spring Boot implementation 'org.springframework.boot:spring-boot-starter-web' testImplementation 'org.springframework.boot:spring-boot-starter-test' // Spring Cloud Starter AWS implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' // Lombok compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' testCompileOnly 'org.projectlombok:lombok' testAnnotationProcessor 'org.projectlombok:lombok' } tasks.named('test') { useJUnitPlatform() }
2. 환경변수 설정Amazon S3에 접근하기 위해 필요한 bucket, accesskey, secretKey 정보를 설정하세요.
application.yml
# SPRING 설정 spring: profiles: active: local group: local: profiles-local dev: profiles-dev --- # AWS 설정 cloud: aws: region: static: ap-northeast-2 # AWS 기본 리전을 설정 stack: auto: false # Spring Cloud를 실행하면, 서버 구성을 자동화하는 CloudFormation이 실행되는데 이를 사용하지 않으므로 false로 설정 logging: level: com: amazonaws: util: EC2MetadataUtils: error # 해당 클래스에서 예외가 발생하면 로그를 출력하지 않음 --- # Multipart 설정 spring: servlet: multipart: max-file-size: 100MB # 요청한 파일 한 개의 크기 max-request-size: 100MB # 요청한 파일 전체의 크기 --- # LOCAL 설정 spring.config.activate.on-profile: "profiles-local" spring: s3: bucket: access-key: secret-key: --- # DEV 설정 spring.config.activate.on-profile: "profiles-dev" spring: s3: bucket: access-key: secret-key:
3. Configuration 설정Amazon S3로 접근하기 위해 필요한 AmazonS3Client 클래스를 사용하기 위해 AmazonS3ClientBuilder 클래스로 객체를 생성한 후 빈으로 등록하세요.
AmazonS3Config.java
package com.example.amazon.s3.config; import com.amazonaws.auth.AWSStaticCredentialsProvider; import com.amazonaws.auth.BasicAWSCredentials; import com.amazonaws.services.s3.AmazonS3Client; import com.amazonaws.services.s3.AmazonS3ClientBuilder; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class AmazonS3Config { @Value("${spring.s3.access-key}") private String accessKey; @Value("${spring.s3.secret-key}") private String secretKey; @Value("${cloud.aws.region.static}") private String region; @Bean public AmazonS3Client amazonS3Client() { BasicAWSCredentials basicAWSCredentials = new BasicAWSCredentials(accessKey, secretKey); return (AmazonS3Client) AmazonS3ClientBuilder .standard() .withCredentials(new AWSStaticCredentialsProvider(basicAWSCredentials)) .withRegion(region) .build(); } }
4. 파일 업로드 및 삭제 구현4_1. 원본 파일명, 업로드된 파일명, 업로드된 파일 경로, 업로드된 파일의 URL를 응답하기 위해 Dto를 추가하세요.
S3FileDto.java
package com.example.amazon.s3.dto; import lombok.Builder; import lombok.Getter; import lombok.Setter; import lombok.ToString; @Getter @Setter @ToString @Builder public class S3FileDto { private String originalFileName; private String uploadFileName; private String uploadFilePath; private String uploadFileUrl; }
4_2. 파일 업로드는 AmazonS3Client의 putObject 메서드, 파일 삭제는 AmazonS3Client의 deleteObject 메서드를 사용하세요.
Amazon3SService.java
package com.example.amazon.s3.service; import com.amazonaws.services.s3.AmazonS3Client; import com.amazonaws.services.s3.model.ObjectMetadata; import com.amazonaws.services.s3.model.PutObjectRequest; import com.example.amazon.s3.dto.S3FileDto; import java.io.IOException; import java.io.InputStream; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.UUID; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; @Slf4j @RequiredArgsConstructor @Service public class Amazon3SService { @Value("${spring.s3.bucket}") private String bucketName; private final AmazonS3Client amazonS3Client; /** * S3로 파일 업로드 */ public List<S3FileDto> uploadFiles(String fileType, List<MultipartFile> multipartFiles) { List<S3FileDto> s3files = new ArrayList<>(); String uploadFilePath = fileType + "/" + getFolderName(); for (MultipartFile multipartFile : multipartFiles) { String originalFileName = multipartFile.getOriginalFilename(); String uploadFileName = getUuidFileName(originalFileName); String uploadFileUrl = ""; ObjectMetadata objectMetadata = new ObjectMetadata(); objectMetadata.setContentLength(multipartFile.getSize()); objectMetadata.setContentType(multipartFile.getContentType()); try (InputStream inputStream = multipartFile.getInputStream()) { String keyName = uploadFilePath + "/" + uploadFileName; // ex) 구분/년/월/일/파일.확장자 // S3에 폴더 및 파일 업로드 amazonS3Client.putObject( new PutObjectRequest(bucketName, keyName, inputStream, objectMetadata)); // TODO : 외부에 공개하는 파일인 경우 Public Read 권한을 추가, ACL 확인 /*amazonS3Client.putObject( new PutObjectRequest(bucket, s3Key, inputStream, objectMetadata) .withCannedAcl(CannedAccessControlList.PublicRead));*/ // S3에 업로드한 폴더 및 파일 URL uploadFileUrl = amazonS3Client.getUrl(bucketName, keyName).toString(); } catch (IOException e) { e.printStackTrace(); log.error("Filed upload failed", e); } s3files.add( S3FileDto.builder() .originalFileName(originalFileName) .uploadFileName(uploadFileName) .uploadFilePath(uploadFilePath) .uploadFileUrl(uploadFileUrl) .build()); } return s3files; } /** * S3에 업로드된 파일 삭제 */ public String deleteFile(String uploadFilePath, String uuidFileName) { String result = "success"; try { String keyName = uploadFilePath + "/" + uuidFileName; // ex) 구분/년/월/일/파일.확장자 boolean isObjectExist = amazonS3Client.doesObjectExist(bucketName, keyName); if (isObjectExist) { amazonS3Client.deleteObject(bucketName, keyName); } else { result = "file not found"; } } catch (Exception e) { log.debug("Delete File failed", e); } return result; } /** * UUID 파일명 반환 */ public String getUuidFileName(String fileName) { String ext = fileName.substring(fileName.indexOf(".") + 1); return UUID.randomUUID().toString() + "." + ext; } /** * 년/월/일 폴더명 반환 */ private String getFolderName() { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); Date date = new Date(); String str = sdf.format(date); return str.replace("-", "/"); } }
4_3. multipart/form-data의 Content-Type을 요청을 처리하기 위해 @RequestPart 어노테이션을 사용하세요.
AmazonS3Controller.java
package com.example.amazon.s3.controller; import com.example.amazon.s3.service.Amazon3SService; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.multipart.MultipartFile; @RequiredArgsConstructor @Controller public class AmazonS3Controller { private final Amazon3SService amazon3SService; @PostMapping("/uploads") public ResponseEntity<Object> uploadFiles( @RequestParam(value = "fileType") String fileType, @RequestPart(value = "files") List<MultipartFile> multipartFiles) { return ResponseEntity .status(HttpStatus.OK) .body(amazon3SService.uploadFiles(fileType, multipartFiles)); } @DeleteMapping("/delete") public ResponseEntity<Object> deleteFile( @RequestParam(value = "uploadFilePath") String uploadFilePath, @RequestParam(value = "uuidFileName") String uuidFileName) { return ResponseEntity .status(HttpStatus.OK) .body(amazon3SService.deleteFile(uploadFilePath, uuidFileName)); } }
5. 파일 업로드 및 삭제 확인
Mock 파일 객체를 생성하기 위해 MockMultipartFile 클래스를 사용
public MockMultipartFile( String name, @Nullable String originalFilename, @Nullable String contentType, InputStream contentStream) throws IOException { this(name, originalFilename, contentType, FileCopyUtils.copyToByteArray(contentStream)); }
- name : form-data의 key 값
- originalFilename : form-data의 value 값
- contentType : 파일의 형식
- contentStream : 바이트 단위의 입력 스트림, 파일을 바이트 단위로 입력할 수 있도록 FileInputStream 클래스를 사용
multipart 타입으로 요청하기 위해 MockMvcRequestBuilders 클래스의 multipart() 메서드를 사용
public static MockMultipartHttpServletRequestBuilder multipart(String urlTemplate, Object... uriVariables) { return new MockMultipartHttpServletRequestBuilder(urlTemplate, uriVariables); }
AmazonS3ControllerTest.javapackage com.example.amazon.s3.controller; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import java.io.FileInputStream; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.mock.web.MockMultipartFile; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; @Slf4j @AutoConfigureMockMvc @SpringBootTest @ActiveProfiles("local") class AmazonS3ControllerTest { @Autowired MockMvc mockMvc; @DisplayName("uploadFiles_S3로 파일 업로드") @Test public void testUploadFiles() throws Exception { // Given String name = "files"; String contentType = "text/plain"; String path = "src/main/resources/static/temp"; String fileType = "temp"; String originalFileName01 = "temp01.txt"; String originalFileName02 = "temp02.txt"; String originalFileName03 = "temp03.txt"; MockMultipartFile multipartFile01 = new MockMultipartFile( name, originalFileName01, contentType, new FileInputStream(path + "/" + originalFileName01)); MockMultipartFile multipartFile02 = new MockMultipartFile( name, originalFileName02, contentType, new FileInputStream(path + "/" + originalFileName02)); MockMultipartFile multipartFile03 = new MockMultipartFile( name, originalFileName03, contentType, new FileInputStream(path + "/" + originalFileName03)); // When ResultActions resultActions = mockMvc.perform( multipart("/uploads") .file(multipartFile01) .file(multipartFile02) .file(multipartFile03) .param("fileType", fileType) ); // Then resultActions .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$[0].originalFileName").value(originalFileName01)) .andExpect(jsonPath("$[0].uploadFileName").isNotEmpty()) .andExpect(jsonPath("$[0].uploadFilePath").isNotEmpty()) .andExpect(jsonPath("$[0].uploadFileUrl").isNotEmpty()) .andExpect(jsonPath("$[1].originalFileName").value(originalFileName02)) .andExpect(jsonPath("$[1].uploadFileName").isNotEmpty()) .andExpect(jsonPath("$[1].uploadFilePath").isNotEmpty()) .andExpect(jsonPath("$[1].uploadFileUrl").isNotEmpty()) .andExpect(jsonPath("$[2].originalFileName").value(originalFileName03)) .andExpect(jsonPath("$[2].uploadFileName").isNotEmpty()) .andExpect(jsonPath("$[2].uploadFilePath").isNotEmpty()) .andExpect(jsonPath("$[2].uploadFileUrl").isNotEmpty()) .andDo(print()); } }
Postman에서 multipart/form-data 요청 시 설정
소스 코드는 Github Repository - https://github.com/tychejin1218/blog.git 에서 amazon-s3 프로젝트를 참조하세요.
반응형'Spring Boot > 기타' 카테고리의 다른 글
[Spring Boot] ActiveMQ 연동하기 (0) 2023.06.11 [Spring Boot] RabbitMQ 연동하기 (0) 2023.06.04 [Spring boot] Database가 Replication일 때 DataSource 설정 (2) 2023.01.17 [Spring Boot] Spring Data JPA + QueryDSL 설정 (2) 2023.01.15 [Spring Boot] 유효성 검사 처리 (Custom Validation) (0) 2022.06.06