Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

스프링 이벤트를 사용한 S3 이미지 파일 삭제 기능 추가 #714

Merged
merged 13 commits into from
Oct 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions backend/src/main/java/hanglog/image/domain/S3ImageEvent.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package hanglog.image.domain;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public class S3ImageEvent {

private final String imageName;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package hanglog.image.infrastructure;

import static hanglog.global.exception.ExceptionCode.INVALID_IMAGE;
import static hanglog.global.exception.ExceptionCode.INVALID_IMAGE_PATH;

import com.amazonaws.AmazonServiceException;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.ObjectMetadata;
import hanglog.global.exception.ImageException;
import hanglog.image.domain.ImageFile;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class ImageUploader {

private static final String CACHE_CONTROL_VALUE = "max-age=3153600";

private final AmazonS3 s3Client;

@Value("${cloud.aws.s3.bucket}")
private String bucket;

@Value("${cloud.aws.s3.folder}")
private String folder;
Comment on lines +26 to +30
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Value때문에 분리해준 건가요? 분리한 이유는 무엇인가요? 어차피 서비스에서 밖에 사용 안되던데...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

레이어드 아키텍처 관점에서 분리했습니다.!


public List<String> uploadImages(final List<ImageFile> imageFiles) {
return imageFiles.stream()
.map(this::uploadImage)
.toList();
}

private String uploadImage(final ImageFile imageFile) {
final String path = folder + imageFile.getHashedName();
final ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentType(imageFile.getContentType());
metadata.setContentLength(imageFile.getSize());
metadata.setCacheControl(CACHE_CONTROL_VALUE);

try (final InputStream inputStream = imageFile.getInputStream()) {
s3Client.putObject(bucket, path, inputStream, metadata);
} catch (final AmazonServiceException e) {
throw new ImageException(INVALID_IMAGE_PATH);
} catch (final IOException e) {
throw new ImageException(INVALID_IMAGE);
}
return imageFile.getHashedName();
}
}
44 changes: 4 additions & 40 deletions backend/src/main/java/hanglog/image/service/ImageService.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,31 @@

import static hanglog.global.exception.ExceptionCode.EMPTY_IMAGE_LIST;
import static hanglog.global.exception.ExceptionCode.EXCEED_IMAGE_LIST_SIZE;
import static hanglog.global.exception.ExceptionCode.INVALID_IMAGE;
import static hanglog.global.exception.ExceptionCode.INVALID_IMAGE_PATH;

import com.amazonaws.AmazonServiceException;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.ObjectMetadata;
import hanglog.global.exception.ImageException;
import hanglog.image.domain.ImageFile;
import hanglog.image.dto.ImagesResponse;
import java.io.IOException;
import java.io.InputStream;
import hanglog.image.infrastructure.ImageUploader;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

@Service
@RequiredArgsConstructor
public class ImageService {

private static final int MAX_IMAGE_LIST_SIZE = 5;
private static final int EMPTY_LIST_SIZE = 0;
private static final String CACHE_CONTROL_VALUE = "max-age=3153600";

private final AmazonS3 s3Client;

@Value("${cloud.aws.s3.bucket}")
private String bucket;

@Value("${cloud.aws.s3.folder}")
private String folder;
private final ImageUploader imageUploader;

public ImagesResponse save(final List<MultipartFile> images) {
validateSizeOfImages(images);
final List<ImageFile> imageFiles = images.stream()
.map(ImageFile::new)
.toList();
final List<String> imageNames = uploadImages(imageFiles);
final List<String> imageNames = imageUploader.uploadImages(imageFiles);
return new ImagesResponse(imageNames);
}

Expand All @@ -51,27 +38,4 @@ private void validateSizeOfImages(final List<MultipartFile> images) {
throw new ImageException(EMPTY_IMAGE_LIST);
}
}

private List<String> uploadImages(final List<ImageFile> imageFiles) {
return imageFiles.stream()
.map(this::uploadImage)
.toList();
}

private String uploadImage(final ImageFile imageFile) {
final String path = folder + imageFile.getHashedName();
final ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentType(imageFile.getContentType());
metadata.setContentLength(imageFile.getSize());
metadata.setCacheControl(CACHE_CONTROL_VALUE);

try (final InputStream inputStream = imageFile.getInputStream()) {
s3Client.putObject(bucket, path, inputStream, metadata);
} catch (final AmazonServiceException e) {
throw new ImageException(INVALID_IMAGE_PATH);
} catch (final IOException e) {
throw new ImageException(INVALID_IMAGE);
}
return imageFile.getHashedName();
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package hanglog.listener;

import static org.springframework.transaction.annotation.Propagation.REQUIRES_NEW;

import hanglog.auth.domain.repository.RefreshTokenRepository;
import hanglog.expense.domain.repository.ExpenseRepository;
import hanglog.image.domain.repository.ImageRepository;
Expand All @@ -17,7 +19,6 @@
import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionalEventListener;

Expand All @@ -37,7 +38,7 @@ public class DeleteEventListener {
private final RefreshTokenRepository refreshTokenRepository;

@Async
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Transactional(propagation = REQUIRES_NEW)
@TransactionalEventListener(fallbackExecution = true)
public void deleteMember(final MemberDeleteEvent event) {
final List<Long> dayLogIds = customDayLogRepository.findDayLogIdsByTripIds(event.getTripIds());
Expand All @@ -51,9 +52,9 @@ public void deleteMember(final MemberDeleteEvent event) {
tripRepository.deleteByMemberId(event.getMemberId());
refreshTokenRepository.deleteByMemberId(event.getMemberId());
}

@Async
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Transactional(propagation = REQUIRES_NEW)
@TransactionalEventListener(fallbackExecution = true)
public void deleteTrip(final TripDeleteEvent event) {
final List<Long> dayLogIds = customDayLogRepository.findDayLogIdsByTripId(event.getTripId());
Expand Down
33 changes: 33 additions & 0 deletions backend/src/main/java/hanglog/listener/S3ImageEventListener.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package hanglog.listener;

import static org.springframework.transaction.annotation.Propagation.REQUIRES_NEW;

import com.amazonaws.services.s3.AmazonS3;
import hanglog.image.domain.S3ImageEvent;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionalEventListener;

@Component
@RequiredArgsConstructor
public class S3ImageEventListener {

private final AmazonS3 s3Client;

@Value("${cloud.aws.s3.bucket}")
private String bucket;

@Value("${cloud.aws.s3.folder}")
private String folder;

@Async
@Transactional(propagation = REQUIRES_NEW)
@TransactionalEventListener(fallbackExecution = true)
public void deleteImageFileInS3(final S3ImageEvent event) {
final String imageName = event.getImageName();
s3Client.deleteObject(bucket, folder + imageName);
Comment on lines +30 to +31
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기는 예외처리 필요없을까요?
s3에서 delete중 예외가 발생할 경우

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기서 catch를 받아 customException을 던져 줄까요.?

}
}
27 changes: 25 additions & 2 deletions backend/src/main/java/hanglog/member/service/MemberService.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
package hanglog.member.service;

import static hanglog.global.exception.ExceptionCode.DUPLICATED_MEMBER_NICKNAME;
import static hanglog.global.exception.ExceptionCode.INVALID_IMAGE_URL;
import static hanglog.global.exception.ExceptionCode.NOT_FOUND_MEMBER_ID;

import hanglog.global.exception.BadRequestException;
import hanglog.image.domain.S3ImageEvent;
import hanglog.member.domain.Member;
import hanglog.member.domain.repository.MemberRepository;
import hanglog.member.dto.request.MyPageRequest;
import hanglog.member.dto.response.MyPageResponse;
import java.net.MalformedURLException;
import java.net.URL;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -17,7 +22,10 @@
@Transactional
public class MemberService {

private static final String IMAGE_URL_HOST = "image.hanglog.com";

private final MemberRepository memberRepository;
private final ApplicationEventPublisher publisher;

@Transactional(readOnly = true)
public MyPageResponse getMyPageInfo(final Long memberId) {
Expand All @@ -33,19 +41,34 @@ public void updateMyPageInfo(final Long memberId, final MyPageRequest myPageRequ
if (member.isNicknameChanged(myPageRequest.getNickname())) {
checkDuplicatedNickname(myPageRequest.getNickname());
}

final Member updateMember = new Member(
memberId,
member.getSocialLoginId(),
myPageRequest.getNickname(),
myPageRequest.getImageUrl()
);
deleteOriginalImage(member.getImageUrl(), updateMember.getImageUrl());
memberRepository.save(updateMember);
}

private void checkDuplicatedNickname(final String nickname) {
if(memberRepository.existsByNickname(nickname)) {
if (memberRepository.existsByNickname(nickname)) {
throw new BadRequestException(DUPLICATED_MEMBER_NICKNAME);
}
}

private void deleteOriginalImage(final String originalUrl, final String updatedUrl) {
if (originalUrl.equals(updatedUrl)) {
return;
}
try {
final URL targetUrl = new URL(originalUrl);
if (targetUrl.getHost().equals(IMAGE_URL_HOST)) {
final String targetName = originalUrl.substring(originalUrl.lastIndexOf("/") + 1);
publisher.publishEvent(new S3ImageEvent(targetName));
}
} catch (final MalformedURLException e) {
throw new BadRequestException(INVALID_IMAGE_URL);
}
}
}
2 changes: 1 addition & 1 deletion backend/src/main/java/hanglog/trip/domain/Trip.java
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ public static Trip of(final Member member, final String title, final LocalDate s

public void update(final TripUpdateRequest updateRequest) {
this.title = updateRequest.getTitle();
this.imageName = updateImageName(updateRequest.getImageUrl());
this.imageName = updateImageName(updateRequest.getImageName());
this.startDate = updateRequest.getStartDate();
this.endDate = updateRequest.getEndDate();
this.description = updateRequest.getDescription();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public class TripUpdateRequest {
@Size(max = 50, message = "여행 제목은 50자를 넘을 수 없습니다.")
private final String title;

private final String imageUrl;
private final String imageName;

@NotNull(message = "여행 시작 날짜를 입력해 주세요.")
@DateTimeFormat(pattern = "yyyy-MM-dd")
Expand Down
5 changes: 5 additions & 0 deletions backend/src/main/java/hanglog/trip/service/ItemService.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import hanglog.expense.domain.repository.ExpenseRepository;
import hanglog.global.exception.BadRequestException;
import hanglog.image.domain.Image;
import hanglog.image.domain.S3ImageEvent;
import hanglog.image.domain.repository.CustomImageRepository;
import hanglog.image.domain.repository.ImageRepository;
import hanglog.trip.domain.DayLog;
Expand All @@ -29,6 +30,7 @@
import hanglog.trip.dto.response.ItemResponse;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -44,6 +46,7 @@ public class ItemService {
private final ExpenseRepository expenseRepository;
private final ImageRepository imageRepository;
private final CustomImageRepository customImageRepository;
private final ApplicationEventPublisher publisher;

public Long save(final Long tripId, final ItemRequest itemRequest) {
final DayLog dayLog = dayLogRepository.findWithItemsById(itemRequest.getDayLogId())
Expand Down Expand Up @@ -200,6 +203,7 @@ private void deleteNotUsedImages(final List<Image> originalImages, final List<Im
return;
}
customImageRepository.deleteAll(deletedImages);
deletedImages.forEach(image -> publisher.publishEvent(new S3ImageEvent(image.getName())));
}

private Expense makeUpdatedExpense(final ExpenseRequest expenseRequest, final Expense originalExpense) {
Expand Down Expand Up @@ -275,6 +279,7 @@ public void delete(final Long itemId) {
expenseRepository.deleteById(item.getExpense().getId());
}
itemRepository.deleteById(itemId);
item.getImages().forEach(image -> publisher.publishEvent(new S3ImageEvent(image.getName())));
}

public List<ItemResponse> getItems() {
Expand Down
9 changes: 9 additions & 0 deletions backend/src/main/java/hanglog/trip/service/TripService.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import hanglog.community.domain.PublishedTrip;
import hanglog.global.exception.AuthException;
import hanglog.global.exception.BadRequestException;
import hanglog.image.domain.S3ImageEvent;
import hanglog.member.domain.Member;
import hanglog.member.domain.repository.MemberRepository;
import hanglog.share.domain.repository.SharedTripRepository;
Expand Down Expand Up @@ -120,6 +121,7 @@ public void update(final Long tripId, final TripUpdateRequest updateRequest) {

updateTripCities(tripId, cities);
updateDayLog(updateRequest, trip);
updateImage(trip.getImageName(), updateRequest.getImageName());
trip.update(updateRequest);
tripRepository.save(trip);
}
Expand All @@ -144,6 +146,13 @@ private void updateDayLog(final TripUpdateRequest updateRequest, final Trip trip
}
}

private void updateImage(final String originalImageName, final String updateImageName) {
if (originalImageName.equals(updateImageName)) {
return;
}
publisher.publishEvent(new S3ImageEvent(originalImageName));
}

private void updateDayLogByPeriod(final Trip trip, final int currentPeriod, final int requestPeriod) {
final DayLog extraDayLog = trip.getDayLogs().get(currentPeriod);
if (currentPeriod < requestPeriod) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -490,10 +490,10 @@ void updateTrip() throws Exception {
.type(JsonFieldType.STRING)
.description("여행 제목")
.attributes(field("constraint", "50자 이하의 문자열")),
fieldWithPath("imageUrl")
fieldWithPath("imageName")
.type(JsonFieldType.STRING)
.description("여행 이미지")
.attributes(field("constraint", "이미지 URL")),
.attributes(field("constraint", "이미지 이름")),
fieldWithPath("startDate")
.type(JsonFieldType.STRING)
.description("여행 시작 날짜")
Expand Down
Loading