diff --git a/server/Recruit-Api/src/main/java/com/econovation/recruit/api/card/service/BoardCacheService.java b/server/Recruit-Api/src/main/java/com/econovation/recruit/api/card/service/BoardCacheService.java new file mode 100644 index 00000000..7ce64d2f --- /dev/null +++ b/server/Recruit-Api/src/main/java/com/econovation/recruit/api/card/service/BoardCacheService.java @@ -0,0 +1,21 @@ +package com.econovation.recruit.api.card.service; + +import com.econovation.recruitdomain.domains.board.domain.Board; +import com.econovation.recruitdomain.out.BoardLoadPort; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class BoardCacheService { + + private final BoardLoadPort boardLoadPort; + + @Cacheable(value = "boardsByColumnsId", key = "#columnsId") + public List getBoardByColumnsId(Integer columnsId) { + return boardLoadPort.getBoardByColumnsId(columnsId); + } + +} diff --git a/server/Recruit-Api/src/main/java/com/econovation/recruit/api/card/service/BoardService.java b/server/Recruit-Api/src/main/java/com/econovation/recruit/api/card/service/BoardService.java index c815613f..277b7a14 100644 --- a/server/Recruit-Api/src/main/java/com/econovation/recruit/api/card/service/BoardService.java +++ b/server/Recruit-Api/src/main/java/com/econovation/recruit/api/card/service/BoardService.java @@ -4,6 +4,11 @@ import com.econovation.recruit.api.card.usecase.BoardLoadUseCase; import com.econovation.recruit.api.card.usecase.BoardRegisterUseCase; +import com.econovation.recruitcommon.annotation.InvalidateCacheByCardLocation; +import com.econovation.recruitcommon.annotation.InvalidateCacheByColumnLocation; +import com.econovation.recruitcommon.annotation.InvalidateCacheByHopeField; +import com.econovation.recruitcommon.annotation.InvalidateCache; +import com.econovation.recruitcommon.annotation.InvalidateCaches; import com.econovation.recruitcommon.utils.Result; import com.econovation.recruitdomain.common.aop.redissonLock.RedissonLock; import com.econovation.recruitdomain.domains.board.domain.Board; @@ -38,6 +43,7 @@ public class BoardService implements BoardLoadUseCase, BoardRegisterUseCase { private final BoardLoadPort boardLoadPort; private final ColumnLoadPort columnLoadPort; private final ColumnRecordPort columnRecordPort; + private final BoardCacheService boardCacheService; /* @Override public Board save(Map newestLocation, String hopeField, Integer navLoc) { @@ -104,11 +110,16 @@ public Board findById(Integer id) { } @Override + @InvalidateCaches({ + @InvalidateCache(cacheName = "boardsByColumnsId", key = "#board.columnId"), + @InvalidateCache(cacheName = "boardCardsByNavigationId", key = "#board.navigationId") + }) public void execute(Board board) { boardRecordPort.save(board); } @Override + @InvalidateCache(cacheName = "boardsByColumnsId", key = "#columnId") public Board createWorkBoard(Integer columnId, Long cardId) { Columns column = columnLoadPort.findById(columnId); List boardByNavigationIdAndColumnId = @@ -160,9 +171,8 @@ public Columns createColumn(String title, Integer navigationId) { }*/ @Override + @InvalidateCacheByHopeField public void createApplicantBoard(String applicantId, String hopeField, Long cardId) { - // \"hopeField\" -> hopeField 로 변경 - hopeField = hopeField; Integer columnsId = 0; if (hopeField.equals("개발자")) { columnsId = DEVELOPER_COLUMNS_ID; @@ -197,6 +207,10 @@ public void createApplicantBoard(String applicantId, String hopeField, Long card @Override @Transactional + @InvalidateCaches({ + @InvalidateCache(cacheName = "columnsByNavigationId", key = "#navigationId"), + @InvalidateCache(cacheName = "boardCardsByNavigationId", key = "#navigationId") + }) public Columns createColumn(String title, Integer navigationId) { Columns column = Columns.builder().title(title).navigationId(navigationId).build(); @@ -244,7 +258,9 @@ public Navigation getNavigationByNavLoc(Integer navLoc) { @Override public List getBoardByColumnsIds(List columnsIds) { - return boardLoadPort.getBoardByColumnsIds(columnsIds); + return columnsIds.stream() + .flatMap(columnId -> boardCacheService.getBoardByColumnsId(columnId).stream()) + .toList(); } @Override @@ -275,6 +291,7 @@ public Result getBoardByNextBoardId(Integer boardId) { paramClassType = UpdateLocationBoardDto.class, leaseTime = 500, waitTime = 500) + @InvalidateCacheByCardLocation public void relocateCard(UpdateLocationBoardDto updateLocationBoardDto) { List invisibleBoard = List.of(1, 2, 3); // 기준 보드는 이동이 불가하다. @@ -295,6 +312,7 @@ public void relocateCard(UpdateLocationBoardDto updateLocationBoardDto) { @Override @Transactional + @InvalidateCacheByColumnLocation public void updateColumnLocation(UpdateLocationColumnDto updateLocationDto) { // 첫번째로 옮기는 경우 (nextColumnId == 0) if (updateLocationDto.getTargetColumnId().equals(0)) { @@ -336,6 +354,7 @@ public void updateColumnLocation(UpdateLocationColumnDto updateLocationDto) { } @Override + @InvalidateCache(cacheName = "boardsByColumnsId", key = "#board.columnId") public void delete(Board board) { boardRecordPort.delete(board); } diff --git a/server/Recruit-Api/src/main/java/com/econovation/recruit/api/card/service/CardService.java b/server/Recruit-Api/src/main/java/com/econovation/recruit/api/card/service/CardService.java index b8419cbb..86366eb4 100644 --- a/server/Recruit-Api/src/main/java/com/econovation/recruit/api/card/service/CardService.java +++ b/server/Recruit-Api/src/main/java/com/econovation/recruit/api/card/service/CardService.java @@ -7,6 +7,9 @@ import com.econovation.recruit.api.card.usecase.CardRegisterUseCase; import com.econovation.recruit.api.card.usecase.ColumnsUseCase; import com.econovation.recruit.api.config.security.SecurityUtils; +import com.econovation.recruitcommon.annotation.InvalidateCacheByCardId; +import com.econovation.recruitcommon.annotation.InvalidateCacheByCreateWorkCard; +import com.econovation.recruitcommon.annotation.InvalidateCacheByUpdateWorkCard; import com.econovation.recruitcommon.utils.Result; import com.econovation.recruitdomain.common.aop.domainEvent.Events; import com.econovation.recruitdomain.common.events.WorkCardDeletedEvent; @@ -32,6 +35,8 @@ import java.util.Optional; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -56,6 +61,7 @@ public List findAll() { @Override @Transactional(readOnly = true) + @Cacheable(value = "boardCardsByNavigationId", key = "#navigationId") public List getByNavigationId(Integer navigationId, Integer year) { Long userId = SecurityUtils.getCurrentUserId(); List columns = columnsUseCase.getByNavigationId(navigationId); @@ -153,6 +159,7 @@ public CardResponseDto findCardById(Long cardId) { @Override @Transactional + @InvalidateCacheByCardId public void deleteById(Long cardId) { Board board = boardLoadUseCase.getBoardByCardId(cardId); Result prevBoard = boardLoadUseCase.getBoardByNextBoardId(board.getId()); @@ -173,6 +180,7 @@ public void deleteById(Long cardId) { @Override @Transactional + @InvalidateCacheByCreateWorkCard public void saveWorkCard(CreateWorkCardDto createWorkCardDto) { Card card = Card.builder() @@ -186,6 +194,7 @@ public void saveWorkCard(CreateWorkCardDto createWorkCardDto) { @Override @Transactional + @InvalidateCacheByUpdateWorkCard public void update(Long cardId, UpdateWorkCardDto updateWorkCardDto) { Card card = cardLoadPort.findById(cardId); // 단 title 이 null일 수도 있고, content가 null일 수도 있다. diff --git a/server/Recruit-Api/src/main/java/com/econovation/recruit/api/card/service/ColumnService.java b/server/Recruit-Api/src/main/java/com/econovation/recruit/api/card/service/ColumnService.java index b38f14c9..821be404 100644 --- a/server/Recruit-Api/src/main/java/com/econovation/recruit/api/card/service/ColumnService.java +++ b/server/Recruit-Api/src/main/java/com/econovation/recruit/api/card/service/ColumnService.java @@ -6,6 +6,7 @@ import com.econovation.recruitdomain.out.ColumnRecordPort; import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; @Service @@ -14,6 +15,7 @@ public class ColumnService implements ColumnsUseCase { private final ColumnRecordPort columnRecordPort; private final ColumnLoadPort columnLoadPort; + @Cacheable(value = "columnsByNavigationId", key = "#navigationId") public List getByNavigationId(Integer navigationId) { return columnLoadPort.getColumnByNavigationId(navigationId); } diff --git a/server/Recruit-Api/src/main/java/com/econovation/recruit/api/comment/service/CommentService.java b/server/Recruit-Api/src/main/java/com/econovation/recruit/api/comment/service/CommentService.java index 5e837349..13d57b1d 100644 --- a/server/Recruit-Api/src/main/java/com/econovation/recruit/api/comment/service/CommentService.java +++ b/server/Recruit-Api/src/main/java/com/econovation/recruit/api/comment/service/CommentService.java @@ -2,6 +2,9 @@ import com.econovation.recruit.api.comment.usecase.CommentUseCase; import com.econovation.recruit.api.config.security.SecurityUtils; +import com.econovation.recruitcommon.annotation.InvalidateCacheByCreateComment; +import com.econovation.recruitcommon.annotation.InvalidateCacheByCommentId; +import com.econovation.recruitcommon.annotation.InvalidateCacheByDeleteComment; import com.econovation.recruitcommon.utils.Result; import com.econovation.recruitdomain.common.aop.redissonLock.RedissonLock; import com.econovation.recruitdomain.domains.card.domain.Card; @@ -24,6 +27,7 @@ import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.NotNull; +import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -39,6 +43,7 @@ public class CommentService implements CommentUseCase { @Override @Transactional + @InvalidateCacheByCreateComment public Comment saveComment(CommentRegisterDto commentDto) { Long userId = SecurityUtils.getCurrentUserId(); // applicantId null 이면 "" 으로 바꿔준다. cardId 가 null 이면 0 으로 바꿔준다. @@ -81,6 +86,7 @@ private Comment convertComment(CommentRegisterDto commentDto, Long userId) { @Override @Transactional + @InvalidateCacheByCommentId public void deleteComment(Long commentId) { Long idpId = SecurityUtils.getCurrentUserId(); Comment comment = commentLoadPort.findById(commentId); @@ -117,6 +123,7 @@ public Comment findById(Long commentId) { @Override @RedissonLock(LockName = "댓글좋아요", identifier = "commentId") @Transactional + @InvalidateCacheByCommentId public void createCommentLike(Long commentId) { // 기존에 눌렀으면 취소 처리 Long idpId = SecurityUtils.getCurrentUserId(); @@ -145,6 +152,7 @@ private void createCommentLike(Comment comment, Long idpId) { @Override @RedissonLock(LockName = "댓글좋아요", identifier = "commentId") @Transactional + @InvalidateCacheByCommentId public void deleteCommentLike(Long commentId) { // 현재 내가 눌렀던 댓글만 삭제할 수 있다. Long idpId = SecurityUtils.getCurrentUserId(); @@ -217,6 +225,7 @@ public Boolean isCheckedLike(Long commentId) { @Override @Transactional + @InvalidateCacheByCommentId public void updateCommentContent(Long commentId, Map contents) { String content = contents.get("content"); // 내가 작성한 comment 만 수정할 수 있다. @@ -230,6 +239,7 @@ public void updateCommentContent(Long commentId, Map contents) { // @Override @Transactional(readOnly = true) + @Cacheable(value = "commentsByApplicantId", key = "#applicantId") public List findByApplicantId(String applicantId) { Long idpId = SecurityUtils.getCurrentUserId(); List comments = commentLoadPort.findByApplicantId(applicantId); @@ -238,6 +248,7 @@ public List findByApplicantId(String applicantId) { @Override @Transactional + @InvalidateCacheByDeleteComment public void deleteCommentByCardId(Long cardId) { List comments = commentLoadPort.findByCardId(cardId); if (comments.isEmpty()) { diff --git a/server/Recruit-Api/src/main/java/com/econovation/recruit/api/config/ServletFilterConfig.java b/server/Recruit-Api/src/main/java/com/econovation/recruit/api/config/ServletFilterConfig.java index 47a24be8..9d8fd288 100644 --- a/server/Recruit-Api/src/main/java/com/econovation/recruit/api/config/ServletFilterConfig.java +++ b/server/Recruit-Api/src/main/java/com/econovation/recruit/api/config/ServletFilterConfig.java @@ -9,6 +9,7 @@ import org.springframework.context.annotation.Profile; import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer; import org.springframework.web.filter.ForwardedHeaderFilter; +import org.springframework.web.filter.ShallowEtagHeaderFilter; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.resource.ResourceUrlEncodingFilter; @@ -23,18 +24,18 @@ public class ServletFilterConfig implements WebMvcConfigurer { @Bean public FilterRegistrationBean securityFilterChain( @Qualifier(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME) - Filter securityFilter) { - FilterRegistrationBean registration = new FilterRegistrationBean(securityFilter); - registration.setOrder(Integer.MAX_VALUE - 3); - registration.setName(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME); - return registration; + Filter securityFilter) { + FilterRegistrationBean registrationBean = new FilterRegistrationBean(securityFilter); + registrationBean.setOrder(Integer.MAX_VALUE - 5); + registrationBean.setName(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME); + return registrationBean; } @Bean public FilterRegistrationBean setResourceUrlEncodingFilter() { FilterRegistrationBean registrationBean = new FilterRegistrationBean(); registrationBean.setFilter(new ResourceUrlEncodingFilter()); - registrationBean.setOrder(Integer.MAX_VALUE - 2); + registrationBean.setOrder(Integer.MAX_VALUE - 4); return registrationBean; } @@ -42,7 +43,7 @@ public FilterRegistrationBean setResourceUrlEncodingFilter() { public FilterRegistrationBean setForwardedHeaderFilterOrder() { FilterRegistrationBean registrationBean = new FilterRegistrationBean(); registrationBean.setFilter(forwardedHeaderFilter); - registrationBean.setOrder(Integer.MAX_VALUE - 1); + registrationBean.setOrder(Integer.MAX_VALUE - 3); return registrationBean; } @@ -50,7 +51,16 @@ public FilterRegistrationBean setForwardedHeaderFilterOrder() { public FilterRegistrationBean setHttpContentCacheFilterOrder() { FilterRegistrationBean registrationBean = new FilterRegistrationBean(); registrationBean.setFilter(httpContentCacheFilter); - registrationBean.setOrder(Integer.MAX_VALUE); + registrationBean.setOrder(Integer.MAX_VALUE - 2); + return registrationBean; + } + + @Bean + public FilterRegistrationBean shallowEtagHeaderFilter() { + FilterRegistrationBean registrationBean + = new FilterRegistrationBean<>(new ShallowEtagHeaderFilter()); + registrationBean.setOrder(Integer.MAX_VALUE - 1); + registrationBean.addUrlPatterns("/*"); return registrationBean; } } diff --git a/server/Recruit-Api/src/main/java/com/econovation/recruit/api/config/WebMvcConfig.java b/server/Recruit-Api/src/main/java/com/econovation/recruit/api/config/WebMvcConfig.java index 04192b4e..e3e8d7b0 100644 --- a/server/Recruit-Api/src/main/java/com/econovation/recruit/api/config/WebMvcConfig.java +++ b/server/Recruit-Api/src/main/java/com/econovation/recruit/api/config/WebMvcConfig.java @@ -2,7 +2,10 @@ import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; +import org.springframework.http.CacheControl; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.mvc.WebContentInterceptor; @Configuration @RequiredArgsConstructor @@ -23,4 +26,14 @@ public class WebMvcConfig implements WebMvcConfigurer { // // registrationBean.setUrlPatterns(Arrays.asList("/api/v1/*")); // return registrationBean;3 // } + + @Override + public void addInterceptors(final InterceptorRegistry registry) { + CacheControl cacheControl = CacheControl.noCache().mustRevalidate(); + + WebContentInterceptor webContentInterceptor = new WebContentInterceptor(); + webContentInterceptor.addCacheMapping(cacheControl, "/**"); + + registry.addInterceptor(webContentInterceptor); + } } diff --git a/server/Recruit-Api/src/main/java/com/econovation/recruit/api/label/service/LabelService.java b/server/Recruit-Api/src/main/java/com/econovation/recruit/api/label/service/LabelService.java index fe66249e..b6db61dc 100644 --- a/server/Recruit-Api/src/main/java/com/econovation/recruit/api/label/service/LabelService.java +++ b/server/Recruit-Api/src/main/java/com/econovation/recruit/api/label/service/LabelService.java @@ -18,6 +18,7 @@ import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.NotNull; +import org.springframework.cache.annotation.CacheEvict; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -36,6 +37,7 @@ public class LabelService implements LabelUseCase { waitTime = 1000L, leaseTime = 1000L) @Transactional + @CacheEvict(value = "boardCardsByNavigationId", allEntries = true) public Boolean createLabel(String applicantId) { Long idpId = SecurityUtils.getCurrentUserId(); Card card = cardLoadPort.findByApplicantId(applicantId); @@ -67,6 +69,7 @@ public List findByApplicantId(String applicantId) { identifier = "applicantId", waitTime = 1000L, leaseTime = 1000L) + @CacheEvict(value = "boardCardsByNavigationId", allEntries = true) public void deleteLabel(String applicantId) { Long idpId = SecurityUtils.getCurrentUserId(); Label label = labelLoadPort.loadLabelByApplicantIdAndIdpId(applicantId, idpId); @@ -87,6 +90,7 @@ public void deleteLabelByCardId(Long cardId) { @Override @Transactional + @CacheEvict(value = "boardCardsByNavigationId", allEntries = true) public Boolean createLabelByCardId(Long cardId) { Long idpId = SecurityUtils.getCurrentUserId(); Card card = cardLoadPort.findById(cardId); diff --git a/server/Recruit-Api/src/main/java/com/econovation/recruit/api/record/service/RecordService.java b/server/Recruit-Api/src/main/java/com/econovation/recruit/api/record/service/RecordService.java index 0ddab0ea..a8fccb9d 100644 --- a/server/Recruit-Api/src/main/java/com/econovation/recruit/api/record/service/RecordService.java +++ b/server/Recruit-Api/src/main/java/com/econovation/recruit/api/record/service/RecordService.java @@ -20,6 +20,8 @@ import java.util.*; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -34,6 +36,7 @@ public class RecordService implements RecordUseCase { @Override @Transactional + @CacheEvict(value = "recordsByPage", allEntries = true) public Record createRecord(CreateRecordDto recordDto) { if (applicantQueryUseCase.execute(recordDto.getApplicantId()) == null) { throw ApplicantNotFoundException.EXCEPTION; @@ -57,6 +60,7 @@ public List findAll() { * Records를 모두 조회합니다 ) */ @Override + @Cacheable(value = "recordsByPage") public RecordsViewResponseDto execute(Integer page, Integer year, String sortType) { List result = recordLoadPort.findAll(page); PageInfo pageInfo = getPageInfo(page); @@ -145,6 +149,7 @@ public Record findByApplicantId(String applicantId) { @Override @Transactional + @CacheEvict(value = "recordsByPage", allEntries = true) public void updateRecordUrl(String applicantId, String url) { recordLoadPort .findByApplicantId(applicantId) @@ -156,6 +161,7 @@ record -> { @Override @Transactional + @CacheEvict(value = "recordsByPage", allEntries = true) public void updateRecordContents(String applicantId, String contents) { recordLoadPort .findByApplicantId(applicantId) @@ -167,6 +173,7 @@ record -> { @Override @Transactional + @CacheEvict(value = "recordsByPage", allEntries = true) public void updateRecord(String applicantId, UpdateRecordDto updateRecordDto) { recordLoadPort .findByApplicantId(applicantId) diff --git a/server/Recruit-Api/src/main/java/com/econovation/recruit/utils/SpELParser.java b/server/Recruit-Api/src/main/java/com/econovation/recruit/utils/SpELParser.java new file mode 100644 index 00000000..964a870a --- /dev/null +++ b/server/Recruit-Api/src/main/java/com/econovation/recruit/utils/SpELParser.java @@ -0,0 +1,26 @@ +package com.econovation.recruit.utils; + +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +public class SpELParser { + public static Object getDynamicValue(JoinPoint joinPoint, String name) { + ExpressionParser parser = new SpelExpressionParser(); + EvaluationContext context = new StandardEvaluationContext(); + + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + String[] parameterNames = signature.getParameterNames(); + Object[] args = joinPoint.getArgs(); + + for (int i = 0; i < parameterNames.length; i++) { + context.setVariable(parameterNames[i], args[i]); + } + + return parser.parseExpression(name).getValue(context); + } + +} diff --git a/server/Recruit-Api/src/main/java/com/econovation/recruit/utils/aop/CacheEvictAspect.java b/server/Recruit-Api/src/main/java/com/econovation/recruit/utils/aop/CacheEvictAspect.java new file mode 100644 index 00000000..66289efa --- /dev/null +++ b/server/Recruit-Api/src/main/java/com/econovation/recruit/utils/aop/CacheEvictAspect.java @@ -0,0 +1,189 @@ +package com.econovation.recruit.utils.aop; + +import static com.econovation.recruitcommon.consts.RecruitStatic.DESIGNER_COLUMNS_ID; +import static com.econovation.recruitcommon.consts.RecruitStatic.DEVELOPER_COLUMNS_ID; +import static com.econovation.recruitcommon.consts.RecruitStatic.PLANNER_COLUMNS_ID; + +import com.econovation.recruit.utils.SpELParser; +import com.econovation.recruitcommon.annotation.InvalidateCache; +import com.econovation.recruitcommon.annotation.InvalidateCacheByCardId; +import com.econovation.recruitcommon.annotation.InvalidateCacheByCardLocation; +import com.econovation.recruitcommon.annotation.InvalidateCacheByColumnLocation; +import com.econovation.recruitcommon.annotation.InvalidateCacheByCreateComment; +import com.econovation.recruitcommon.annotation.InvalidateCacheByCreateWorkCard; +import com.econovation.recruitcommon.annotation.InvalidateCacheByCommentId; +import com.econovation.recruitcommon.annotation.InvalidateCacheByDeleteComment; +import com.econovation.recruitcommon.annotation.InvalidateCacheByHopeField; +import com.econovation.recruitcommon.annotation.InvalidateCacheByUpdateWorkCard; +import com.econovation.recruitcommon.annotation.InvalidateCaches; +import com.econovation.recruitdomain.domains.board.domain.Board; +import com.econovation.recruitdomain.domains.board.domain.Columns; +import com.econovation.recruitdomain.domains.card.domain.Card; +import com.econovation.recruitdomain.domains.comment.domain.Comment; +import com.econovation.recruitdomain.domains.dto.CommentRegisterDto; +import com.econovation.recruitdomain.domains.dto.CreateWorkCardDto; +import com.econovation.recruitdomain.domains.dto.UpdateLocationBoardDto; +import com.econovation.recruitdomain.domains.dto.UpdateLocationColumnDto; +import com.econovation.recruitdomain.out.BoardLoadPort; +import com.econovation.recruitdomain.out.CardLoadPort; +import com.econovation.recruitdomain.out.ColumnLoadPort; +import com.econovation.recruitdomain.out.CommentLoadPort; +import lombok.RequiredArgsConstructor; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Component; + +@Aspect +@Component +@RequiredArgsConstructor +public class CacheEvictAspect { + + private final String BOARDS_BY_COLUMNS_ID = "boardsByColumnsId"; + private final String COLUMNS_BY_NAVIGATION_ID = "columnsByNavigationId"; + private final String BOARD_CARDS_BY_NAVIGATION_ID = "boardCardsByNavigationId"; + private final String COMMENTS_BY_APPLICANT_ID = "commentsByApplicantId"; + + private final CacheManager cacheManager; + private final CardLoadPort cardLoadPort; + private final BoardLoadPort boardLoadPort; + private final ColumnLoadPort columnLoadPort; + private final CommentLoadPort commentLoadPort; + + @Before("@annotation(invalidateCaches)") + public void invalidateCaches(JoinPoint joinPoint, InvalidateCaches invalidateCaches) { + InvalidateCache[] value = invalidateCaches.value(); + for (InvalidateCache invalidateCache : value) { + invalidateCache(joinPoint, invalidateCache); + } + } + + @Before("@annotation(invalidateCache)") + public void invalidateCache(JoinPoint joinPoint, InvalidateCache invalidateCache) { + String cacheName = invalidateCache.cacheName(); + String key = invalidateCache.key(); + + if (key.isEmpty()) { + evictCache(cacheName, null); + return; + } + String parsedKey = (String) SpELParser.getDynamicValue(joinPoint, invalidateCache.key()); + evictCache(cacheName, parsedKey); + } + + private void evictCache(String cacheName, String key) { + Cache cache = cacheManager.getCache(cacheName); + if (cache == null) { + return; + } + + if (key == null) { + cache.clear(); + } else { + cache.evictIfPresent(key); + } + } + + @Before("@annotation(invalidateCacheByHopeField) && args(*, hopeField, ..)") + public void invalidateCacheByHopeField(InvalidateCacheByHopeField invalidateCacheByHopeField, String hopeField) { + Integer columnsId = 0; + switch (hopeField) { + case "개발자" -> columnsId = DEVELOPER_COLUMNS_ID; + case "디자이너" -> columnsId = DESIGNER_COLUMNS_ID; + case "기획자" -> columnsId = PLANNER_COLUMNS_ID; + default -> { + } + } + + evictCache(BOARDS_BY_COLUMNS_ID, columnsId.toString()); + evictCache(BOARD_CARDS_BY_NAVIGATION_ID, "1"); + } + + @Before("@annotation(invalidateCacheByColumnLocation) && args(updateLocationDto)") + public void invalidateCachetByColumnLocation(InvalidateCacheByColumnLocation invalidateCacheByColumnLocation, UpdateLocationColumnDto updateLocationDto) { + // Column 이동은 동일한 navigation에서 이루어지므로 하나만 조회해서 navigationId를 가져오고 무효화 + Columns column = columnLoadPort.findById(updateLocationDto.getColumnId()); + Integer navigationId = column.getNavigationId(); + + evictCache(COLUMNS_BY_NAVIGATION_ID, navigationId.toString()); + evictCache(BOARD_CARDS_BY_NAVIGATION_ID, navigationId.toString()); + } + + @Before("@annotation(invalidateCacheByCardLocation) && args(updateLocationDto)") + public void invalidateCachetByCardLocation(InvalidateCacheByCardLocation invalidateCacheByCardLocation, UpdateLocationBoardDto updateLocationDto) { + // Board 이동은 동일한 navigation에서 이루어지므로 하나만 조회해서 navigationId를 가져오고 무효화 + // Column에 해당하는 Board가 달라지므로 current, target 둘 다 조회해서 columnId를 가져오고 무효화 + + Board currentBoard = boardLoadPort.getBoardById(updateLocationDto.getBoardId()); + Board targetBoard = boardLoadPort.getBoardById(updateLocationDto.getTargetBoardId()); + + Integer navigationId = currentBoard.getNavigationId(); + + evictCache(BOARDS_BY_COLUMNS_ID, currentBoard.getColumnId().toString()); + evictCache(BOARDS_BY_COLUMNS_ID, targetBoard.getColumnId().toString()); + evictCache(BOARD_CARDS_BY_NAVIGATION_ID, navigationId.toString()); + } + + @Before("@annotation(invalidateCacheByCardId)") + public void invalidateCacheByCardId(JoinPoint joinPoint, InvalidateCacheByCardId invalidateCacheByCardId) { + Long cardId = (Long) joinPoint.getArgs()[0]; + Board board = boardLoadPort.getBoardByCardId(cardId); + + evictCache(BOARD_CARDS_BY_NAVIGATION_ID, board.getNavigationId().toString()); + } + + @Before("@annotation(invalidateCacheByCreateWorkCard) && args(createWorkCardDto)") + public void invalidateCacheByCreateWorkCard(InvalidateCacheByCreateWorkCard invalidateCacheByCreateWorkCard, CreateWorkCardDto createWorkCardDto) { + Columns column = columnLoadPort.findById(createWorkCardDto.getColumnId()); + + evictCache(BOARD_CARDS_BY_NAVIGATION_ID, column.getNavigationId().toString()); + } + + @Before("@annotation(invalidateCacheByUpdateWorkCard) && args(cardId, ..)") + public void invalidateCacheByUpdateWorkCard(InvalidateCacheByUpdateWorkCard invalidateCacheByUpdateWorkCard, Long cardId) { + Board board = boardLoadPort.getBoardByCardId(cardId); + + evictCache(BOARD_CARDS_BY_NAVIGATION_ID, board.getNavigationId().toString()); + } + + @Before("@annotation(invalidateCacheByCreateComment) && args(commentDto)") + public void invalidateCacheByCreateComment(InvalidateCacheByCreateComment invalidateCacheByCreateComment, CommentRegisterDto commentDto) { + // 업무카드 댓글 생성 요청 + if (commentDto.getApplicantId() == null) { + Board board = boardLoadPort.getBoardByCardId(commentDto.getCardId()); + evictCache(BOARD_CARDS_BY_NAVIGATION_ID, board.getNavigationId().toString()); + return; + } + + // 지원서카드 댓글 생성 요청 + Card card = cardLoadPort.findByApplicantId(commentDto.getApplicantId()); + Board board = boardLoadPort.getBoardByCardId(card.getId()); + + // Card의 comment count를 변경하므로 무효화 + evictCache(BOARD_CARDS_BY_NAVIGATION_ID, board.getNavigationId().toString()); + // 지원자의 댓글 목록 조회 캐시 무효화 + evictCache(COMMENTS_BY_APPLICANT_ID, commentDto.getApplicantId()); + } + + @Before("@annotation(invalidateCacheByDeleteComment) && args(cardId)") + public void invalidateCacheByDeleteComment(InvalidateCacheByDeleteComment invalidateCacheByDeleteComment, Long cardId) { + Board board = boardLoadPort.getBoardByCardId(cardId); + Card card = cardLoadPort.findById(cardId); + + // 댓글 삭제 시 업무카드의 댓글 수 변경으로 인한 무효화 + evictCache(BOARD_CARDS_BY_NAVIGATION_ID, board.getNavigationId().toString()); + } + + @Before("@annotation(invalidateCacheByCommentId) && args(commentId, ..)") + public void invalidateCacheByCommentId(InvalidateCacheByCommentId invalidateCacheByCommentId, Long commentId) { + Comment comment = commentLoadPort.findById(commentId); + + // 지원서 카드의 댓글인 경우 캐시 무효화 + if (comment.getApplicantId() != null) { + evictCache(COMMENTS_BY_APPLICANT_ID, comment.getApplicantId()); + } + } + +} diff --git a/server/Recruit-Common/src/main/java/com/econovation/recruitcommon/annotation/InvalidateCache.java b/server/Recruit-Common/src/main/java/com/econovation/recruitcommon/annotation/InvalidateCache.java new file mode 100644 index 00000000..d3b7b325 --- /dev/null +++ b/server/Recruit-Common/src/main/java/com/econovation/recruitcommon/annotation/InvalidateCache.java @@ -0,0 +1,15 @@ +package com.econovation.recruitcommon.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface InvalidateCache { + + String cacheName() default ""; + + String key() default ""; +} diff --git a/server/Recruit-Common/src/main/java/com/econovation/recruitcommon/annotation/InvalidateCacheByCardId.java b/server/Recruit-Common/src/main/java/com/econovation/recruitcommon/annotation/InvalidateCacheByCardId.java new file mode 100644 index 00000000..5e83dd0b --- /dev/null +++ b/server/Recruit-Common/src/main/java/com/econovation/recruitcommon/annotation/InvalidateCacheByCardId.java @@ -0,0 +1,12 @@ +package com.econovation.recruitcommon.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface InvalidateCacheByCardId { + +} diff --git a/server/Recruit-Common/src/main/java/com/econovation/recruitcommon/annotation/InvalidateCacheByCardLocation.java b/server/Recruit-Common/src/main/java/com/econovation/recruitcommon/annotation/InvalidateCacheByCardLocation.java new file mode 100644 index 00000000..f000c512 --- /dev/null +++ b/server/Recruit-Common/src/main/java/com/econovation/recruitcommon/annotation/InvalidateCacheByCardLocation.java @@ -0,0 +1,12 @@ +package com.econovation.recruitcommon.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface InvalidateCacheByCardLocation { + +} diff --git a/server/Recruit-Common/src/main/java/com/econovation/recruitcommon/annotation/InvalidateCacheByColumnLocation.java b/server/Recruit-Common/src/main/java/com/econovation/recruitcommon/annotation/InvalidateCacheByColumnLocation.java new file mode 100644 index 00000000..ab211c70 --- /dev/null +++ b/server/Recruit-Common/src/main/java/com/econovation/recruitcommon/annotation/InvalidateCacheByColumnLocation.java @@ -0,0 +1,12 @@ +package com.econovation.recruitcommon.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface InvalidateCacheByColumnLocation { + +} diff --git a/server/Recruit-Common/src/main/java/com/econovation/recruitcommon/annotation/InvalidateCacheByCommentId.java b/server/Recruit-Common/src/main/java/com/econovation/recruitcommon/annotation/InvalidateCacheByCommentId.java new file mode 100644 index 00000000..f6ce799c --- /dev/null +++ b/server/Recruit-Common/src/main/java/com/econovation/recruitcommon/annotation/InvalidateCacheByCommentId.java @@ -0,0 +1,12 @@ +package com.econovation.recruitcommon.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface InvalidateCacheByCommentId { + +} diff --git a/server/Recruit-Common/src/main/java/com/econovation/recruitcommon/annotation/InvalidateCacheByCreateComment.java b/server/Recruit-Common/src/main/java/com/econovation/recruitcommon/annotation/InvalidateCacheByCreateComment.java new file mode 100644 index 00000000..d747b1e2 --- /dev/null +++ b/server/Recruit-Common/src/main/java/com/econovation/recruitcommon/annotation/InvalidateCacheByCreateComment.java @@ -0,0 +1,12 @@ +package com.econovation.recruitcommon.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface InvalidateCacheByCreateComment { + +} diff --git a/server/Recruit-Common/src/main/java/com/econovation/recruitcommon/annotation/InvalidateCacheByCreateWorkCard.java b/server/Recruit-Common/src/main/java/com/econovation/recruitcommon/annotation/InvalidateCacheByCreateWorkCard.java new file mode 100644 index 00000000..f2330a0c --- /dev/null +++ b/server/Recruit-Common/src/main/java/com/econovation/recruitcommon/annotation/InvalidateCacheByCreateWorkCard.java @@ -0,0 +1,12 @@ +package com.econovation.recruitcommon.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface InvalidateCacheByCreateWorkCard { + +} diff --git a/server/Recruit-Common/src/main/java/com/econovation/recruitcommon/annotation/InvalidateCacheByDeleteComment.java b/server/Recruit-Common/src/main/java/com/econovation/recruitcommon/annotation/InvalidateCacheByDeleteComment.java new file mode 100644 index 00000000..08ad5181 --- /dev/null +++ b/server/Recruit-Common/src/main/java/com/econovation/recruitcommon/annotation/InvalidateCacheByDeleteComment.java @@ -0,0 +1,12 @@ +package com.econovation.recruitcommon.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface InvalidateCacheByDeleteComment { + +} diff --git a/server/Recruit-Common/src/main/java/com/econovation/recruitcommon/annotation/InvalidateCacheByHopeField.java b/server/Recruit-Common/src/main/java/com/econovation/recruitcommon/annotation/InvalidateCacheByHopeField.java new file mode 100644 index 00000000..0e2309e5 --- /dev/null +++ b/server/Recruit-Common/src/main/java/com/econovation/recruitcommon/annotation/InvalidateCacheByHopeField.java @@ -0,0 +1,12 @@ +package com.econovation.recruitcommon.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface InvalidateCacheByHopeField { + +} diff --git a/server/Recruit-Common/src/main/java/com/econovation/recruitcommon/annotation/InvalidateCacheByUpdateWorkCard.java b/server/Recruit-Common/src/main/java/com/econovation/recruitcommon/annotation/InvalidateCacheByUpdateWorkCard.java new file mode 100644 index 00000000..c6296c8f --- /dev/null +++ b/server/Recruit-Common/src/main/java/com/econovation/recruitcommon/annotation/InvalidateCacheByUpdateWorkCard.java @@ -0,0 +1,12 @@ +package com.econovation.recruitcommon.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface InvalidateCacheByUpdateWorkCard { + +} diff --git a/server/Recruit-Common/src/main/java/com/econovation/recruitcommon/annotation/InvalidateCaches.java b/server/Recruit-Common/src/main/java/com/econovation/recruitcommon/annotation/InvalidateCaches.java new file mode 100644 index 00000000..b7b51ef8 --- /dev/null +++ b/server/Recruit-Common/src/main/java/com/econovation/recruitcommon/annotation/InvalidateCaches.java @@ -0,0 +1,13 @@ +package com.econovation.recruitcommon.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface InvalidateCaches { + + InvalidateCache[] value() default {}; +} diff --git a/server/Recruit-Domain/src/main/java/com/econovation/recruitdomain/domains/BaseTimeEntity.java b/server/Recruit-Domain/src/main/java/com/econovation/recruitdomain/domains/BaseTimeEntity.java index 24294388..8cd56420 100644 --- a/server/Recruit-Domain/src/main/java/com/econovation/recruitdomain/domains/BaseTimeEntity.java +++ b/server/Recruit-Domain/src/main/java/com/econovation/recruitdomain/domains/BaseTimeEntity.java @@ -1,5 +1,9 @@ package com.econovation.recruitdomain.domains; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import java.time.LocalDateTime; import javax.persistence.EntityListeners; import javax.persistence.MappedSuperclass; @@ -12,8 +16,16 @@ @MappedSuperclass @EntityListeners(AuditingEntityListener.class) public class BaseTimeEntity { - @CreatedDate private LocalDateTime createdAt; - @LastModifiedDate private LocalDateTime updatedAt; + + @JsonSerialize(using = LocalDateTimeSerializer.class) + @JsonDeserialize(using = LocalDateTimeDeserializer.class) + @CreatedDate + private LocalDateTime createdAt; + + @JsonSerialize(using = LocalDateTimeSerializer.class) + @JsonDeserialize(using = LocalDateTimeDeserializer.class) + @LastModifiedDate + private LocalDateTime updatedAt; public BaseTimeEntity() { this.createdAt = LocalDateTime.now(); diff --git a/server/Recruit-Domain/src/main/java/com/econovation/recruitdomain/domains/board/adaptor/BoardAdaptor.java b/server/Recruit-Domain/src/main/java/com/econovation/recruitdomain/domains/board/adaptor/BoardAdaptor.java index 91d4df55..14f99079 100644 --- a/server/Recruit-Domain/src/main/java/com/econovation/recruitdomain/domains/board/adaptor/BoardAdaptor.java +++ b/server/Recruit-Domain/src/main/java/com/econovation/recruitdomain/domains/board/adaptor/BoardAdaptor.java @@ -63,6 +63,11 @@ public List getBoardByColumnsIds(List columnsIds) { return boardRepository.findByColumnIdIn(columnsIds); } + @Override + public List getBoardByColumnsId(Integer columnsId) { + return boardRepository.findByColumnId(columnsId); + } + @Override public Board getBoardByCardId(Long cardId) { Optional board = boardRepository.findByCardId(cardId); diff --git a/server/Recruit-Domain/src/main/java/com/econovation/recruitdomain/domains/board/domain/BoardRepository.java b/server/Recruit-Domain/src/main/java/com/econovation/recruitdomain/domains/board/domain/BoardRepository.java index 95264bc7..147651b2 100644 --- a/server/Recruit-Domain/src/main/java/com/econovation/recruitdomain/domains/board/domain/BoardRepository.java +++ b/server/Recruit-Domain/src/main/java/com/econovation/recruitdomain/domains/board/domain/BoardRepository.java @@ -16,5 +16,7 @@ public interface BoardRepository extends JpaRepository { List findByColumnIdIn(List columnsIds); + List findByColumnId(Integer columnId); + Optional findByCardId(Long cardId); } diff --git a/server/Recruit-Domain/src/main/java/com/econovation/recruitdomain/domains/card/dto/BoardCardResponseDto.java b/server/Recruit-Domain/src/main/java/com/econovation/recruitdomain/domains/card/dto/BoardCardResponseDto.java index 1ab2876b..d68cf903 100644 --- a/server/Recruit-Domain/src/main/java/com/econovation/recruitdomain/domains/card/dto/BoardCardResponseDto.java +++ b/server/Recruit-Domain/src/main/java/com/econovation/recruitdomain/domains/card/dto/BoardCardResponseDto.java @@ -3,13 +3,17 @@ import com.econovation.recruitdomain.domains.board.domain.Board; import com.econovation.recruitdomain.domains.board.domain.CardType; import com.econovation.recruitdomain.domains.card.domain.Card; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.Getter; +import lombok.NoArgsConstructor; @Getter @Data +@NoArgsConstructor @Builder +@AllArgsConstructor public class BoardCardResponseDto { private Long id; private Integer boardId; diff --git a/server/Recruit-Domain/src/main/java/com/econovation/recruitdomain/out/BoardLoadPort.java b/server/Recruit-Domain/src/main/java/com/econovation/recruitdomain/out/BoardLoadPort.java index f21adee3..6e86baef 100644 --- a/server/Recruit-Domain/src/main/java/com/econovation/recruitdomain/out/BoardLoadPort.java +++ b/server/Recruit-Domain/src/main/java/com/econovation/recruitdomain/out/BoardLoadPort.java @@ -19,5 +19,7 @@ public interface BoardLoadPort { List getBoardByColumnsIds(List columnsIds); + List getBoardByColumnsId(Integer columnsId); + Board getBoardByCardId(Long cardId); }