Skip to content

Commit

Permalink
refactor: S3 이미지 업로드 의존성 분리
Browse files Browse the repository at this point in the history
  • Loading branch information
kevstevie committed Dec 14, 2023
1 parent 2d5c8f4 commit 27c82c4
Show file tree
Hide file tree
Showing 15 changed files with 285 additions and 140 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,128 +3,58 @@
import com.woowacourse.matzip.application.response.ImageUploadResponse;
import com.woowacourse.matzip.domain.image.ImageExtension;
import com.woowacourse.matzip.domain.image.UnusedImage;
import com.woowacourse.matzip.domain.image.unusedImageRepository;
import com.woowacourse.matzip.exception.UploadFailedException;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Value;
import com.woowacourse.matzip.domain.image.UnusedImageRepository;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
import software.amazon.awssdk.services.s3.model.DeleteObjectsRequest;
import software.amazon.awssdk.services.s3.model.ObjectIdentifier;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;

@Service
@Transactional(readOnly = true)
public class ImageService {

private static final String EXTENSION_DELIMITER = ".";
private static final String CLOUDFRONT_URL = "https://image.matzip.today/";
private final ImageUploader imageUploader;
private final UnusedImageRepository unusedImageRepository;

private final S3Client s3Client;
private final String bucketName;
private final unusedImageRepository unusedImageRepository;

public ImageService(final S3Client s3Client, @Value("${cloud.aws.s3.bucket}") final String bucketName, unusedImageRepository unusedImageRepository) {
this.s3Client = s3Client;
this.bucketName = bucketName;
public ImageService(final ImageUploader imageUploader, final UnusedImageRepository unusedImageRepository) {
this.imageUploader = imageUploader;
this.unusedImageRepository = unusedImageRepository;
}

@Transactional
public ImageUploadResponse uploadImage(final MultipartFile file) {
String extension = validateExtension(file);
String key = createKey(extension);
PutObjectRequest request = createPutRequest(file, key);
try {
s3Client.putObject(request, RequestBody.fromBytes(file.getBytes()));
} catch (IOException e) {
throw new UploadFailedException();
}
String originalFileName = file.getOriginalFilename();
ImageExtension.validateExtension(originalFileName);
String imageUrl = imageUploader.uploadImage(file);
unusedImageRepository.save(
UnusedImage.builder()
.key(key)
.build()
.imageUrl(imageUrl)
.build()
);
return new ImageUploadResponse(CLOUDFRONT_URL + key);
}

private String validateExtension(final MultipartFile file) {
String originalFilename = file.getOriginalFilename();
String extension = Objects.requireNonNull(originalFilename)
.substring(originalFilename.lastIndexOf('.') + 1);
return ImageExtension.validateExtension(extension);
}

private String createKey(final String extension) {
String uuid = UUID.randomUUID().toString();
return uuid + EXTENSION_DELIMITER + extension;
}

private PutObjectRequest createPutRequest(final MultipartFile file, final String key) {
return PutObjectRequest.builder()
.bucket(bucketName)
.key(key)
.contentLength(file.getSize())
.build();
return new ImageUploadResponse(imageUrl);
}

@Transactional
public void deleteUsingImage(final String imageUrl) {
if (imageUrl != null) {
String key = imageUrl.substring(CLOUDFRONT_URL.length());
unusedImageRepository.deleteByKey(key);
}
unusedImageRepository.deleteByImageUrl(imageUrl);
}

@Transactional
public void deleteImageWhenReviewDeleted(final String imageUrl) {
if (imageUrl != null) {
String key = imageUrl.substring(CLOUDFRONT_URL.length());
DeleteObjectRequest request = createDeleteRequest(key);
s3Client.deleteObject(request);
}
}

private DeleteObjectRequest createDeleteRequest(final String key) {
return DeleteObjectRequest.builder()
.bucket(bucketName)
.key(key)
.build();
imageUploader.deleteImage(imageUrl);
unusedImageRepository.deleteByImageUrl(imageUrl);
}

@Scheduled(cron = "0 0 4 * * *")
@Transactional
public void deleteUnusedImages() {
LocalDateTime deleteBoundary = LocalDateTime.now().minusDays(1L);
LocalDateTime deleteBoundary = LocalDate.now().atStartOfDay();
List<UnusedImage> unusedImages = unusedImageRepository.findAllByCreatedAtBefore(deleteBoundary);
unusedImageRepository.deleteAllByCreatedAtBefore(deleteBoundary);
List<ObjectIdentifier> identifiers = toIdentifiers(unusedImages);
s3Client.deleteObjects(createDeleteObjectsRequest(identifiers));
}

private List<ObjectIdentifier> toIdentifiers(final List<UnusedImage> unusedImages) {
return unusedImages.stream()
.map(UnusedImage::getKey)
.map(key -> ObjectIdentifier.builder()
.key(key)
.build())
.collect(Collectors.toList());
}

private DeleteObjectsRequest createDeleteObjectsRequest(final List<ObjectIdentifier> identifiers) {
return DeleteObjectsRequest.builder()
.bucket(bucketName)
.delete(it -> it.objects(identifiers).build())
.build();
imageUploader.deleteImages(unusedImages);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.woowacourse.matzip.application;

import com.woowacourse.matzip.domain.image.UnusedImage;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;

public interface ImageUploader {

String uploadImage(final MultipartFile file);

void deleteImage(final String imageUrl);

void deleteImages(final List<UnusedImage> unusedImages);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package com.woowacourse.matzip.application;

import com.woowacourse.matzip.domain.image.UnusedImage;
import com.woowacourse.matzip.exception.UploadFailedException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
import software.amazon.awssdk.services.s3.model.DeleteObjectsRequest;
import software.amazon.awssdk.services.s3.model.ObjectIdentifier;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;

import java.io.IOException;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors;

import static com.woowacourse.matzip.environment.ProfileUtil.LOCAL;
import static com.woowacourse.matzip.environment.ProfileUtil.PROD;

@Profile({LOCAL, PROD})
@Component
public class S3ImageUploader implements ImageUploader {

private static final String EXTENSION_DELIMITER = ".";
private static final String URL_DELIMITER = "/";

private final S3Client s3Client;
private final String bucketName;
private final String cloudFrontUrl;

public S3ImageUploader(final S3Client s3Client,
@Value("${cloud.aws.s3.bucket}") final String bucketName,
@Value("${cloud.aws.s3.cloud-front-url}") final String cloudFrontUrl) {
this.s3Client = s3Client;
this.bucketName = bucketName;
this.cloudFrontUrl = cloudFrontUrl;
}

@Override
public String uploadImage(final MultipartFile file) {
String key = createKey(file);
PutObjectRequest request = createPutRequest(file, key);
try {
s3Client.putObject(request, RequestBody.fromBytes(file.getBytes()));
} catch (IOException e) {
throw new UploadFailedException();
}

return cloudFrontUrl + key;
}

private PutObjectRequest createPutRequest(final MultipartFile file, final String key) {
return PutObjectRequest.builder()
.bucket(bucketName)
.key(key)
.contentLength(file.getSize())
.build();
}

private String createKey(final MultipartFile file) {
String fileName = file.getOriginalFilename();
Objects.requireNonNull(fileName);
String extension = fileName.substring(fileName.lastIndexOf(EXTENSION_DELIMITER + 1));
String uuid = UUID.randomUUID().toString();
return uuid + EXTENSION_DELIMITER + extension;
}

@Override
public void deleteImage(final String imageUrl) {
DeleteObjectRequest request = createDeleteRequest(imageUrl);
s3Client.deleteObject(request);
}

private DeleteObjectRequest createDeleteRequest(final String imageUrl) {
String key = imageUrl.substring(imageUrl.lastIndexOf(URL_DELIMITER) + 1);
return DeleteObjectRequest.builder()
.bucket(bucketName)
.key(key)
.build();
}

@Override
public void deleteImages(final List<UnusedImage> unusedImages) {

List<ObjectIdentifier> identifiers = toIdentifiers(unusedImages);
s3Client.deleteObjects(createDeleteObjectsRequest(identifiers));
}

private List<ObjectIdentifier> toIdentifiers(final List<UnusedImage> unusedImages) {
return unusedImages.stream()
.map(UnusedImage::getImageUrl)
.map(key -> ObjectIdentifier.builder()
.key(key)
.build())
.collect(Collectors.toList());
}

private DeleteObjectsRequest createDeleteObjectsRequest(final List<ObjectIdentifier> identifiers) {
return DeleteObjectsRequest.builder()
.bucket(bucketName)
.delete(it -> it.objects(identifiers).build())
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import software.amazon.awssdk.auth.credentials.SystemPropertyCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;

import static com.woowacourse.matzip.environment.ProfileUtil.LOCAL;
import static com.woowacourse.matzip.environment.ProfileUtil.PROD;

@Profile({LOCAL, PROD})
@Configuration
public class S3Config {

Expand Down
3 changes: 3 additions & 0 deletions matzip-app-external-api/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ spring:
prod:
- web-prod
- db-prod
active:
- local
config:
import: classpath:prod/application.yml

Expand Down Expand Up @@ -43,6 +45,7 @@ cloud:
aws:
s3:
bucket: matzip-image
cloud-front-url: https://image.matzip.today/
region:
static: ap-northeast-2
stack:
Expand Down
16 changes: 12 additions & 4 deletions matzip-app-external-api/src/main/resources/schema-local.sql
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,19 @@ create table restaurant_request

create table bookmark
(
id bigint NOT NULL AUTO_INCREMENT,
member_id bigint NOT NULL,
restaurant_id bigint NOT NULL,
id bigint NOT NULL AUTO_INCREMENT,
member_id bigint NOT NULL,
restaurant_id bigint NOT NULL,
primary key (id)
);

alter table bookmark add constraint unique_member_restaurant unique (member_id, restaurant_id);
create table unused_image
(
id bigint NOT NULL AUTO_INCREMENT,
image_url varchar(255) NOT NULL,
created_at TIMESTAMP NOT NULL
);

alter table bookmark
add constraint unique_member_restaurant unique (member_id, restaurant_id);

Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.woowacourse.matzip.application;

import com.woowacourse.matzip.domain.image.UnusedImage;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;

import static com.woowacourse.matzip.environment.ProfileUtil.TEST;

@Component
@Profile(TEST)
public class TestImageUploader implements ImageUploader{

@Override
public String uploadImage(MultipartFile file) {
return null;
}

@Override
public void deleteImage(String imageUrl) {

}

@Override
public void deleteImages(List<UnusedImage> unusedImages) {

}
}
12 changes: 0 additions & 12 deletions matzip-app-external-api/src/test/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,5 @@ security:
secret-key: testtesttesttesttestteststestkey
expire-length: 3600000

cloud:
aws:
s3:
bucket: matzip-image
region:
static: ap-northeast-2
stack:
auto: false
credentials:
access-key: access-key
secret-key: secret-key

server:
port: 8180
Loading

0 comments on commit 27c82c4

Please sign in to comment.