diff --git a/backend/techpick-api/src/main/java/techpick/api/annotation/LoginUserIdDistributedLock.java b/backend/techpick-api/src/main/java/techpick/api/annotation/LoginUserIdDistributedLock.java index ee6e46484..0eb9cb0b6 100644 --- a/backend/techpick-api/src/main/java/techpick/api/annotation/LoginUserIdDistributedLock.java +++ b/backend/techpick-api/src/main/java/techpick/api/annotation/LoginUserIdDistributedLock.java @@ -14,7 +14,5 @@ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface LoginUserIdDistributedLock { - String key(); - long timeout() default 3000; } diff --git a/backend/techpick-api/src/main/java/techpick/api/domain/folder/service/FolderService.java b/backend/techpick-api/src/main/java/techpick/api/domain/folder/service/FolderService.java index 72df8b33a..825a618d5 100644 --- a/backend/techpick-api/src/main/java/techpick/api/domain/folder/service/FolderService.java +++ b/backend/techpick-api/src/main/java/techpick/api/domain/folder/service/FolderService.java @@ -65,6 +65,7 @@ public List getBasicFolderList(Long userId) { /** * 생성하려는 폴더가 미분류폴더, 휴지통이 아닌지 검증합니다. * */ + @LoginUserIdDistributedLock @Transactional public FolderResult saveFolder(FolderCommand.Create command) { Folder parentFolder = folderDataHandler.getFolder(command.parentFolderId()); @@ -89,6 +90,7 @@ public FolderResult updateFolder(FolderCommand.Update command) { * 현재 폴더들의 부모가 같은지 검증합니다. * 이동하려는 폴더가 미분류폴더, 휴지통이 아닌지 검증합니다. * */ + @LoginUserIdDistributedLock @Transactional public void moveFolder(FolderCommand.Move command) { Folder destinationFolder = folderDataHandler.getFolder(command.destinationFolderId()); @@ -109,8 +111,7 @@ public void moveFolder(FolderCommand.Move command) { } } - // TODO: 리팩토링 필요 - @LoginUserIdDistributedLock(key = "DELETE_FOLDER") + @LoginUserIdDistributedLock @Transactional public void deleteFolder(FolderCommand.Delete command) { // 먼저 공유 부터 해제 diff --git a/backend/techpick-api/src/main/java/techpick/api/domain/pick/service/PickService.java b/backend/techpick-api/src/main/java/techpick/api/domain/pick/service/PickService.java index bb15fe23a..8acaf0958 100644 --- a/backend/techpick-api/src/main/java/techpick/api/domain/pick/service/PickService.java +++ b/backend/techpick-api/src/main/java/techpick/api/domain/pick/service/PickService.java @@ -9,6 +9,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import techpick.api.annotation.LoginUserIdDistributedLock; import techpick.api.domain.folder.exception.ApiFolderException; import techpick.api.domain.pick.dto.PickCommand; import techpick.api.domain.pick.dto.PickMapper; @@ -69,6 +70,7 @@ public List getFolderListChildPickList(PickCommand.Re .toList(); } + @LoginUserIdDistributedLock @Transactional public PickResult.Pick saveNewPick(PickCommand.Create command) { validateRootAccess(command.parentFolderId()); @@ -78,6 +80,7 @@ public PickResult.Pick saveNewPick(PickCommand.Create command) { return pickMapper.toPickResult(pickDataHandler.savePick(command)); } + @LoginUserIdDistributedLock @Transactional public PickResult.Pick updatePick(PickCommand.Update command) { validatePickAccess(command.userId(), command.id()); @@ -86,6 +89,7 @@ public PickResult.Pick updatePick(PickCommand.Update command) { return pickMapper.toPickResult(pickDataHandler.updatePick(command)); } + @LoginUserIdDistributedLock @Transactional public void movePick(PickCommand.Move command) { validateRootAccess(command.destinationFolderId()); @@ -101,6 +105,7 @@ public void movePick(PickCommand.Move command) { pickDataHandler.movePickToCurrentFolder(command); } + @LoginUserIdDistributedLock @Transactional public void deletePick(PickCommand.Delete command) { List pickList = pickDataHandler.getPickList(command.idList()); diff --git a/backend/techpick-api/src/main/java/techpick/api/domain/tag/service/TagService.java b/backend/techpick-api/src/main/java/techpick/api/domain/tag/service/TagService.java index bff8f3470..ef9a7d8f5 100644 --- a/backend/techpick-api/src/main/java/techpick/api/domain/tag/service/TagService.java +++ b/backend/techpick-api/src/main/java/techpick/api/domain/tag/service/TagService.java @@ -6,6 +6,7 @@ import org.springframework.transaction.annotation.Transactional; import lombok.RequiredArgsConstructor; +import techpick.api.annotation.LoginUserIdDistributedLock; import techpick.api.domain.tag.dto.TagCommand; import techpick.api.domain.tag.dto.TagMapper; import techpick.api.domain.tag.dto.TagResult; @@ -33,6 +34,7 @@ public List getUserTagList(Long userId) { .map(tagMapper::toResult).toList(); } + @LoginUserIdDistributedLock @Transactional public TagResult saveTag(TagCommand.Create command) { validateDuplicateTagName(command.userId(), command.name()); @@ -49,6 +51,7 @@ public TagResult updateTag(TagCommand.Update command) { return tagMapper.toResult(tagDataHandler.updateTag(command)); } + @LoginUserIdDistributedLock @Transactional public void moveUserTag(TagCommand.Move command) { Tag tag = tagDataHandler.getTag(command.id()); @@ -58,6 +61,7 @@ public void moveUserTag(TagCommand.Move command) { tagDataHandler.moveTag(command.userId(), command); } + @LoginUserIdDistributedLock @Transactional public void deleteTag(TagCommand.Delete command) { Tag tag = tagDataHandler.getTag(command.id()); diff --git a/backend/techpick-api/src/main/java/techpick/api/infrastructure/lock/util/LoginUserIdDistributedLockAspect.java b/backend/techpick-api/src/main/java/techpick/api/infrastructure/lock/util/LoginUserIdDistributedLockAspect.java index 56acd0284..a7901ab49 100644 --- a/backend/techpick-api/src/main/java/techpick/api/infrastructure/lock/util/LoginUserIdDistributedLockAspect.java +++ b/backend/techpick-api/src/main/java/techpick/api/infrastructure/lock/util/LoginUserIdDistributedLockAspect.java @@ -1,15 +1,20 @@ package techpick.api.infrastructure.lock.util; +import java.lang.reflect.Field; + +import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import lombok.RequiredArgsConstructor; import techpick.api.annotation.LoginUserIdDistributedLock; import techpick.api.infrastructure.lock.LockProvider; -import techpick.security.annotation.LoginUserId; +@Order(1) @Aspect @Component @RequiredArgsConstructor @@ -19,13 +24,12 @@ public class LoginUserIdDistributedLockAspect { @Around("@annotation(loginUserIdDistributedLock)") public Object handleDistributedLock(ProceedingJoinPoint joinPoint, - LoginUserIdDistributedLock loginUserIdDistributedLock, @LoginUserId Long userId) throws Throwable { - String key = loginUserIdDistributedLock.key(); + LoginUserIdDistributedLock loginUserIdDistributedLock) throws Throwable { + String key = getMethodName(joinPoint); long timeout = loginUserIdDistributedLock.timeout(); + Long userId = getUserIdFromArgs(joinPoint); - if (!lockProvider.acquireLock(key, timeout, userId)) { - throw new LockException("락 획득 실패, key : " + key); - } + lockProvider.acquireLock(key, timeout, userId); try { return joinPoint.proceed(); @@ -33,4 +37,47 @@ public Object handleDistributedLock(ProceedingJoinPoint joinPoint, lockProvider.releaseLock(key, userId); } } + + /** + * @author sangwon + * 리플렉션을 통해 메서드 이름을 가져오는 메서드 + */ + private String getMethodName(JoinPoint joinPoint) { + MethodSignature signature = (MethodSignature)joinPoint.getSignature(); + return signature.getMethod().getName(); + } + + /** + * @author sangwon + * 리플렉션을 통해 메서드 파라미터에 있는 userId를 가져온다. + */ + private Long getUserIdFromArgs(JoinPoint joinPoint) { + Object[] args = joinPoint.getArgs(); + MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature(); + String[] parameterNames = methodSignature.getParameterNames(); + + for (int i = 0; i < parameterNames.length; i++) { + Object arg = args[i]; + + if ("userId".equals(parameterNames[i]) && arg instanceof Long) { + return (Long)arg; + } + + if (arg != null) { + try { + // Reflection으로 "userId" 필드를 추출 + Field field = arg.getClass().getDeclaredField("userId"); + field.setAccessible(true); // private 필드 접근 허용 + Object userId = field.get(arg); + if (userId instanceof Long) { + return (Long)userId; + } + } catch (NoSuchFieldException | IllegalAccessException ignored) { + // 해당 파라미터에 "userId"가 없으면 무시 + } + } + } + + throw new IllegalArgumentException("userId 파라미터를 찾을 수 없습니다."); + } } diff --git a/backend/techpick-api/src/main/java/techpick/api/infrastructure/pick/PickDataHandler.java b/backend/techpick-api/src/main/java/techpick/api/infrastructure/pick/PickDataHandler.java index 980a4856c..c231d0dea 100644 --- a/backend/techpick-api/src/main/java/techpick/api/infrastructure/pick/PickDataHandler.java +++ b/backend/techpick-api/src/main/java/techpick/api/infrastructure/pick/PickDataHandler.java @@ -53,7 +53,7 @@ public Pick getPick(Long pickId) { @Transactional(readOnly = true) public Pick getPickUrl(Long userId, String url) { return pickRepository.findByUserIdAndLinkUrl(userId, url) - .orElseThrow(ApiPickException::PICK_NOT_FOUND); + .orElseThrow(ApiPickException::PICK_NOT_FOUND); } @Transactional(readOnly = true) @@ -91,30 +91,30 @@ public boolean existsByUserIdAndLink(Long userId, Link link) { public Pick savePick(PickCommand.Create command) throws ApiPickException { User user = userRepository.findById(command.userId()).orElseThrow(ApiUserException::USER_NOT_FOUND); Folder folder = folderRepository.findById(command.parentFolderId()) - .orElseThrow(ApiFolderException::FOLDER_NOT_FOUND); + .orElseThrow(ApiFolderException::FOLDER_NOT_FOUND); Link link = linkRepository.findByUrl(command.linkInfo().url()) - .map(existLink -> { - existLink.updateMetadata(command.linkInfo().title(), - command.linkInfo().description(), - command.linkInfo().imageUrl()); - return existLink; - }) - .orElseGet(() -> linkRepository.save(linkMapper.of(command.linkInfo()))); + .map(existLink -> { + existLink.updateMetadata(command.linkInfo().title(), + command.linkInfo().description(), + command.linkInfo().imageUrl()); + return existLink; + }) + .orElseGet(() -> linkRepository.save(linkMapper.of(command.linkInfo()))); // 픽 존재 여부 검증 pickRepository.findByUserAndLink(user, link) - .ifPresent((__) -> { - throw ApiPickException.PICK_MUST_BE_UNIQUE_FOR_A_URL(); - }); + .ifPresent((__) -> { + throw ApiPickException.PICK_MUST_BE_UNIQUE_FOR_A_URL(); + }); Pick savedPick = pickRepository.save(pickMapper.toEntity(command, user, folder, link)); Folder parentFolder = savedPick.getParentFolder(); attachPickToParentFolder(savedPick, parentFolder); List pickTagList = tagRepository.findAllById(command.tagIdOrderedList()) - .stream() - .map(tag -> PickTag.of(savedPick, tag)) - .toList(); + .stream() + .map(tag -> PickTag.of(savedPick, tag)) + .toList(); pickTagRepository.saveAll(pickTagList); return savedPick; @@ -132,7 +132,7 @@ public Pick updatePick(PickCommand.Update command) { if (command.parentFolderId() != null) { Folder parentFolder = pick.getParentFolder(); Folder destinationFolder = folderRepository.findById(command.parentFolderId()) - .orElseThrow(ApiFolderException::FOLDER_NOT_FOUND); + .orElseThrow(ApiFolderException::FOLDER_NOT_FOUND); detachPickFromParentFolder(pick, parentFolder); attachPickToParentFolder(pick, destinationFolder); @@ -148,7 +148,7 @@ public Pick updatePick(PickCommand.Update command) { public void movePickToCurrentFolder(PickCommand.Move command) { List pickIdList = command.idList(); Folder folder = folderRepository.findById(command.destinationFolderId()) - .orElseThrow(ApiFolderException::FOLDER_NOT_FOUND); + .orElseThrow(ApiFolderException::FOLDER_NOT_FOUND); movePickListToDestinationFolder(pickIdList, folder, command.orderIdx()); } @@ -156,7 +156,7 @@ public void movePickToCurrentFolder(PickCommand.Move command) { public void movePickToOtherFolder(PickCommand.Move command) { List pickIdList = command.idList(); Folder destinationFolder = folderRepository.findById(command.destinationFolderId()) - .orElseThrow(ApiFolderException::FOLDER_NOT_FOUND); + .orElseThrow(ApiFolderException::FOLDER_NOT_FOUND); List pickList = pickRepository.findAllById(pickIdList); pickList.forEach(pick -> { @@ -194,7 +194,7 @@ public void deletePickList(PickCommand.Delete command) { @Transactional public void attachTagToPickTag(Pick pick, Long tagId) { Tag tag = tagRepository.findById(tagId) - .orElseThrow(ApiTagException::TAG_NOT_FOUND); + .orElseThrow(ApiTagException::TAG_NOT_FOUND); PickTag pickTag = PickTag.of(pick, tag); pickTagRepository.save(pickTag); } @@ -202,7 +202,7 @@ public void attachTagToPickTag(Pick pick, Long tagId) { @Transactional public void detachTagFromPickTag(Pick pick, Long tagId) { pickTagRepository.findByPickAndTagId(pick, tagId) - .ifPresent(pickTagRepository::delete); + .ifPresent(pickTagRepository::delete); } // 부모 폴더의 픽 리스트에 추가 @@ -227,25 +227,16 @@ private void movePickListToDestinationFolder(List pickIdList, Folder folde private void updateNewTagIdList(Pick pick, List newTagOrderList) { // 1. 기존 태그와 새로운 태그를 비교하여 없어진 태그를 PickTag 테이블에서 제거 - log.info("---------- (1) {}", pick.getTagIdOrderedList().toString()); pick.getTagIdOrderedList().stream() - .filter(tagId -> { - return !newTagOrderList.contains(tagId); - }) + .filter(tagId -> !newTagOrderList.contains(tagId)) .forEach(tagId -> detachTagFromPickTag(pick, tagId)); // 2. 새로운 태그 중 기존에 없는 태그를 PickTag 테이블에 추가 newTagOrderList.stream() - .filter(tagId -> { - log.info("---------- (2) {}", pick.getTagIdOrderedList().toString()); - return !pick.getTagIdOrderedList().contains(tagId); - }) - .forEach(tagId -> attachTagToPickTag(pick, tagId)); - - log.info("---------- (3) {}", pick.getTagIdOrderedList().toString()); + .filter(tagId -> !pick.getTagIdOrderedList().contains(tagId)) + .forEach(tagId -> attachTagToPickTag(pick, tagId)); pick.updateTagOrderList(newTagOrderList); - log.info("---------- (4) {}", pick.getTagIdOrderedList().toString()); } } \ No newline at end of file