#8 aws S3 이미지 파일 업로드

seojin's 개발블로그·2023년 8월 11일
0

영화 사이트 제작

목록 보기
8/19

구현 하려는 기능: 영화 포스터 이미지와 프리뷰 이미지 업로드

영화 데이터 create 기능을 구현하려는데 영화에 대한 기본 정보 외에
포스터 이미지와 프리뷰 이미지를 등록하는 기능을 만들려고 한다.

파일 저장소는 경제, 편의의 이유로 aws s3를 선택했다

📌 AWS S3 설정

1. 버킷 생성



우선 이미지 파일들을 담을 aws s3 버킷을 먼저 생성해주었다
설정의 경우 리전을 서울로 변경해주고 팀원과 나의 pc에서 이용할 것이기 때문에
퍼블릭 액세스 차단 설정을 풀어 주었다. 나머지 설정은 기본 설정을 그대로 두었다

2. 버킷 이용자 추가


iam 서비스로 이동하여 액세스 관리의 사용자 탭에서 사용자 추가를 진행한다.

그리고 사용자 이름을 클릭하여 권한 정책을 추가 해주면 된다.

모든 버킷에 대한 액세스 권한을 제공하는 정책 설정이다
현재 프로젝트는 이용자가 나와 팀원분 밖에 없기에 전체 액세스 권한으로 정책을 설정했다.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Statement1",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:*",
            "Resource": "arn:aws:s3:::버킷 이름/*"
        }
    ]
}

그리고 다시 s3로 넘어와서 버킷 정책을 등록해주었다.
참고 블로그
위의 블로그를 참고 했으며 해당 버킷에 대해 모든 사용자에게 모든 작업 권한을 부여 한다는 의미로 생각하면 된다.

📌 S3와 spring 연동

버킷을 생성한 후에는 버킷을 이용할 spring 환경과 연동하는 작업을 진행하였다.
참고 블로그

1. 의존성 추가

implementation group: 'io.awspring.cloud', name: 'spring-cloud-starter-aws', version: '2.4.4'

build.gradle에 연동을 위한 의존성 추가를 진행하였다.
참고한 블로그에서는 아래의 의존성을 사용중이나

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

2.2.6 버전 취약점 해당 버전에서 취약점 발견 이슈가 있어서 최신 aws 연동 라이브러리
기능상 차이가 없는 최신 버전의 다른 라이브러리를 이용하게 되었다

2. yml 설정 추가

spring:
  servlet:
    multipart:
      max-file-size: 20MB
      max-request-size: 20MB

cloud:
  aws:
    s3:
      bucket: ${storage_name}
    stack.auto: false
    region.static: ap-northeast-2
    credentials:
      accessKey: ${storage_access_key}
      secretKey: ${storage_secret_key}

위의 연동 설정을 yml 파일에 적어 주었다
${~~~}는 이전 포스팅의 yml 파일 관리법에서 정한대로
보안이 필요한 값들을 환경변수화 시켜 누군가 악의적으로 자원을 이용해 과금되는것을 예방하기 위한 조치이다.
그리고 혹여나 너무 큰 파일이 업로드되는것을 방지하기 위해 파일의 사이즈 제한 설정을 넣어주었다.

3. config 작성

@Configuration
public class S3Config {
    @Value("${cloud.aws.credentials.accessKey}")
    private String accessKey;
    @Value("${cloud.aws.credentials.secretKey}")
    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();
    }
}

yml의 연동정보를 이용하여 실제로 s3 스토리지와 연결을 시켜주는 클라이언트를 등록하는 클래스
AmazonS3Client를 bean으로 등록하여 다른 클래스에서 이용시에 의존성 주입을 받아 이용할 수 있게된다.

📌 기능 구현

1. 블랙박스 시퀀스 다이어그램

내가 생각한 영화 정보 등록 api의 알고리즘
dto로 랩핑된 데이터를 받아 서비스 레이어에서는 이미지를 업로드 후
해당 이미지의 주소를 담은 movie entity로 데이터를 convert 하여
레포지토리에 저장을 요청하고 레포지토리는 DB에 데이터를 저장한다
라고 알고리즘을 정하고 구현에 들어갔다.

2. RequestDto

@Getter
@Setter
public class MovieUploadRequestDto {

    private String title;
    private String director;
    private String cast;
    private String country;
    private String genre;
    private String runtime;
    private String age;
    private String content;
    private String posterImgPath;
    private String previewImgPath;
    @JsonFormat(shape= JsonFormat.Shape.STRING, pattern="yyyy-MM-dd", timezone="Asia/Seoul")
    private Date openingDate;

    public Movie toEntity(){
        return Movie.builder()
                .title(this.title)
                .director(this.director)
                .cast(this.cast)
                .country(this.country)
                .genre(this.genre)
                .runtime(this.runtime)
                .age(this.age)
                .content(this.content)
                .posterImgPath(this.posterImgPath)
                .previewImgPath(this.previewImgPath)
                .build();
    }

}

요청으로 들어올 dto 회원과 달리 입력값에 대한 제약사항이 없다
관리자가 작성하는 데이터이고 아직 제약사항을 만들 이유를 찾지 못해서 그대로 두었고
서비스 레이어에서 이미지 업로드후 dto에 이미지 주소를 입력 받아야하기 때문에 이번 dto에는 setter를 만들었다.
개봉일은 json으로 string 타입으로 입력될 것이기 때문에 JsonFormat을 이용하였고 레포지토리로 넘기기전 movie 객체로 만들기 위한 toEntity() 메서드를 작성하였다.

3. Controller

@PostMapping("/api/movie/uploadMovieInfo")
public ResponseEntity<ApiResponse<?>> uploadMovieInfo(@RequestPart MovieUploadRequestDto requestDto,
                                                      @RequestPart (value= "posterImage", required = false) MultipartFile posterImg,
                                                      @RequestPart (value= "previewImages", required = false) List<MultipartFile> previewImgs){
        Movie movie = movieService.saveMovieInfo(requestDto, posterImg, previewImgs);
        
        return ResponseEntity.ok(new ApiResponse<>(1, "조회 성공", movie));
}

우선 예외처리 없이 구현하였으며 saveMovieInfo라는 이름의 메서드를 이용하기로 하였고 입력 파라미터로는 dto와 이미지 파일들을 넣어 주었다.

4. Service

포스터 이미지 업로드를 위해 우선 s3client가 이용하는 파일 저장 메서드를 살펴보았다.

@Override
public PutObjectResult putObject(String bucketName, String key, File file)
            throws SdkClientException, AmazonServiceException {
    return putObject(new PutObjectRequest(bucketName, key, file)
           .withMetadata(new ObjectMetadata()));
}

@Override
public PutObjectResult putObject(String bucketName, String key, InputStream input, ObjectMetadata metadata)
            throws SdkClientException, AmazonServiceException {
    return putObject(new PutObjectRequest(bucketName, key, input, metadata));
}

dto를 사용하는 putObject도 있지만 간단한 구현을 위해 위의 두 메서드중에 선택을 하기로 했다.
두 메서드의 차이는 메타데이터의 유무인데 여러 자료를 찾아보니 메타데이터에 데이터의 크기등을 명시할 수 있어 사용자의 편의와 혹시 모를 데이터 정보 제공에 대비해 두번째에 있는 메타데이터를 파라미터로 받는 메서드를 이용하기로 하였다.

@Value("${cloud.aws.s3.bucket}")
private String bucketName;
 
public Movie saveMovieInfo(MovieUploadRequestDto requestDto, MultipartFile posterImg, List<MultipartFile> previewImgs) throws IOException {
    requestDto.setPosterImgPath(uploadToS3(posterImg));
    requestDto.setPreviewImgPath(uploadToS3(previewImgs));
        
    return movieRepository.save(requestDto.toEntity());
}

저장 메서드 시퀀스 다이어그램에 나타낸대로 이미지를 s3 버킷에 업로드한 후
dto를 컨버팅하여 저장요청을 하도록 구현을 하였다.

public String uploadToS3(MultipartFile multipartFile) throws IOException {
    String key = UUID.randomUUID() + "_" + multipartFile.getOriginalFilename();

    ObjectMetadata metadata = new ObjectMetadata();
    metadata.setContentLength(multipartFile.getInputStream().available());

    amazonS3Client.putObject(bucketName, key, multipartFile.getInputStream(), metadata);

    return amazonS3Client.getUrl(bucketName, key).toString();
}

우선 포스터 이미지 저장용 메서드를 구현하였다
키 값에 UUID를 붙여서 중복 저장을 방지하고 메타 데이터의 경우에는
담을수 있는 정보가 매우 다양하지만 지금은 업로드할 파일의 InputStream에서 읽을 수 있는 바이트 수를 계산해서 데이터의 크기만 메타데이터에 기록하기로 하였고 버킷이름, 키값, 파일, 메타데이터를 s3로 전송하여준다.

public List<String> uploadToS3(List<MultipartFile> multipartFile) throws IOException {

    List<String> imgUrlList = new ArrayList<>();

    multipartFile.forEach(file -> {
        String key = UUID.randomUUID() + "_" + file.getOriginalFilename();
        ObjectMetadata metadata = new ObjectMetadata();
        metadata.setContentLength(file.getSize());

        try {
            amazonS3Client.putObject(bucketName, key, file.getInputStream(), metadata);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
            
        imgUrlList.add(key);
    });
    return imgUrlList;
}

두번째는 프리뷰 이미지들을 저장하는 메서드를 구현했다
이미지 업로드를 반복하고 해당 이미지들의 url들을 리스트로 반환하여준다.

5. Entity 수정

여러개의 url을 담아야하는데 엔티티에는 String으로만 선언을 해놓아서 엔티이와 dto를 수정해주었다.

//Entity
@ElementCollection //컬렉션 타입을 다른 테이블에 저장하도록 지정
@CollectionTable(name = "preview_images", joinColumns = @JoinColumn(name = "movie_id")) 
@Column(name = "preview_img_path") // 영화 id값을 참조하도록 설정
private List<String> previewImgPath = new ArrayList<>();

//dto
private List<String> previewImgPath; //원래는 dto도 String 이였음

6. 테스트



결국 성공했지만 그 과정이 조금 험난했다 아래의 발생했던 문제에 적어놨다

📌 발생했던 문제

1. 415 Unsupported Media Type Error

지원되지 않는 형식으로 클라이언트가 요청을 해서 서버가 요청에 대한 승인을 거부한 오류

파라미터 이름도 전부 맞췄고 dto의 json형식도 문제가 없었는데 계속해서 415 에러가 발생했다. 원인은
HTTP: Content-Type 에 대해 알아보자
내가 컨텐츠 타입에 대해 무지했기 때문에 발생한 에러였다
나는 @Requestpart 어노테이션으로 dto와 이미지를 동시에 받았는데
이런 파트에 대해 타입을 명시해 주지 않아서 생긴 에러였다.
postman에서 requestDto content type에 application/json을 넣어주어 해결하였다.

2. 데이터 삽입 에러


이미지는 정상 삽입이 되나 정작 영화 데이터는 데이터베이스에 등록이 되지 않는 문제가 발생했다. 에러 메세지는

ERROR 35393 --- [nio-8080-exec-9] o.h.engine.jdbc.spi.SqlExceptionHelper : (conn=12040) Incorrect string value: '\xE1\x84\x8B\xE1\x85\xA9...' for column movie.preview_images.preview_img_path at row 1

라는 메세지가 출력되었고 프리뷰 이미지 저장 테이블에 문제가 있는듯 했다.

@ElementCollection //컬렉션 타입을 다른 테이블에 저장하도록 지정
@CollectionTable(name = "preview_images", joinColumns = @JoinColumn(name = "movie_id")) 
@Column(name = "preview_img_path") // 영화 id값을 참조하도록 설정
private List<String> previewImgPath = new ArrayList<>();

아,,, @Column을 지우는걸 깜빡했다...
@CollectionTable 에서 movie_id를 참조하는 테이블을 뽑기때문에 movie테이블의 칼럼 이름과 새로운 테이블의 이름이 일치해야 하는데 일치하지 않아 발생한 문제였다. @Column을 지워주자 해결이되었다.

3. 한글 데이터 삽입 에러

위의 문제들을 해결한 후 테스트 데이터를 입력하였을때는 데이터가 정상입력이 되었으나 제목에 오펜하이머라고 한글을 입력하자
다시 에러가 발생하였다.

스프링에서의 에러메세지는 깜빡하고 캡쳐를 못하였다 ㅠㅠ
하지만 title에 문제가 있다는 에러였고 한글이 문제인것 같아
title을 영어로 바꾸고 director를 한글로하자 director에 문제가 있다는 에러 메세지가 출력이 되었다.

인코딩 형식 변경하기
위의 블로그를 참고하여 데이터베이스 인코딩 타입을 수정하였으나

ALTER Database movie default character set utf8;

계속 한글 데이터에서 에러가 발생하여서 rds 설정을 변경하려 여러 자료를 찾았는데
rds 설정 변경
아,,, 기존 테이블을 삭제하지 않아서 발생하는 문제였다
기존 테이블을 drop하고 다시 만드니 에러가 해결이 되었다.

📌 후기

awsS3client 코드들도 훑어보고 여러개의 이미지 url을 하나의 엔티티 변수에 어떻게 담을지 고민하다보니 몇시간이 훌쩍 지나있었다.

코드를 만드는 시간은 한 20퍼센트 정도도 안됐던 날이다
그래도 s3client 메서드 활용들도 공부하고 메타데이터의 필요성도 알게되고
코드도 나름? 깔끔해보이게 만들수 있어서 좋았다

예외처리까지 못끝낸게 조금 아쉽지만 영화 엔티티 파트에서
가장 오래걸릴거라 생각한 이미지 업로드 부분이 해결되어서 나머지 부분은 후다닥
만들수 있을거 같다

공부도 많이하고 나름 뿌듯한 하루였다.

profile
개발 공부하는 블로그

0개의 댓글