Springboot S3 업로드를 구현하는 시리즈입니다.

전체 소스 코드는 여기에서 확인할 수 있습니다.



이전글에서 S3에 이미지를 업로드하는 방법에 대해 알아보았습니다.

이 글에서 아래의 내용들을 다뤄보도록 하겠습니다.

  • S3의 CDN으로 AWS CloudFront 서비스를 이용한 상태에서 이미지를 불러오는 방법
  • 이미지 수정과 삭제 구현





1. CloudFront

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

캐싱을 지원하기 때문에 S3에 저장된 컨텐츠를 직접 접근하지 않아도 됩니다.

따라서 S3의 비용이 감소하며, 더 빠른 응답을 지원하므로 꼭 함께 적용해주는 것이 좋습니다.


연동 방법에 대해서는 이글을 참고하시길 바랍니다.





2. 퍼블리싱

src/main/resources/templates/gallery.html

<body>
<h1>파일 업로드</h1> <hr>

<form th:action="@{/gallery}" method="post" enctype="multipart/form-data">
제목 : <input type="text" name="title"> <br>
파일 : <input type="file" name="file"> <br>
<button>등록하기</button>
</form>

<hr>

<div th:each="gallery : ${galleryList}">
<div style="margin-bottom: 30px">
<p th:inline="text">제목 : [[${gallery.title}]]</p>
<img th:src="${gallery.imgFullPath}" style="width: 500px; height: 300px;">
</div>

<form th:action="@{/gallery}" method="post" enctype="multipart/form-data">
<input type="hidden" name="id" th:value="${gallery.id}">
<input type="hidden" name="title" th:value="${gallery.title}">
<input type="hidden" name="filePath" th:value="${gallery.filePath}">
파일 : <input type="file" name="file"> <br>
<button>수정하기</button>
</form>
<hr>
</div>

</body>
  • 기존에 존재하던 gallery.html 파일에 갤러리 정보를 노출하는 영역과 수정 영역을 추가했습니다.
  • hidden 값인 id, title, filePath는 이미지 수정시 사용하는 값이므로 같이 넘겨줍니다.





3. 이미지 조회

1] 이미지 조회 - Controller

src/main/java/com/victolee/s3exam/controller/GalleryController.java

@GetMapping("/gallery")
public String dispWrite(Model model) {
List<GalleryDto> galleryDtoList = galleryService.getList();

model.addAttribute("galleryList", galleryDtoList);

return "/gallery";
}
  • 기존에 존재하던 dispWrite() 메서드를 수정하여, 갤러리 목록을 가져옵니다. 



2] 이미지 조회 - S3Service

S3에 이미지를 조회하는 방법으로 AmazonS3Client.getObject() 메서드가 있지만, 이는 S3에 직접 요청하여 객체를 가져옵니다.

여기서는 S3에 직접 접근하는 것이 아닌, CloudFront을 통해 캐싱된 이미지를 가져올 것이므로 해당 메서드를 사용하지 않습니다.


src/main/java/com/victolee/s3exam/service/S3Service.java

public static final String CLOUD_FRONT_DOMAIN_NAME = "dq582wpwqowa9.cloudfront.net";

...

public String upload(MultipartFile file) throws IOException {
String fileName = file.getOriginalFilename();

s3Client.putObject(new PutObjectRequest(bucket, fileName, file.getInputStream(), null)
.withCannedAcl(CannedAccessControlList.PublicRead));

return fileName;
}
  • 기존에 존재하던 upload() 메서드를 수정합니다.
    • s3Client.getUrl()을 호출하여 업로드한 S3 객체의 URL을 반환했던 것을 fileName 변수만 반환합니다.
    • return 값은 galleryService에서 파일경로를 DB에 저장하기 위한 값으로 사용되는데, 사실은 fileName 변수가 S3 객체를 식별하는 key 값이고 이를 DB에 저장하는 것입니다.
  • CLOUD_FRONT_DOMAIN_NAME
    • 이 상수는 CloudFront 도메인명입니다. ( 배포 생성시 할당된 기본 값을 사용 )
      • 상수 정보를 따로 관리하는 것이 좋을텐데, 예제에서는 간단하게 멤버 변수로 정의했습니다.
    • 따라서 이미지를 조회할 때, S3 URL(s3Client.getUrl())이 아닌 CloudFront URL(CLOUD_FRONT_DOMAIN_NAME)을 사용하게 됩니다.
      • ex) S3 키 값(fileName 변수)이 sample.jpg라 할 때, 이미지는 "dq582wpwqowa9.cloudfront.net/sample.jpg" 에서 가져오게 됩니다.





3] 이미지 조회 - GalleryService

src/main/java/com/victolee/s3exam/service/GalleryService.java

public List<GalleryDto> getList() {
List<GalleryEntity> galleryEntityList = galleryRepository.findAll();
List<GalleryDto> galleryDtoList = new ArrayList<>();

for (GalleryEntity galleryEntity : galleryEntityList) {
galleryDtoList.add(convertEntityToDto(galleryEntity));
}

return galleryDtoList;
}

private GalleryDto convertEntityToDto(GalleryEntity galleryEntity) {
return GalleryDto.builder()
.id(galleryEntity.getId())
.title(galleryEntity.getTitle())
.filePath(galleryEntity.getFilePath())
.imgFullPath("https://" + s3Service.CLOUD_FRONT_DOMAIN_NAME + "/" + galleryEntity.getFilePath())
.build();
}
  • convertEntityToDto() 메서드는 Controller <--> Service 통신 간에 dto 객체를 사용하기 위해, repository로 부터 얻은 entity 객체를 dto로 변환하는 메서드입니다. 
    • .imgFullPath("https://" + s3Service.CLOUD_FRONT_DOMAIN_NAME + "/" + galleryEntity.getFilePath())
      • 앞에서도 언급했지만 파일을 업로드할 때, 파일명만 DB에 저장하므로 이는 S3 객체의 key 값이 됩니다.
      • s3Service에 정의된 상수(CLOUD_FRONT_DOMAIN_NAME)를 불러와서 "CloudFront URL + key"를 조합하여 이미지 full path를 dto에 정의합니다.




4] 이미지 조회 - GalleryDto

src/main/java/com/victolee/s3exam/dto/GalleryDto.java

private String imgFullPath;

...

@Builder
public GalleryDto(Long id, String title, String filePath, String imgFullPath) {
this.id = id;
this.title = title;
this.filePath = filePath;
this.imgFullPath = imgFullPath;
}
  • 멤버 변수 imgFullPath를 추가하고, 빌더에도 정의해줍니다.




6] 이미지 조회 - GalleryRepository

src/main/java/com/victolee/s3exam/domain/repository/GalleryRepository.java

public interface GalleryRepository extends JpaRepository<GalleryEntity, Long> {
@Override
List<GalleryEntity> findAll();
}





5] 이미지 조회 - 테스트


이미지를 업로드하고 개발자도구로 src 애트리뷰트를 확인해보면 CloudFront URL로부터 이미지를 가져온 것을 확인할 수 있습니다.




"개발자 도구 - Network" 탭에서 Response Headers 정보를 살펴보면 cloudfront에서 Cache가 Hit된 것을 확인할 수 있습니다.







4. 이미지 수정/삭제


이미지 수정은 업로드와 마찬가지로 putObject() 메서드를 사용하여 객체를 수정합니다.

따라서 아무런 코드를 수정하지 않고 S3 이미지를 수정할 수 있습니다.


그런데 파일명이 같은 이미지를 수정했을 경우, S3에는 정상적으로 수정된 이미지로 업로드가 되지만 CloudFront는 캐시되어 있는 상태이므로 페이지에 새로운 이미지로 반영되지 않습니다.

CloudFront 배포 설정에 따라 캐싱되는 시간에 차이가 있는데, 기본 값은 1일 입니다.

어쨋든 이미지를 수정했음에도 바로 반영이 안되므로 이는 서비스 운영 관점에서 이슈입니다.


따라서 이를 해결하려면 아래의 작업이 필요하며, 그 이유는 다음과 같습니다.

  • 고유한 파일명(객체의 key)으로 S3에 업로드
    • key가 중복되면 CloudFront 캐시에 의해, 수정된 이미지가 반영이 안될 수 있음
    • Invalidation으로 강제로 캐시를 purge 할 수 있지만, 그러면 cache를 하는 의미가 없게됨
  • 업로드 시, 해당 키의 객체가 존재하면 삭제 후 업로드
    • 이미지 수정시, 서비스에서 사용하지 않는 파일이 버킷에 여전히 저장되고 있으므로 버킷의 용량이 커짐 ---> 비용 증가





1] 이미지 수정/삭제 - Controller

src/main/java/com/victolee/s3exam/controller/GalleryController.java

@PostMapping("/gallery")
public String execWrite(GalleryDto galleryDto, MultipartFile file) throws IOException {
String imgPath = s3Service.upload(galleryDto.getFilePath(), file);
...
}

  • service.upload()를 호출할 때, 기존의 파일명을 파라미터로 전달합니다.
  • 이 값은 gallery.html에 hidden 값으로 정의되어 있습니다.




2] 이미지 조회 - S3Service

src/main/java/com/victolee/s3exam/service/S3Service.java

public String upload(String currentFilePath, MultipartFile file) throws IOException {
// 고유한 key 값을 갖기위해 현재 시간을 postfix로 붙여줌
SimpleDateFormat date = new SimpleDateFormat("yyyymmddHHmmss");
String fileName = file.getOriginalFilename() + "-" + date.format(new Date());

// key가 존재하면 기존 파일은 삭제
if ("".equals(currentFilePath) == false && currentFilePath != null) {
boolean isExistObject = s3Client.doesObjectExist(bucket, currentFilePath);

if (isExistObject == true) {
s3Client.deleteObject(bucket, currentFilePath);
}
}

// 파일 업로드
s3Client.putObject(new PutObjectRequest(bucket, fileName, file.getInputStream(), null)
.withCannedAcl(CannedAccessControlList.PublicRead));

return fileName;
}
  • fileName 변수
    • 단순히 file 객체의 파일명이 아닌, 현재 시간을 postfix에 추가한 값으로 할당합니다.
  • doesObjectExist()
    • 버킷에 해당 key를 가진 객체가 존재하는지 확인합니다.
    • 이미지 수정 시, 기존의 이미지를 버킷에 제거해줘야 버킷의 용량을 줄일 수 있으므로 deleteObject() 메서드를 호출하여 객체를 제거합니다.
  • putObject()
    • 파일 신규 업로드, 수정 업로드 모두 같은 메서드를 사용합니다.





2] 테스트


1) 기존 파일



2) 수정 업로드


동일한 파일명이지만 다른 그림을 업로드 했을 때, postfix로 인해서 CloudFront의 Cache가 적용되지 않은 것을 확인할 수 있습니다.

DB에도 잘 key값이 잘 반영되었네요.




3) S3에서 delete 되었는지 확인


신규/수정 업로드를 각각 수행했지만, 최종적으로는 마지막에 업로드한 파일만 존재하고 있는 것을 확인할 수 있습니다.




이상으로 Cloud Front를 적용했을 때 S3에 파일 업로드/조회 하는 방법에 대해 알아보았습니다.