diff --git a/Api/src/main/java/allchive/server/api/archiving/service/UpdateArchivingUseCase.java b/Api/src/main/java/allchive/server/api/archiving/service/UpdateArchivingUseCase.java index fa4d01e4..f4b76540 100644 --- a/Api/src/main/java/allchive/server/api/archiving/service/UpdateArchivingUseCase.java +++ b/Api/src/main/java/allchive/server/api/archiving/service/UpdateArchivingUseCase.java @@ -5,6 +5,8 @@ import allchive.server.api.common.util.UrlUtil; import allchive.server.api.config.security.SecurityUtil; import allchive.server.core.annotation.UseCase; +import allchive.server.core.event.Event; +import allchive.server.core.event.events.s3.S3ImageDeleteEvent; import allchive.server.domain.domains.archiving.adaptor.ArchivingAdaptor; import allchive.server.domain.domains.archiving.domain.Archiving; import allchive.server.domain.domains.archiving.service.ArchivingDomainService; @@ -44,7 +46,7 @@ private void eliminateOldImage(Long archivingId, String newUrl) { Archiving archiving = archivingAdaptor.findById(archivingId); if (UrlUtil.validateS3Key(archiving.getImageUrl()) && !archiving.getImageUrl().equals(UrlUtil.convertUrlToKey(newUrl))) { - s3DeleteObjectService.deleteS3Object(List.of(archiving.getImageUrl())); + Event.raise(S3ImageDeleteEvent.from(List.of(archiving.getImageUrl()))); } } } diff --git a/Api/src/main/java/allchive/server/api/content/controller/ContentController.java b/Api/src/main/java/allchive/server/api/content/controller/ContentController.java index e9357f64..f906f48d 100644 --- a/Api/src/main/java/allchive/server/api/content/controller/ContentController.java +++ b/Api/src/main/java/allchive/server/api/content/controller/ContentController.java @@ -26,8 +26,9 @@ public class ContentController { @Operation(summary = "컨텐츠를 생성합니다.") @PostMapping() - public void createContent(@RequestBody CreateContentRequest createContentRequest) { - createContentUseCase.execute(createContentRequest); + public ContentTagResponse createContent( + @RequestBody CreateContentRequest createContentRequest) { + return createContentUseCase.execute(createContentRequest); } @Operation(summary = "컨텐츠 내용을 가져옵니다.") diff --git a/Api/src/main/java/allchive/server/api/content/service/CreateContentUseCase.java b/Api/src/main/java/allchive/server/api/content/service/CreateContentUseCase.java index 3ef78545..6869ea43 100644 --- a/Api/src/main/java/allchive/server/api/content/service/CreateContentUseCase.java +++ b/Api/src/main/java/allchive/server/api/content/service/CreateContentUseCase.java @@ -4,6 +4,7 @@ import allchive.server.api.config.security.SecurityUtil; import allchive.server.api.content.model.dto.request.CreateContentRequest; +import allchive.server.api.content.model.dto.response.ContentTagResponse; import allchive.server.api.content.model.mapper.ContentMapper; import allchive.server.core.annotation.UseCase; import allchive.server.domain.domains.archiving.service.ArchivingDomainService; @@ -33,27 +34,30 @@ public class CreateContentUseCase { private final ArchivingDomainService archivingDomainService; @Transactional - public void execute(CreateContentRequest request) { - validateExecution(request); + public ContentTagResponse execute(CreateContentRequest request) { + Long userId = SecurityUtil.getCurrentUserId(); + validateExecution(userId, request); Content content = contentMapper.toEntity(request); updateTagUsedAt(request.getTagIds()); - createContentTagGroup(content, request.getTagIds()); + List contentTagGroupList = + createContentTagGroup(content, request.getTagIds()); contentDomainService.save(content); archivingDomainService.updateContentCnt( request.getArchivingId(), request.getContentType(), PLUS_ONE); + return contentMapper.toContentTagResponse(content, contentTagGroupList, true, userId); } - private void validateExecution(CreateContentRequest request) { - Long userId = SecurityUtil.getCurrentUserId(); + private void validateExecution(Long userId, CreateContentRequest request) { archivingValidator.verifyUser(userId, request.getArchivingId()); tagValidator.validateExistTagsAndUser(request.getTagIds(), userId); } - private void createContentTagGroup(Content content, List tagIds) { + private List createContentTagGroup(Content content, List tagIds) { List tags = tagAdaptor.queryTagByTagIdIn(tagIds); List contentTagGroupList = contentMapper.toContentTagGroupEntityList(content, tags); contentTagGroupDomainService.saveAll(contentTagGroupList); + return contentTagGroupList; } private void updateTagUsedAt(List tagIds) { diff --git a/Api/src/main/java/allchive/server/api/content/service/UpdateContentUseCase.java b/Api/src/main/java/allchive/server/api/content/service/UpdateContentUseCase.java index c33ffaee..a36af0d6 100644 --- a/Api/src/main/java/allchive/server/api/content/service/UpdateContentUseCase.java +++ b/Api/src/main/java/allchive/server/api/content/service/UpdateContentUseCase.java @@ -8,6 +8,8 @@ import allchive.server.api.content.model.dto.request.UpdateContentRequest; import allchive.server.api.content.model.mapper.ContentMapper; import allchive.server.core.annotation.UseCase; +import allchive.server.core.event.Event; +import allchive.server.core.event.events.s3.S3ImageDeleteEvent; import allchive.server.domain.domains.archiving.service.ArchivingDomainService; import allchive.server.domain.domains.content.adaptor.ContentAdaptor; import allchive.server.domain.domains.content.adaptor.TagAdaptor; @@ -87,7 +89,7 @@ private void eliminateOldImage(Long contentId, String newUrl) { Content content = contentAdaptor.findById(contentId); if (UrlUtil.validateS3Key(content.getImageUrl()) && !content.getImageUrl().equals(UrlUtil.convertUrlToKey(newUrl))) { - s3DeleteObjectService.deleteS3Object(List.of(content.getImageUrl())); + Event.raise(S3ImageDeleteEvent.from(List.of(content.getImageUrl()))); } } } diff --git a/Api/src/main/java/allchive/server/api/recycle/service/ClearDeletedObjectUseCase.java b/Api/src/main/java/allchive/server/api/recycle/service/ClearDeletedObjectUseCase.java index 17ce7d2c..f03a4f61 100644 --- a/Api/src/main/java/allchive/server/api/recycle/service/ClearDeletedObjectUseCase.java +++ b/Api/src/main/java/allchive/server/api/recycle/service/ClearDeletedObjectUseCase.java @@ -4,6 +4,8 @@ import allchive.server.api.config.security.SecurityUtil; import allchive.server.api.recycle.model.dto.request.ClearDeletedObjectRequest; import allchive.server.core.annotation.UseCase; +import allchive.server.core.event.Event; +import allchive.server.core.event.events.s3.S3ImageDeleteEvent; import allchive.server.domain.domains.archiving.adaptor.ArchivingAdaptor; import allchive.server.domain.domains.archiving.domain.Archiving; import allchive.server.domain.domains.archiving.service.ArchivingDomainService; @@ -73,7 +75,7 @@ private void deleteS3Object(List contents, List archivings) .filter(url -> !url.isEmpty()) .filter(url -> !url.startsWith("http")) .collect(Collectors.toList())); - s3DeleteObjectService.deleteS3Object(imageKeys); + Event.raise(S3ImageDeleteEvent.from(imageKeys)); } private List getContentsId(List contents, ClearDeletedObjectRequest request) { diff --git a/Api/src/main/java/allchive/server/api/recycle/service/ClearOldDeletedObjectUseCase.java b/Api/src/main/java/allchive/server/api/recycle/service/ClearOldDeletedObjectUseCase.java index 161e2bcf..e8eca4d2 100644 --- a/Api/src/main/java/allchive/server/api/recycle/service/ClearOldDeletedObjectUseCase.java +++ b/Api/src/main/java/allchive/server/api/recycle/service/ClearOldDeletedObjectUseCase.java @@ -2,6 +2,8 @@ import allchive.server.core.annotation.UseCase; +import allchive.server.core.event.Event; +import allchive.server.core.event.events.s3.S3ImageDeleteEvent; import allchive.server.domain.domains.archiving.adaptor.ArchivingAdaptor; import allchive.server.domain.domains.archiving.domain.Archiving; import allchive.server.domain.domains.archiving.service.ArchivingDomainService; @@ -76,7 +78,7 @@ private void deleteS3Object(List contents, List archivings) .filter(url -> !url.isEmpty()) .filter(url -> !url.startsWith("http")) .collect(Collectors.toList())); - s3DeleteObjectService.deleteS3Object(imageKeys); + Event.raise(S3ImageDeleteEvent.from(imageKeys)); } private List getArchivingIds(List recycles) { diff --git a/Api/src/main/java/allchive/server/api/user/service/UpdateUserInfoUseCase.java b/Api/src/main/java/allchive/server/api/user/service/UpdateUserInfoUseCase.java index 176cd976..dc3e8cf2 100644 --- a/Api/src/main/java/allchive/server/api/user/service/UpdateUserInfoUseCase.java +++ b/Api/src/main/java/allchive/server/api/user/service/UpdateUserInfoUseCase.java @@ -5,6 +5,8 @@ import allchive.server.api.config.security.SecurityUtil; import allchive.server.api.user.model.dto.request.UpdateUserInfoRequest; import allchive.server.core.annotation.UseCase; +import allchive.server.core.event.Event; +import allchive.server.core.event.events.s3.S3ImageDeleteEvent; import allchive.server.domain.domains.user.adaptor.UserAdaptor; import allchive.server.domain.domains.user.domain.User; import allchive.server.domain.domains.user.service.UserDomainService; @@ -43,7 +45,7 @@ private void eliminateOldImage(Long userId, String newUrl) { User user = userAdaptor.findById(userId); if (UrlUtil.validateS3Key(user.getProfileImgUrl()) && !user.getProfileImgUrl().equals(UrlUtil.convertUrlToKey(newUrl))) { - s3DeleteObjectService.deleteS3Object(List.of(user.getProfileImgUrl())); + Event.raise(S3ImageDeleteEvent.from(List.of(user.getProfileImgUrl()))); } } } diff --git a/Core/src/main/java/allchive/server/core/async/CustomAsyncExceptionHandler.java b/Core/src/main/java/allchive/server/core/async/CustomAsyncExceptionHandler.java new file mode 100644 index 00000000..b00e1389 --- /dev/null +++ b/Core/src/main/java/allchive/server/core/async/CustomAsyncExceptionHandler.java @@ -0,0 +1,23 @@ +package allchive.server.core.async; + + +import java.lang.reflect.Method; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class CustomAsyncExceptionHandler implements AsyncUncaughtExceptionHandler { + + @Override + public void handleUncaughtException(Throwable throwable, Method method, Object... params) { + log.error("Exception message - " + throwable); + log.error("Method name - " + method.getName()); + for (Object param : params) { + log.error("Parameter value - " + param); + } + } +} diff --git a/Core/src/main/java/allchive/server/core/config/EnableAsyncConfig.java b/Core/src/main/java/allchive/server/core/config/EnableAsyncConfig.java new file mode 100644 index 00000000..b80ba224 --- /dev/null +++ b/Core/src/main/java/allchive/server/core/config/EnableAsyncConfig.java @@ -0,0 +1,43 @@ +package allchive.server.core.config; + +import static allchive.server.core.consts.AllchiveConst.*; + +import allchive.server.core.async.CustomAsyncExceptionHandler; +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadPoolExecutor; +import lombok.RequiredArgsConstructor; +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.AsyncConfigurer; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +@EnableAsync +@Configuration +@RequiredArgsConstructor +public class EnableAsyncConfig implements AsyncConfigurer { + + private final CustomAsyncExceptionHandler customAsyncExceptionHandler; + + @Override + public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { + return customAsyncExceptionHandler; + } + + @Bean(name = "s3ImageTaskExecutor") + public Executor s3ImageTaskExecutor() { + return createTaskExecutor("S3_IMAGE_TASK_EXECUTOR"); + } + + private Executor createTaskExecutor(String name) { + ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); + taskExecutor.setCorePoolSize(CORE_POOL_SIZE); + taskExecutor.setMaxPoolSize(MAX_POOL_SIZE); + taskExecutor.setQueueCapacity(QUEUE_CAPACITY); + taskExecutor.setThreadNamePrefix(name); + taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + taskExecutor.initialize(); + return taskExecutor; + } +} diff --git a/Core/src/main/java/allchive/server/core/consts/AllchiveConst.java b/Core/src/main/java/allchive/server/core/consts/AllchiveConst.java index a9f7c47d..97594d87 100644 --- a/Core/src/main/java/allchive/server/core/consts/AllchiveConst.java +++ b/Core/src/main/java/allchive/server/core/consts/AllchiveConst.java @@ -46,4 +46,8 @@ public class AllchiveConst { public static final int PLUS_ONE = 1; public static final int MINUS_ONE = -1; public static final int ZERO = 0; + + public static final int CORE_POOL_SIZE = 1; + public static final int MAX_POOL_SIZE = 30; + public static final int QUEUE_CAPACITY = 500; } diff --git a/Core/src/main/java/allchive/server/core/event/Event.java b/Core/src/main/java/allchive/server/core/event/Event.java new file mode 100644 index 00000000..115e4ce8 --- /dev/null +++ b/Core/src/main/java/allchive/server/core/event/Event.java @@ -0,0 +1,18 @@ +package allchive.server.core.event; + + +import org.springframework.context.ApplicationEventPublisher; + +public class Event { + private static ApplicationEventPublisher publisher; + + static void setPublisher(ApplicationEventPublisher publisher) { + Event.publisher = publisher; + } + + public static void raise(Object event) { + if (publisher != null) { + publisher.publishEvent(event); + } + } +} diff --git a/Core/src/main/java/allchive/server/core/event/EventConfig.java b/Core/src/main/java/allchive/server/core/event/EventConfig.java new file mode 100644 index 00000000..ef152608 --- /dev/null +++ b/Core/src/main/java/allchive/server/core/event/EventConfig.java @@ -0,0 +1,18 @@ +package allchive.server.core.event; + + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class EventConfig { + @Autowired private ApplicationEventPublisher applicationEventPublisher; + + @Bean + public InitializingBean eventsInitializer() { + return () -> Event.setPublisher(applicationEventPublisher); + } +} diff --git a/Core/src/main/java/allchive/server/core/event/events/s3/S3ImageDeleteEvent.java b/Core/src/main/java/allchive/server/core/event/events/s3/S3ImageDeleteEvent.java new file mode 100644 index 00000000..c7916522 --- /dev/null +++ b/Core/src/main/java/allchive/server/core/event/events/s3/S3ImageDeleteEvent.java @@ -0,0 +1,20 @@ +package allchive.server.core.event.events.s3; + + +import java.util.List; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class S3ImageDeleteEvent { + private final List keys; + + @Builder + private S3ImageDeleteEvent(List keys) { + this.keys = keys; + } + + public static S3ImageDeleteEvent from(List keys) { + return S3ImageDeleteEvent.builder().keys(keys).build(); + } +} diff --git a/Domain/src/main/java/allchive/server/domain/domains/archiving/repository/ArchivingCustomRepositoryImpl.java b/Domain/src/main/java/allchive/server/domain/domains/archiving/repository/ArchivingCustomRepositoryImpl.java index 4c3c3ca6..7b576ea3 100644 --- a/Domain/src/main/java/allchive/server/domain/domains/archiving/repository/ArchivingCustomRepositoryImpl.java +++ b/Domain/src/main/java/allchive/server/domain/domains/archiving/repository/ArchivingCustomRepositoryImpl.java @@ -55,7 +55,7 @@ public Slice querySliceArchivingByUserId( .select(archiving) .from(archiving) .where(userIdEq(userId), categoryEq(category), deleteStatusFalse()) - .orderBy(pinDesc(userId), scrapCntDesc(), createdAtDesc()) + .orderBy(pinDesc(userId), createdAtDesc()) .offset(pageable.getOffset()) .limit(pageable.getPageSize() + PLUS_ONE) .fetch(); diff --git a/Infrastructure/src/main/java/allchive/server/infrastructure/s3/service/S3DeleteObjectService.java b/Infrastructure/src/main/java/allchive/server/infrastructure/s3/service/S3DeleteObjectService.java index c8a2c15d..2ef0854f 100644 --- a/Infrastructure/src/main/java/allchive/server/infrastructure/s3/service/S3DeleteObjectService.java +++ b/Infrastructure/src/main/java/allchive/server/infrastructure/s3/service/S3DeleteObjectService.java @@ -2,11 +2,14 @@ import allchive.server.core.error.exception.S3ObjectNotFoundException; +import allchive.server.core.event.events.s3.S3ImageDeleteEvent; import com.amazonaws.services.s3.AmazonS3; -import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; @Service @RequiredArgsConstructor @@ -16,12 +19,18 @@ public class S3DeleteObjectService { @Value("${aws.s3.bucket}") private String bucket; - public void deleteS3Object(List keys) { - keys.forEach( - key -> { - validateExistObject(key); - amazonS3.deleteObject(bucket, key); - }); + @Async(value = "s3ImageTaskExecutor") + @TransactionalEventListener( + value = S3ImageDeleteEvent.class, + phase = TransactionPhase.AFTER_COMMIT) + public void deleteS3Object(S3ImageDeleteEvent s3ImageDeleteEvent) { + s3ImageDeleteEvent + .getKeys() + .forEach( + key -> { + validateExistObject(key); + amazonS3.deleteObject(bucket, key); + }); } private void validateExistObject(String key) {