메인 프로젝트 (8)메인페이지 - S3 이미지 업로드

InSeok·2022년 12월 10일
0

프로젝트

목록 보기
8/13
post-custom-banner

Post 등록 - S3 이미지 업로드

구현 기능

  1. 게시글 작성 및 파일 업로드 동시 처리
  2. 다중 파일 업로드
  3. DB에는 파일 관련 정보 및 저장 경로만 저장하고 실제 파일은 S3에 저장
  • S3 프리티어 버킷 용량이 한정 되있으므로 최대용량을 설정해준다.
  • S3 계정 정보 입력

application.yml

spring:
  servlet:
    multipart:
      maxFileSize: 10MB // 파일 최대 사이즈
      maxRequestSize: 20MB //요청한 최대사이즈

#aws
cloud:
  aws:
    credentials:
      accessKey: YOUR_ACCESS_KEY
      secretKey: YOUR_SECRET_KEY
    s3:
      bucket: YOUR_BUCKET_NAME
    region:
      static: YOUR_REGION
    stack:
      auto: false

build.gradle

dependencies {
//AwsS3
	implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
}

@Controller

@PostMapping
public ResponseEntity<?> create(
        @AuthMember MemberDetails memberDetails,
        PostCreateVO postCreate) {  //해결 3번 이용

			//S3에 파일업로드 후 저장된경로 리스트 반환
        List<String> imagePathList = awsS3Service.StoreFile(postCreate.getFiles());			   
								...
        return new ResponseEntity<>(new SingleResponse<>(postId), HttpStatus.CREATED);
}

문제

  • @RequestBody는 body로 전달받은 JSON형태의 데이터를 파싱해주지만, Content-Type
    이 multipart/form-data로 전달되어 올 때는 Exception을 발생시킴

해결

1. @RequestPart 이용

  • File과 Dto를 같이 받기 위해서 사용
@PostMapping("")
@ResponseStatus(HttpStatus.CREATED)
public Long create(
     @RequestPart(value="image", required=false) List<MultipartFile> files,
     @RequestPart(value = "requestDto") BoardCreateRequestDto requestDto
) throws Exception {
...
}

2. @RequestParam 이용

  • Dto 없이 파리미터로 전달받음
@PostMapping("")
@ResponseStatus(HttpStatus.CREATED)
public Long create(
     @RequestParam(value="image", required=false) List<MultipartFile> files,
     @RequestParam(value="id") String id,
     @RequestParam(value="title") String title,
     @RequestParam(value="content") String content
) throws Exception {
...
}

3. VO클래스 생성

  • 전달받을 데이터가 적을 경우는 @RequestPart나 @RequestParam을 사용해도 상관없으나, 전달받을 데이터가 많을 경우 코드가 지저분하게 보일 수 있어 게시글 -파일 처리용 VO클래스를 하나 선언하여 처리하였다.
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class PostCreateVO {

    @NotBlank
    private String content;

    private List<TagDto> tagDtos;

    private List<MultipartFile> files;

    @Builder
    public PostCreateVO(String content, List<TagDto> tagDtos, List<MultipartFile> files) {
        this.content = content;
        this.tagDtos = tagDtos;
        this.files = files;
    }
}

S3 파일 업로드

  • List<MultipartFile> 을 전달받아 파일을 S3 버킷에 저장한 후 저장한 파일 경로를 List<String> imagePathList 에 담아 반환 한다.

업로드 흐름

  1. List<MultipartFile>의 파일 유무를 체크 - 없을경우 NoImage 예외를 던짐
  2. getOriginalFilename() 메서드로 파일 이름을 추출한후 createStoreFileName() 메서드로 중복되지 않는 파일이름 생성한다.
  3. S3에 이미지 파일을 업로드한다. - 업로드 실패할 경우 UploadFailed 예외를 던짐
  4. 추후 S3에 저장한 이미지를 불러와 빠르게 응답하기 위해 CloudFront 도메인 + 파일명을 경로로 imagePathList에 담아 리턴한다.

AWS CloudFront 정적, 동적 컨텐츠를 빠르게 응답하기 위한 캐시 기능을 제공하는 CDN 서비스

@Slf4j
@RequiredArgsConstructor
@Service
public class AwsS3Service {

    private final AmazonS3 amazonS3;

    //S3 버킷
    @Value("${cloud.aws.s3.bucket}")
    private String bucketName;

    //CloudFront
    @Value("${cloud.aws.cloudFront.distributionDomain}")
    private String cloudFront;

/**
* S3 bucket에 이미지 파일 저장
*@param files S3 에 등록할 파일 목록
*@return DB의 Picture 테이블 path Column에 저장할 이미지경로 리턴
*/
public List<String> StoreFile(List<MultipartFile> files) {

        //(1) 파일 유무 체크
        isFileExist(files);

        //반환할 이미지 저장경로 리스트
        List<String> imagePathList = new ArrayList<>();

        for (MultipartFile file : files) {
            String originalName = file.getOriginalFilename(); // 파일 이름
            String storeName = createStoreFileName(originalName);
            long size = file.getSize(); // 파일 크기

            ObjectMetadata objectMetadata = new ObjectMetadata();
            objectMetadata.setContentType(file.getContentType());
            objectMetadata.setContentLength(size);

            //(3) S3에 업로드
            try (InputStream inputStream = file.getInputStream()) {
                amazonS3.putObject(new PutObjectRequest(bucketName, storeName, inputStream, objectMetadata)
                        .withCannedAcl(CannedAccessControlList.PublicRead));
            } catch (IOException e) {
                throw new UploadFailed(); // 업로드 실패 예외
            }
            // (4)CloudFront 도메인명 + 저장한 파일명
            String imagePath = cloudFront + "/" + storeName;
            imagePathList.add(imagePath);
        }
        return imagePathList;
    }

		// 파일 유무 체크
    private void isFileExist(List<MultipartFile> files) {
        if (files.isEmpty()) {
            throw new NoImage();
        }
    }

		(2)
    /**
     * 저장되는 파일이름 중복이 되지 않게 하기 위해서
     * UUID로 생성한 랜덤값 + 파일확장명으로 업로드
     */
    private String createStoreFileName(String originalName) {
        String ext = extractExt(originalName);
        String uuid = UUID.randomUUID().toString();
        return uuid + "." + ext;
    }

    // 파일 확장명 추출
    private static String extractExt(String originalName) {
        int pos = originalName.lastIndexOf(".");
        return originalName.substring(pos + 1);
    }
profile
백엔드 개발자
post-custom-banner

0개의 댓글