Skip to content

Commit

Permalink
feat: S3와 데이터베이스의 정합성 오류 해결 (#820)
Browse files Browse the repository at this point in the history
* feat: 이미지 업로드, 삭제 실패시 재시도 추가

* refactor: 이미지 여러건 삭제 메소드 제거

* feat: 이미지 삭제 요청 비동기 처리

* style: 탭 오류 삭제

* feat: S3, 데이터베이스 불일치 해결 스케줄링 기능 구현
  • Loading branch information
Arachneee committed Nov 13, 2024
1 parent f41745f commit d693a5b
Show file tree
Hide file tree
Showing 16 changed files with 413 additions and 116 deletions.
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

0 comments on commit d693a5b

Please sign in to comment.