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 12 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 HANG_LOG_HOST = "image.hanglog.com";
Copy link
Collaborator

Choose a reason for hiding this comment

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

IMAGE_URL_HOST 또는 PROFILE_URL_HOST 어떤가요? (PROFILE이 명확해ㅓ 좋긴한데, Member도메인에서 imageUrl이라는 변수명을 사용하고 있어서 통일하려면 IMAGE_URL_HOST가 좋겠네요 ㅠㅠ)
변수명만 봤을때는 이게 멤버 프로필 이미지의 url host name이라는 것을 모를 것 같습니다.

Copy link
Member Author

Choose a reason for hiding this comment

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

날카로운 지적입니다 홍고라니
변경하였습니다.


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()
);
deletePreviousImage(member.getImageUrl(), updateMember.getImageUrl());
Copy link
Collaborator

Choose a reason for hiding this comment

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

얘는 previous네요?
previous vs before vs original
하나 통일할까요?

Copy link
Member Author

Choose a reason for hiding this comment

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

인정해버렸습니다

오리진으로 통일하겠습니다.

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 deletePreviousImage(final String previousUrl, final String updatedUrl) {
if (previousUrl.equals(updatedUrl)) {
return;
}
try {
final URL targetUrl = new URL(previousUrl);
if (targetUrl.getHost().equals(HANG_LOG_HOST)) {
final String targetName = previousUrl.substring(previousUrl.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 beforeImageName, final String updateImageName) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

beforeImageName.........이름 좋군요
그런데 Item에서는 originalPlace같은 네이밍을 사용하고 있어서 둘 중 하나로 통일하면 좋을 것 같아요

Copy link
Member Author

Choose a reason for hiding this comment

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

오리진 의견도 나와버렸군요

if (beforeImageName.equals(updateImageName)) {
return;
}
publisher.publishEvent(new S3ImageEvent(beforeImageName));
}

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