Skip to content

Commit

Permalink
Merge pull request #75 from f-lab-edu/feature/73
Browse files Browse the repository at this point in the history
[#73] 파일 저장 동기적 처리
  • Loading branch information
f-lab-moony authored Apr 11, 2024
2 parents ef31f72 + 840c0a6 commit 50649ba
Show file tree
Hide file tree
Showing 4 changed files with 45 additions and 33 deletions.
5 changes: 3 additions & 2 deletions src/main/java/com/stoury/controller/ExceptionController.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.stoury.exception.feed.FeedCreateException;
import com.stoury.exception.feed.FeedSearchException;
import com.stoury.exception.feed.FeedUpdateException;
import com.stoury.exception.graphiccontent.GraphicContentsException;
import com.stoury.exception.location.GeocodeApiException;
import com.stoury.exception.member.MemberCreateException;
import com.stoury.exception.member.MemberDeleteException;
Expand Down Expand Up @@ -39,10 +40,10 @@ public ResponseEntity<ErrorResponse> handle404(RuntimeException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ErrorResponse.of(ex.getMessage()));
}

@ExceptionHandler(value = {GeocodeApiException.class})
@ExceptionHandler(value = {GeocodeApiException.class, GraphicContentsException.class})
public ResponseEntity<ErrorResponse> handle500(RuntimeException ex) {
log.error(ex.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ErrorResponse.of(ex.getMessage()));
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ErrorResponse.of("Some error occur."));
}

@ExceptionHandler(value = {NotAuthorizedException.class})
Expand Down
18 changes: 11 additions & 7 deletions src/main/java/com/stoury/event/EventHandlers.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,42 +5,46 @@
import com.stoury.domain.Member;
import com.stoury.domain.Tag;
import com.stoury.exception.feed.FeedSearchException;
import com.stoury.exception.graphiccontent.GraphicContentsException;
import com.stoury.projection.FeedResponseEntity;
import com.stoury.repository.FeedRepository;
import com.stoury.service.storage.StorageService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
import org.springframework.web.multipart.MultipartFile;

import java.nio.file.Paths;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

@Slf4j
@Component
@RequiredArgsConstructor
public class EventHandlers {
private final StorageService storageService;
private final FeedRepository feedRepository;

@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onFileSaveEventHandler(GraphicSaveEvent graphicSaveEvent) {
MultipartFile fileToSave = graphicSaveEvent.getFileToSave();
@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
public void onFeedCreationFailEventHandler(GraphicSaveEvent graphicSaveEvent) {
String path = graphicSaveEvent.getPath();
storageService.saveFileAtPath(fileToSave, Paths.get(path));
storageService.deleteFileAtPath(Paths.get(path));
}

@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onFileDeleteEventHandler(GraphicDeleteEvent graphicDeleteEvent) {
String path = graphicDeleteEvent.getPath();
storageService.deleteFileAtPath(Paths.get(path));
try {
storageService.deleteFileAtPath(Paths.get(path));
} catch (GraphicContentsException exception) {
log.error(exception.getMessage());
}
}

@Async
Expand Down
48 changes: 26 additions & 22 deletions src/main/java/com/stoury/service/FeedService.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import com.stoury.repository.LikeRepository;
import com.stoury.repository.MemberRepository;
import com.stoury.service.location.LocationService;
import com.stoury.service.storage.StorageService;
import com.stoury.utils.FileUtils;
import com.stoury.utils.JsonMapper;
import com.stoury.utils.SupportedFileType;
Expand All @@ -32,6 +33,7 @@
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
Expand All @@ -43,6 +45,7 @@
public class FeedService {
@Value("${path-prefix}")
public String pathPrefix;
private final StorageService storageService;
private final FeedRepository feedRepository;
private final MemberRepository memberRepository;
private final LikeRepository likeRepository;
Expand Down Expand Up @@ -76,10 +79,12 @@ private List<GraphicContent> saveGraphicContents(List<MultipartFile> graphicCont
for (int i = 0; i < graphicContents.size(); i++) {
MultipartFile file = graphicContents.get(i);

GraphicSaveEvent event = publishNewFileEvent(file);
String contentPath = event.getPath();
String path = FileUtils.createFilePath(file, pathPrefix);

graphicContentList.add(new GraphicContent(contentPath, i));
graphicContentList.add(new GraphicContent(path, i));

storageService.saveFileAtPath(file, Paths.get(path));
eventPublisher.publishEvent(new GraphicSaveEvent(this, file, path)); // NOSONAR
}

return graphicContentList;
Expand All @@ -97,13 +102,6 @@ private Set<Tag> getOrCreateTags(Set<String> tagNames) {
.collect(Collectors.toSet());
}

private GraphicSaveEvent publishNewFileEvent(MultipartFile file) {
String path = FileUtils.createFilePath(file, pathPrefix);
GraphicSaveEvent event = new GraphicSaveEvent(this, file, path);
eventPublisher.publishEvent(event); // NOSONAR
return event;
}

private void validate(Long writerId, FeedCreateRequest feedCreateRequest, List<MultipartFile> graphicContents) {
if (writerId == null) {
throw new FeedCreateException("Writer id cannot be null");
Expand Down Expand Up @@ -149,8 +147,10 @@ private FeedResponse toFeedResponse(FeedResponseEntity feedResponseEntity) {

SimpleMemberResponse writer = new SimpleMemberResponse(feedResponseEntity.getWriterId(), feedResponseEntity.getWriterUsername());
List<GraphicContentResponse> graphicContents =
jsonMapper.stringJsonToObject(feedResponseEntity.getGraphicContentPaths(), new TypeReference<List<GraphicContentResponse>>() {});
Set<String> tags = jsonMapper.stringJsonToObject(feedResponseEntity.getTagNames(), new TypeReference<Set<String>>() {});
jsonMapper.stringJsonToObject(feedResponseEntity.getGraphicContentPaths(), new TypeReference<List<GraphicContentResponse>>() {
});
Set<String> tags = jsonMapper.stringJsonToObject(feedResponseEntity.getTagNames(), new TypeReference<Set<String>>() {
});
LocationResponse location = new LocationResponse(feedResponseEntity.getCity(), feedResponseEntity.getCountry());


Expand All @@ -167,7 +167,11 @@ protected FeedResponse updateFeed(Long feedId, FeedUpdateRequest feedUpdateReque
feed.updateTags(getOrCreateTags(feedUpdateRequest.tagNames()));
feed.deleteSelectedGraphics(feedUpdateRequest.deleteGraphicContentSequence());

publishDeleteFileEvents(beforeDeleteGraphicContents, feed.getGraphicContents());
List<GraphicContent> toDeleteGraphicContents = beforeDeleteGraphicContents.stream()
.filter(graphicContent -> !feed.getGraphicContents().contains(graphicContent))
.toList();

publishDeleteFileEvents(toDeleteGraphicContents);
eventPublisher.publishEvent(new FeedResponseUpdateEvent(this, feed.getId()));

return FeedResponse.from(feed, likeRepository.getCountByFeedId(feed.getId().toString()));
Expand All @@ -185,19 +189,19 @@ public FeedResponse updateFeedIfOwner(Long feedId, FeedUpdateRequest feedUpdateR
return updateFeed(feedId, feedUpdateRequest);
}

private void publishDeleteFileEvents(List<GraphicContent> beforeDeleteGraphicContents,
List<GraphicContent> afterDeleteGraphicContents) {
for (GraphicContent beforeDeleteGraphicContent : beforeDeleteGraphicContents) {
if (!afterDeleteGraphicContents.contains(beforeDeleteGraphicContent)) {
eventPublisher.publishEvent(new GraphicDeleteEvent(this, beforeDeleteGraphicContent.getPath()));
}
private void publishDeleteFileEvents(List<GraphicContent> toDeleteGraphicContents) {
for (GraphicContent toDeleteGraphicContent : toDeleteGraphicContents) {
String path = toDeleteGraphicContent.getPath();
eventPublisher.publishEvent(new GraphicDeleteEvent(this, path));
}
}

protected void deleteFeed(Long feedId) {
Feed feed = feedRepository.findById(Objects.requireNonNull(feedId))
.orElseThrow(FeedSearchException::new);
feedRepository.delete(feed);

publishDeleteFileEvents(feed.getGraphicContents());
eventPublisher.publishEvent(new FeedResponseDeleteEvent(this, feedId));
}

Expand All @@ -215,10 +219,10 @@ public void deleteFeedIfOwner(Long feedId, Long memberId) {

@Transactional(readOnly = true)
public FeedResponse getFeed(Long feedId) {
Feed feed = feedRepository.findById(Objects.requireNonNull(feedId))
Long feedIdNonNull = Objects.requireNonNull(feedId);
FeedResponseEntity feedResponseEntity = feedRepository.findFeedResponseById(feedIdNonNull)
.orElseThrow(FeedSearchException::new);
long likes = likeRepository.getCountByFeedId(feed.getId().toString());

return FeedResponse.from(feed, likes);
return toFeedResponse(feedResponseEntity);
}
}
7 changes: 5 additions & 2 deletions src/test/groovy/com/stoury/service/FeedServiceTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,22 @@ import com.stoury.repository.FeedRepository
import com.stoury.repository.LikeRepository
import com.stoury.repository.MemberRepository
import com.stoury.service.location.LocationService
import com.stoury.service.storage.StorageService
import com.stoury.utils.JsonMapper
import org.springframework.context.ApplicationEventPublisher
import org.springframework.mock.web.MockMultipartFile
import spock.lang.Specification

class FeedServiceTest extends Specification {
def storageService = Mock(StorageService)
def memberRepository = Mock(MemberRepository)
def tagService = Mock(TagService)
def feedRepository = Mock(FeedRepository)
def likeRepository = Mock(LikeRepository)
def eventPublisher = Mock(ApplicationEventPublisher)
def locationService = Mock(LocationService)
def jsonMapper = new JsonMapper(new ObjectMapper())
def feedService = new FeedService(feedRepository, memberRepository, likeRepository,
def feedService = new FeedService(storageService, feedRepository, memberRepository, likeRepository,
tagService, locationService, eventPublisher, jsonMapper)

def writer = Mock(Member)
Expand Down Expand Up @@ -132,12 +134,13 @@ class FeedServiceTest extends Specification {
def "피드 삭제 성공"() {
given:
def writer = new Member(id: 1)
def feed = new Feed(id: 1, member: writer)
def feed = new Feed(id: 1, member: writer, graphicContents: [new GraphicContent("/gc1", 0)])
feedRepository.findById(_) >> Optional.of(feed)
when:
feedService.deleteFeedIfOwner(feed.id, writer.id)
then:
1 * feedRepository.delete(_)
1 * eventPublisher.publishEvent(_ as GraphicDeleteEvent)
1 * eventPublisher.publishEvent(_ as FeedResponseDeleteEvent)
}

Expand Down

0 comments on commit 50649ba

Please sign in to comment.