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

[BE] S3와 데이터베이스의 정합성 오류 해결 #820

Merged
merged 5 commits into from
Nov 9, 2024
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
2 changes: 2 additions & 0 deletions server/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.retry:spring-retry'
implementation 'org.springframework:spring-aspects'

implementation 'io.jsonwebtoken:jjwt:0.9.1'
implementation 'javax.xml.bind:jaxb-api:2.3.1'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;

@Slf4j
@EnableRetry
@EnableAsync
@EnableScheduling
@SpringBootApplication
public class HaengdongApplication {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,128 @@
package server.haengdong.application;

import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutorService;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import server.haengdong.application.response.EventImageSaveAppResponse;
import server.haengdong.application.response.ImageInfo;
import server.haengdong.exception.HaengdongErrorCode;
import server.haengdong.exception.HaengdongException;

@RequiredArgsConstructor
@Service
public class EventImageFacadeService {

private final EventService eventService;
private final ImageService imageService;
private final ExecutorService executorService;

public void uploadImages(String token, List<MultipartFile> images) {
eventService.validateImageCount(token, images.size());
List<String> imageNames = imageService.uploadImages(images);
eventService.saveImages(token, imageNames);
List<EventImageSaveAppResponse> imageNames = eventService.saveImages(token, getOriginalNames(images));
List<CompletableFuture<String>> futures = createFutures(images, imageNames);
CompletableFuture<List<String>> allFutures = createAllFutures(token, futures, imageNames);

try {
allFutures.join();
} catch (CompletionException e) {
throw new HaengdongException(HaengdongErrorCode.IMAGE_UPLOAD_FAIL, e);
}
}

private List<CompletableFuture<String>> createFutures(
List<MultipartFile> images, List<EventImageSaveAppResponse> imageNames
) {
return IntStream.range(0, imageNames.size())
.mapToObj(i -> CompletableFuture.supplyAsync(() -> {
imageService.uploadImage(images.get(i), imageNames.get(i).name());
return imageNames.get(i).name();
}, executorService))
.toList();
}

private CompletableFuture<List<String>> createAllFutures(
String token, List<CompletableFuture<String>> futures, List<EventImageSaveAppResponse> imageNames
) {
return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.thenApply(v -> applyFutures(futures))
.exceptionally(ex -> {
eventService.deleteImages(token, getImageIds(imageNames));
getSuccessUploadImages(futures).forEach(imageService::deleteImage);

throw new HaengdongException(HaengdongErrorCode.IMAGE_UPLOAD_FAIL, ex);
});
}

private List<String> applyFutures(List<CompletableFuture<String>> futures) {
return futures.stream()
.map(CompletableFuture::join)
.toList();
}

private List<String> getSuccessUploadImages(List<CompletableFuture<String>> futures) {
return futures.stream()
.filter(future -> future.isDone() && !future.isCompletedExceptionally())
.map(CompletableFuture::join)
.toList();
}

private List<String> getOriginalNames(List<MultipartFile> images) {
return images.stream()
.map(MultipartFile::getOriginalFilename)
.toList();
}

private List<Long> getImageIds(List<EventImageSaveAppResponse> imageNames) {
return imageNames.stream()
.map(EventImageSaveAppResponse::id)
.toList();
}

public void deleteImage(String token, Long imageId) {
String imageName = eventService.deleteImage(token, imageId);
imageService.deleteImage(imageName);
}

@Scheduled(cron = "0 0 0 * * MON")
public void removeUnmatchedImage() {
Instant endDate = Instant.now().minus(1, ChronoUnit.DAYS);

List<EventImageSaveAppResponse> savedEventImages = eventService.findImagesDateBefore(endDate);
List<ImageInfo> foundImages = imageService.findImages();

removeImageNotInS3(savedEventImages, foundImages);
removeImageNotInRepository(savedEventImages, foundImages);
}

private void removeImageNotInS3(List<EventImageSaveAppResponse> eventImages, List<ImageInfo> images) {
Set<String> imageNames = images.stream()
.map(ImageInfo::name)
.collect(Collectors.toSet());

eventImages.stream()
.filter(eventImage -> !imageNames.contains(eventImage.name()))
.map(EventImageSaveAppResponse::id)
.forEach(eventService::deleteImage);
}

private void removeImageNotInRepository(List<EventImageSaveAppResponse> eventImages, List<ImageInfo> images) {
Set<String> imageNames = eventImages.stream()
.map(EventImageSaveAppResponse::name)
.collect(Collectors.toSet());

images.stream()
.filter(imageInfo -> imageInfo.createAt().isBefore(Instant.now().minus(2, ChronoUnit.DAYS)))
.map(ImageInfo::name)
.filter(name -> !imageNames.contains(name))
.forEach(imageService::deleteImage);
}
}
69 changes: 47 additions & 22 deletions server/src/main/java/server/haengdong/application/EventService.java
Original file line number Diff line number Diff line change
@@ -1,27 +1,28 @@
package server.haengdong.application;

import java.time.Instant;
import java.util.List;
import java.util.Map.Entry;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import server.haengdong.application.request.EventAppRequest;
import server.haengdong.application.request.EventLoginAppRequest;
import server.haengdong.application.request.EventUpdateAppRequest;
import server.haengdong.application.response.EventAppResponse;
import server.haengdong.application.response.EventDetailAppResponse;
import server.haengdong.application.response.EventImageAppResponse;
import server.haengdong.application.response.EventImageSaveAppResponse;
import server.haengdong.application.response.MemberBillReportAppResponse;
import server.haengdong.domain.RandomValueProvider;
import server.haengdong.domain.bill.Bill;
import server.haengdong.domain.bill.BillRepository;
import server.haengdong.domain.bill.MemberBillReport;
import server.haengdong.domain.event.Event;
import server.haengdong.domain.event.EventImage;
import server.haengdong.domain.event.EventImageRepository;
import server.haengdong.domain.event.EventRepository;
import server.haengdong.domain.event.EventTokenProvider;
import server.haengdong.domain.member.Member;
import server.haengdong.exception.AuthenticationException;
import server.haengdong.exception.HaengdongErrorCode;
Expand All @@ -35,7 +36,7 @@ public class EventService {
private static final int MAX_IMAGE_COUNT = 10;

private final EventRepository eventRepository;
private final EventTokenProvider eventTokenProvider;
private final RandomValueProvider randomValueProvider;
private final BillRepository billRepository;
private final EventImageRepository eventImageRepository;

Expand All @@ -44,7 +45,7 @@ public class EventService {

@Transactional
public EventAppResponse saveEvent(EventAppRequest request) {
String token = eventTokenProvider.createToken();
String token = randomValueProvider.createRandomValue();
Event event = request.toEvent(token);
eventRepository.save(event);

Expand All @@ -64,11 +65,6 @@ public void validatePassword(EventLoginAppRequest request) throws HaengdongExcep
}
}

private Event getEvent(String token) {
return eventRepository.findByToken(token)
.orElseThrow(() -> new HaengdongException(HaengdongErrorCode.EVENT_NOT_FOUND));
}

public List<MemberBillReportAppResponse> getMemberBillReports(String token) {
Event event = eventRepository.findByToken(token)
.orElseThrow(() -> new HaengdongException(HaengdongErrorCode.EVENT_NOT_FOUND));
Expand Down Expand Up @@ -105,14 +101,28 @@ public void updateEvent(String token, EventUpdateAppRequest request) {
}

@Transactional
public void saveImages(String token, List<String> imageNames) {
public List<EventImageSaveAppResponse> saveImages(String token, List<String> originalImageNames) {
Event event = getEvent(token);
validateImageCount(originalImageNames, event);

List<EventImage> images = imageNames.stream()
.map(imageName -> new EventImage(event, imageName))
List<EventImage> eventImages = originalImageNames.stream()
.map(imageName -> new EventImage(event, randomValueProvider.createRandomValue() + imageName))
.toList();

eventImageRepository.saveAll(images);
eventImageRepository.saveAll(eventImages);

return eventImages.stream()
.map(EventImageSaveAppResponse::of)
.toList();
}

private void validateImageCount(List<String> images, Event event) {
Long imageCount = eventImageRepository.countByEvent(event);
Long totalImageCount = imageCount + images.size();

if (totalImageCount > MAX_IMAGE_COUNT) {
throw new HaengdongException(HaengdongErrorCode.IMAGE_COUNT_INVALID, totalImageCount);
}
}

public List<EventImageAppResponse> findImages(String token) {
Expand All @@ -130,8 +140,7 @@ private String createUrl(EventImage image) {

@Transactional
public String deleteImage(String token, Long imageId) {
EventImage eventImage = eventImageRepository.findById(imageId)
.orElseThrow(() -> new HaengdongException(HaengdongErrorCode.IMAGE_NOT_FOUND));
EventImage eventImage = getEventImage(imageId);

Event event = eventImage.getEvent();
if (event.isTokenMismatch(token)) {
Expand All @@ -141,13 +150,29 @@ public String deleteImage(String token, Long imageId) {
return eventImage.getName();
}

public void validateImageCount(String token, int uploadImageCount) {
Event event = getEvent(token);
Long imageCount = eventImageRepository.countByEvent(event);
Long totalImageCount = imageCount + uploadImageCount;
@Transactional
public void deleteImage(Long imageId) {
eventImageRepository.deleteById(imageId);
}

if (totalImageCount > MAX_IMAGE_COUNT) {
throw new HaengdongException(HaengdongErrorCode.IMAGE_COUNT_INVALID, totalImageCount);
}
@Transactional
public void deleteImages(String token, List<Long> imageIds) {
imageIds.forEach(imageId -> deleteImage(token, imageId));
}

private Event getEvent(String token) {
return eventRepository.findByToken(token)
.orElseThrow(() -> new HaengdongException(HaengdongErrorCode.EVENT_NOT_FOUND));
}

private EventImage getEventImage(Long imageId) {
return eventImageRepository.findById(imageId)
.orElseThrow(() -> new HaengdongException(HaengdongErrorCode.IMAGE_NOT_FOUND));
}

public List<EventImageSaveAppResponse> findImagesDateBefore(Instant date) {
return eventImageRepository.findByCreatedAtAfter(date).stream()
.map(EventImageSaveAppResponse::of)
.toList();
}
}
Loading
Loading