Spring Boot로 AWS S3에 파일 업로드 및 삭제하기

변현섭·2023년 6월 27일
1

지난 포스팅에서는 Spring Boot와 AWS S3를 연동하는 방법에 대해 다루어 보았습니다. 연동을 했다면 S3를 이용해 파일을 업로드할 수 있어야 할 것입니다. 또한 삭제가 필요한 상황이면, API를 호출해서 삭제도 할 수 있어야 합니다. 이번 포스팅에선 그 과정에 대해 알아보려 합니다.

오늘 소개할 코드는 아래의 깃허브 링크를 통해 확인하실 수 있습니다. 제 벨로그의 시리즈 중 하나인 따라하면서 배우는 JPA에서 사용하는 코드입니다.
>> 깃허브 링크

Ⅰ. 기본 설정

1. 버킷 설정

① AWS S3 서비스에 들어간다.

② 좌측 메뉴의 버킷을 클릭한 후 저번에 생성한 버킷을 클릭한다.

③ 권한 탭을 누르고 스크롤을 아래로 내리면 객체 소유권이 보일 것이다. 우측의 편집 버튼을 클릭한다.

④ 아래와 같이 설정을 변경한다.

ACL(Access Control List)을 활성화함으로써 AWS S3 버킷의 객체에 대한 액세스 권한을 제어할 수 있다. S3 버킷은 기본적으로 개인 또는 팀의 액세스만 허용하도록 구성되어 있다. 따라서 ACL을 활성화하면, 적절한 액세스 제어를 위해 권한을 설정해야 한다. 저번 포스팅에서 설정한 것이 이 과정에 해당한다. 이로써, 버킷 설정이 완료된다.

2. Postman 설정

이번 포스팅부터는 postman에 이미지를 넣을 것이므로 관련된 설정을 먼저 진행하려 한다.

① postman의 우상단에 설정 버튼 > Settings > General > Loacation에 들어간다.

② choose 버튼을 이용해 사용할 이미지가 있는 위치를 선택하면 된다. 위 예시에서는 바탕화면을 지정하고 있다.

③ S3에 업로드하려는 image 파일을 바탕화면에 둔 상태에서 API를 실행한다.

이로써 postman 설정까지 완료된다. 이제 코드만 작성하면 S3에 파일을 업로드할 수 있다.

Ⅱ. Board 클래스

이번 포스팅에서는 S3에 이미지를 업로드하기 위해 알아야 하는 최소한의 기능만 설명만 하고, 나머지 설명은 아래의 링크에서 이어서 진행할 예정이니 참고 바란다.
>> 따라하면서 배우는 JPA

게시판이라면, 글뿐 아니라 사진도 게시할 수 있어야 하는만큼 Board 클래스에 사진을 게시할 수 있는 기능이 추가되었다.

1. photoList

게시글과 사진의 매핑관계는 일대다이다. 즉, 하나의 게시글에는 여러 사진이 게시될 수 있다는 의미이다. 일대다관계에서 주인은 누구인가? 앞서 이야기한 바 있듯 일대다관계에서는 다가 주인이다. 여기서는 게시 사진이 주인이 되고, 게시글은 이에 mappedBy된다. 그리고 Board 클래스에서는 PostPhoto를 리스트 형태로 관리해야 한다.

2. addPhotoList

1) PostPhoto


① PostPhoto 클래스는 Board 테이블의 주인이다.

② createBoard()

  • 게시 사진이 매핑되는 게시글을 세팅하는 메서드이다.
  • 사실 setBoard()라고 이름 짓는 것이 더 명확하긴 하나, 객체에 대한 set 함수를 제공하는 것은 최대한 지양해야 하기 때문에 createBoard라는 이름을 썼다.
  • 기능은 setBoard와 동일하므로, 원한다면 setBoard로 바꿔 써도 상관 없다.

2) add

① 게시된 사진들을 List 형태로 관리하기 위해 photoList에 add하고 있다.

② 앞서 설명한 createBoard() 메서드로 게시 사진에 매핑될 게시글을 set하고 있다.

Ⅲ. Board Controller - "board" 요청

1. @RequestPart

① HTTP 요청의 일부를 처리하기 위해 사용되는 어노테이션이다.

② @RequestPart는 @RequestParam과 비슷한 역할을 수행하지만, @RequestParam은 주로 application/x-www-form-urlencoded 형식의 요청 파라미터를 처리하는 데 사용되는 반면, @RequestPart는 multipart/form-data 형식의 요청 데이터를 처리하는 데 사용된다.

③ 파일 업로드와 관련된 작업을 수행하는데 주로 사용된다.

④ value 속성

  • image라는 key 값에 대한 value를 어노테이션이 향하는 변수에 입력하는 역할을 수행한다.

⑤ require 속성

  • 반드시 입력해야 하는 변수의 경우 require 속성 값은 true여야 하고, 굳이 입력하지 않아도 되는 경우라면 require 속성 값은 false여야 한다.
  • require은 default로 true이기 때문에 true인 경우에는 생략할 수 있다.

아래의 사진을 보면 @RequestPart를 이해하는 데에 도움이 될 것이다.

2. MultipartFile

① 파일 업로드를 처리하기 위해 제공되는 인터페이스이다.

② 업로드된 파일의 메타데이터와 파일 데이터를 제공하며, 파일의 원본 이름, 크기, MIME 유형 등의 정보를 얻을 수 있다.

※ 메타데이터
메타데이터는 데이터의 특성, 속성 또는 설명을 제공하는 정보이다. 특히 파일에서의 메타데이터는 파일에 대한 추가적인 정보를 의미한다. 예를 들면 파일의 이름, 크기, 생성 일자, 수정 일자, 파일 형식 등이 포함될 수 있다. 이러한 정보는 파일을 식별 및 분류하고, 파일에 대한 추가적인 작업을 수행하는 데에 유용하다.

※ MIME
Multipurpose Internet Mail Extensions의 약자로, 인터넷에서 다양한 종류의 데이터를 식별하는 데 사용되는 표준화된 방법이다. MIME은 데이터의 형식이나 유형을 나타내기 위해 사용되며, 주로 파일의 확장자에 기반하여 식별한다.
MIME 유형은 데이터의 특성과 형식을 기술하는 문자열로 구성되며, 주 타입과 서브타입으로 구분된다. 예를 들어 "text/plain"은 일반텍스트 데이터를 나타내고, "image/jpeg"는 JPEG 이미지 파일을 나타낸다.

③ MultipartFile은 파일 데이터를 읽고 저장할 수 있는 다양한 메서드를 제공한다.

  • getOriginalFilename(): 업로드된 파일의 원본 이름을 반환한다.
  • getSize(): 업로드된 파일의 크기를 반환한다.
  • getContentType(): 업로드된 파일의 MIME 유형을 반환한다.
  • getBytes(): 업로드된 파일의 데이터를 바이트 배열로 반환한다.
  • getInputStream(): 업로드된 파일의 데이터를 읽기 위한 InputStream 객체를 반환한다.

3. createBoard()

1) builder

① Board 객체의 빌더를 생성한다. 빌더는 객체 생성과 속성 설정을 동시에 처리할 수 있는 방법이다.

② .photoList(new ArrayList<>())

  • Board 객체의 photoList 속성을 빈 ArrayList로 초기화한다.
  • photoList는 Board 객체에 연결된 사진 리스트를 나타내고 있다.
  • commentList도 마찬가지이다.

2) GetS3Res


① @Data

  • @Data 어노테이션이 향하는 클래스의 필드들에 대한 Getter, Setter, toString(), equals(), hashCode() 메서드 등을 Lombok이 자동으로 생성해준다.

② List<GetS3Res>

  • Member가 1장 이상의 사진을 게시글에 게시한 경우, GetS3Res 타입의 리스트에 imgUrl과 fileName을 저장한다.
  • imgUrl과 fileName이 어떤 값인지에 대해서는 아래의 uploadFile()에서 자세히 다루기로 한다.

3) uploadFile()

① for each 람다식

  • 리스트에 대한 for each문을 람다식으로 쓸 때에는 리스트에 닷 오퍼레이터로 for each를 쓰고, 리스트의 각 원소를 받을 변수명을 지정하면 된다. 여기서는 file로 지정했다.

② createFileName()

  • UUID란 Universally Unique IDentifier의 줄임말로, 네트워크 상에서 고유성이 보장되는 id를 만들기 위한 표준 규약이다.

  • UUID.randomUUID().toString()은 랜덤한 고유 식별자(UUID)를 생성하고, toString() 메서드를 통해 문자열 형태로 변환한다.

  • UUID는 일반적으로 32자의 16진수로 표현된다.

  • getFileExtension()

    • 마지막 점 (.) 이후의 부분을 추출하는 작업을 수행한다.
    • 즉, 파일의 확장자를 반환하는 메서드이다("."까지 포함해서 반환한다).
    • 만약 파일명에 "."이 없어 인덱스를 벗어나면 파일 형식이 잘못되었음을 사용자에게 알린다.
    • 위 예시에서는 파일 형식만 확인할 뿐 파일 타입에 무관하게 업로드를 허용하고 있지만, 필요에 따라 파일 타입을 제한하기 위한 용도로 사용할 수도 있다.
  • conacat 메서드로 UUID 문자열과 확장자를 결합한다.

  • 완전히 동일한 파일을 업로드하더라도 fileName은 unique하게 설정된다.

③ objectMetadata

  • 업로드할 파일의 메타데이터를 설정하기 위한 객체이다.
  • setContentLength(file.getSize())로 파일의 크기를 objectMetadata에 설정한다.
  • setContentType(file.getContentType())으로 파일의 컨텐츠 타입을 objectMetadata에 설정한다.

④ file.getInputStream()

  • MultipartFile을 InputStream으로 변환하여 업로드할 파일의 내용을 읽는다.
  • InputStream은 업로드할 파일의 데이터를 읽어오는 용도로 사용된다.

⑤ putObject()

  • s3Client를 사용하여 S3 버킷에 파일을 업로드하는 역할을 수행한다.
  • PutObjectRequest 타입 즉시 반환 객체를 생성하여 업로드할 파일의 버킷 이름, 파일 이름, 파일 내용(inputStream), 메타데이터(objectMetadata)를 설정한다.
  • withCannedAcl(CannedAccessControlList.PublicRead)는 업로드된 파일이 공개 읽기 권한을 가지도록 설정한다.

⑥ IOException

  • 파일 업로드 중에 예외가 발생하면 IOException을 처리하도록 한다.
  • 예외 발생 시, "파일 업로드에 실패했습니다."라는 메시지와 함께 HttpStatus.INTERNAL_SERVER_ERROR를 반환한다.

⑦ fileList.add

  • 버킷에 업로드된 파일의 공개 URL과 파일 이름을 사용하여 GetS3Res 객체를 생성하고, 이를 fileList에 추가한다.
  • getUrl() 메서드의 반환타입은 URL이지만, GetS3Res에선 imgUrl을 String으로 저장하기 때문에 .toString()을 반드시 붙여주어야 한다.
  • 이 fileList가 uploadFile() 메서드의 반환 값이다.

4) saveAllPostPhotoByBoard()


① for each문

  • getS3Res가 리스트를 순회하면서 리스트의 각 원소의 url과 파일명을 갖는 newPostPhoto 객체를 생성하고 있다.
  • 생성된 객체는 PostPhoto 타입 리스트에 저장된다.
  • 사진이 게시된 게시글에서 게시 사진에 대한 리스트를 관리한다.

② savePostPhoto()

  • JPA에서 제공하는 메서드로 리스트의 각 원소를 모두 Repository에 저장한다.

Ⅳ. Board Controller - "delete" 요청

1. getMemberIdx()

Http 요청 헤더에서 access token을 가져와 parsing한 후 멤버의 ID를 추출하여 반환하는 메서드이다. 게시글 삭제 요청에 멤버의 ID 값이 필요한 이유는 아래에서 자세히 설명하겠다.

2. deleteBoard()

1) writer와 visitor

writer는 삭제하려는 게시글의 작성자를 의미하고, visitor는 게시글을 방문한 사람(삭제 요청을 보낸 사람)을 의미한다. 만약, 게시글을 작성하지 않은 사람이 게시글에 대한 삭제 요청을 보냈다면, 이 요청은 처리되지 않아야 한다.

따라서 access token에서 파싱한 멤버(visitor)의 ID와 게시글 작성자(writer)의 ID를 비교하여 같은 경우에 대해서만 요청을 처리한다. 이것이 1번에서 member의 ID 값을 추출한 이유이다. 이로써 게시글 작성자가 삭제를 요청한 경우에만 게시글 삭제가 진행될 수 있다.

2) findAllByBoardId()



게시글의 ID를 입력하면 해당하는 게시글에 게시된 사진을 모두 select하여 반환하는 메서드이다. 반환된 사진은 allByBoard라는 리스트에 추가된다.

3) deleteAllPostPhotos


게시 사진을 받아 for each 문으로 리스트를 순회하며 파일의 이름으로 삭제를 진행한다.

deleteObject() 메서드는 DeleteObjectRequest 타입의 객체를 입력 파라미터로 받는데, DeleteObjectRequest는 버킷의 이름과 key를 포함하고 있다. 여기서 key는 파일의 이름으로 고유 식별자이기 때문에, 다른 게시글에 존재하는 동일한 사진은 지워지지 않는다. 이로써 삭제하려는 게시글에 게시된 사진이 모두 S3에서 제거된다.

4) findAllId()


findAllByBoardId와 입력 파라미터도 갖고 where 조건도 같지만, id를 반환한다는 점에서 차이가 있다. 즉, 삭제하려는 게시글에 게시된 사진들의 ID를 select하여 반환하는 메서드이다. 반환된 ID는 ids라는 리스트에 저장된다.

5) deleteAllPostPhotoByBoard()



게시 사진의 ID를 이용해 Repository에서 삭제하는 메서드이다. 이로써 PostPhotoRepository에서 사진을 삭제할 수 있다. 물론 아래의 JPQL 쿼리를 이용하여 한번에 PostPhoto를 삭제하는 것도 가능하다.

6) deleteBoard()


마지막으로 Board Repository에서 게시글을 삭제하면 모든 삭제 과정이 완료된다. 정리하면, 하나의 게시글을 삭제하기 위해선 3번의 삭제 과정이 필요하다.

  • S3에서 삭제
  • PostPhoto Repository에서 삭제
  • Board Repository에서 삭제

이번 포스팅을 통해 S3에 파일을 업로드하고, 삭제하는 방법을 알아보았다. 구현한 API에 대한 테스트 방법과 결과에 대해서는 따라하면서 배우는 JPA 시리즈를 참고하기 바란다.

profile
Java Spring, Android Kotlin, Node.js, ML/DL 개발을 공부하는 인하대학교 정보통신공학과 학생입니다.

0개의 댓글