From 0ff67220229a1df236842875d3e92701a95a27dd Mon Sep 17 00:00:00 2001 From: mmihye Date: Thu, 3 Apr 2025 00:21:29 +0900 Subject: [PATCH 1/4] =?UTF-8?q?[#264]=20fix:=20=EB=B3=80=EC=88=98=EB=AA=85?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/CategoryController.java | 8 ++++---- ...to.java => ChangeCategoryPriorityDto.java} | 2 +- .../category/service/CategoryService.java | 19 +++++++++---------- 3 files changed, 14 insertions(+), 15 deletions(-) rename linkmind/src/main/java/com/app/toaster/category/controller/request/{ChangeCateoryPriorityDto.java => ChangeCategoryPriorityDto.java} (82%) diff --git a/linkmind/src/main/java/com/app/toaster/category/controller/CategoryController.java b/linkmind/src/main/java/com/app/toaster/category/controller/CategoryController.java index dd4cb164..7c41e82b 100644 --- a/linkmind/src/main/java/com/app/toaster/category/controller/CategoryController.java +++ b/linkmind/src/main/java/com/app/toaster/category/controller/CategoryController.java @@ -1,6 +1,6 @@ package com.app.toaster.category.controller; -import com.app.toaster.category.controller.request.ChangeCateoryPriorityDto; +import com.app.toaster.category.controller.request.ChangeCategoryPriorityDto; import com.app.toaster.category.controller.request.ChangeCateoryTitleDto; import com.app.toaster.category.controller.request.CreateCategoryDto; import com.app.toaster.common.dto.ApiResponse; @@ -33,7 +33,7 @@ public class CategoryController { @PostMapping @ResponseStatus(HttpStatus.CREATED) - public ApiResponse createCateory( + public ApiResponse createCategory( @UserId Long userId, @Valid @RequestBody CreateCategoryDto createCategoryDto ){ @@ -61,9 +61,9 @@ public ApiResponse getCategories(@UserId Long userId){ @ResponseStatus(HttpStatus.OK) public ApiResponse editCategoryPriority( @UserId Long userId, - @RequestBody ChangeCateoryPriorityDto changeCateoryPriorityDto + @RequestBody ChangeCategoryPriorityDto changeCategoryPriorityDto ){ - categoryService.editCategoryPriority(changeCateoryPriorityDto); + categoryService.editCategoryPriority(changeCategoryPriorityDto); return ApiResponse.success(Success.UPDATE_CATEGORY_TITLE_SUCCESS); } diff --git a/linkmind/src/main/java/com/app/toaster/category/controller/request/ChangeCateoryPriorityDto.java b/linkmind/src/main/java/com/app/toaster/category/controller/request/ChangeCategoryPriorityDto.java similarity index 82% rename from linkmind/src/main/java/com/app/toaster/category/controller/request/ChangeCateoryPriorityDto.java rename to linkmind/src/main/java/com/app/toaster/category/controller/request/ChangeCategoryPriorityDto.java index 4337f794..67bc009b 100644 --- a/linkmind/src/main/java/com/app/toaster/category/controller/request/ChangeCateoryPriorityDto.java +++ b/linkmind/src/main/java/com/app/toaster/category/controller/request/ChangeCategoryPriorityDto.java @@ -2,7 +2,7 @@ import jakarta.validation.constraints.NotNull; -public record ChangeCateoryPriorityDto( +public record ChangeCategoryPriorityDto( @NotNull Long categoryId, @NotNull diff --git a/linkmind/src/main/java/com/app/toaster/category/service/CategoryService.java b/linkmind/src/main/java/com/app/toaster/category/service/CategoryService.java index fd7ffbd2..d1ab6e5a 100644 --- a/linkmind/src/main/java/com/app/toaster/category/service/CategoryService.java +++ b/linkmind/src/main/java/com/app/toaster/category/service/CategoryService.java @@ -1,6 +1,6 @@ package com.app.toaster.category.service; -import com.app.toaster.category.controller.request.ChangeCateoryPriorityDto; +import com.app.toaster.category.controller.request.ChangeCategoryPriorityDto; import com.app.toaster.category.controller.request.ChangeCateoryTitleDto; import com.app.toaster.category.controller.request.CreateCategoryDto; import com.app.toaster.category.controller.response.CategoriesResponse; @@ -41,7 +41,7 @@ public class CategoryService { private final CategoryRepository categoryRepository; private final ToastRepository toastRepository; - private final static int MAX_CATERGORY_NUMBER = 15; + private final static int MAX_CATEGORY_NUMBER = 15; private final TimerRepository timerRepository; @Transactional @@ -52,9 +52,8 @@ public void createCategory(final Long userId, final CreateCategoryDto createCate val maxPriority = categoryRepository.findMaxPriorityByUser(presentUser); val categoryNum = categoryRepository.countAllByUser(presentUser); - System.out.println(categoryNum); - if (categoryNum >= MAX_CATERGORY_NUMBER) { + if (categoryNum >= MAX_CATEGORY_NUMBER) { throw new CustomException(Error.BAD_REQUEST_CREATE_CLIP_EXCEPTION, Error.BAD_REQUEST_CREATE_CLIP_EXCEPTION.getMessage()); } @@ -141,22 +140,22 @@ public GetCategoryResponseDto getCategory(final Long userId, final Long category //순서 업데이트 @Transactional - public void editCategoryPriority(ChangeCateoryPriorityDto changeCateoryPriorityDto) { + public void editCategoryPriority(ChangeCategoryPriorityDto changeCategoryPriorityDto) { - val newPriority = changeCateoryPriorityDto.newPriority(); + val newPriority = changeCategoryPriorityDto.newPriority(); - Category category = categoryRepository.findById(changeCateoryPriorityDto.categoryId()) + Category category = categoryRepository.findById(changeCategoryPriorityDto.categoryId()) .orElseThrow(() -> new NotFoundException(Error.NOT_FOUND_CATEGORY_EXCEPTION, Error.NOT_FOUND_CATEGORY_EXCEPTION.getMessage())); int currentPriority = category.getPriority(); - category.updateCategoryPriority(changeCateoryPriorityDto.newPriority()); + category.updateCategoryPriority(changeCategoryPriorityDto.newPriority()); if (currentPriority < newPriority) - categoryRepository.decreasePriorityByOne(changeCateoryPriorityDto.categoryId(), currentPriority, + categoryRepository.decreasePriorityByOne(changeCategoryPriorityDto.categoryId(), currentPriority, newPriority, category.getUser().getUserId()); else if (currentPriority > newPriority) - categoryRepository.increasePriorityByOne(changeCateoryPriorityDto.categoryId(), currentPriority, + categoryRepository.increasePriorityByOne(changeCategoryPriorityDto.categoryId(), currentPriority, newPriority, category.getUser().getUserId()); } From 8f0aec13e1123e8bc3e4a65038205a1df49728c7 Mon Sep 17 00:00:00 2001 From: mmihye Date: Fri, 4 Apr 2025 12:11:54 +0900 Subject: [PATCH 2/4] =?UTF-8?q?[#264]=20feat:=20=EC=B9=B4=ED=85=8C?= =?UTF-8?q?=EA=B3=A0=EB=A6=AC=20=EC=88=98=EC=A0=95=EC=97=90=20=EB=8C=80?= =?UTF-8?q?=ED=95=B4=20=EB=9D=BD=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/CategoryRepository.java | 7 +++++++ .../category/service/CategoryService.java | 18 ++++++++++-------- linkmind/src/main/resources/data.sql | 1 - 3 files changed, 17 insertions(+), 9 deletions(-) delete mode 100644 linkmind/src/main/resources/data.sql diff --git a/linkmind/src/main/java/com/app/toaster/category/infrastructure/CategoryRepository.java b/linkmind/src/main/java/com/app/toaster/category/infrastructure/CategoryRepository.java index 12b86c06..b1782d8e 100644 --- a/linkmind/src/main/java/com/app/toaster/category/infrastructure/CategoryRepository.java +++ b/linkmind/src/main/java/com/app/toaster/category/infrastructure/CategoryRepository.java @@ -9,11 +9,18 @@ import org.springframework.data.jpa.repository.JpaRepository; import com.app.toaster.category.domain.Category; + +import org.springframework.data.jpa.repository.Lock; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import jakarta.persistence.LockModeType; + public interface CategoryRepository extends JpaRepository { + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("SELECT c FROM Category c WHERE c.priority BETWEEN :minPriority AND :maxPriority") + List findAllByPriorityBetweenForUpdate(@Param("minPriority") int minPriority, @Param("maxPriority") int maxPriority); @Query("SELECT COALESCE(MAX(c.priority), 0) FROM Category c WHERE c.user = :user") int findMaxPriorityByUser(@Param("user") User user); diff --git a/linkmind/src/main/java/com/app/toaster/category/service/CategoryService.java b/linkmind/src/main/java/com/app/toaster/category/service/CategoryService.java index d1ab6e5a..c0aa5392 100644 --- a/linkmind/src/main/java/com/app/toaster/category/service/CategoryService.java +++ b/linkmind/src/main/java/com/app/toaster/category/service/CategoryService.java @@ -141,23 +141,25 @@ public GetCategoryResponseDto getCategory(final Long userId, final Long category //순서 업데이트 @Transactional public void editCategoryPriority(ChangeCategoryPriorityDto changeCategoryPriorityDto) { - val newPriority = changeCategoryPriorityDto.newPriority(); Category category = categoryRepository.findById(changeCategoryPriorityDto.categoryId()) - .orElseThrow(() -> new NotFoundException(Error.NOT_FOUND_CATEGORY_EXCEPTION, - Error.NOT_FOUND_CATEGORY_EXCEPTION.getMessage())); + .orElseThrow(() -> new NotFoundException(Error.NOT_FOUND_CATEGORY_EXCEPTION, + Error.NOT_FOUND_CATEGORY_EXCEPTION.getMessage())); int currentPriority = category.getPriority(); - category.updateCategoryPriority(changeCategoryPriorityDto.newPriority()); - if (currentPriority < newPriority) + if (currentPriority < newPriority) { + categoryRepository.findAllByPriorityBetweenForUpdate(currentPriority, newPriority); categoryRepository.decreasePriorityByOne(changeCategoryPriorityDto.categoryId(), currentPriority, - newPriority, category.getUser().getUserId()); - else if (currentPriority > newPriority) + newPriority, category.getUser().getUserId()); + } else { + categoryRepository.findAllByPriorityBetweenForUpdate(newPriority, currentPriority); categoryRepository.increasePriorityByOne(changeCategoryPriorityDto.categoryId(), currentPriority, - newPriority, category.getUser().getUserId()); + newPriority, category.getUser().getUserId()); + } + category.updateCategoryPriority(changeCategoryPriorityDto.newPriority()); } @Transactional diff --git a/linkmind/src/main/resources/data.sql b/linkmind/src/main/resources/data.sql deleted file mode 100644 index 8b137891..00000000 --- a/linkmind/src/main/resources/data.sql +++ /dev/null @@ -1 +0,0 @@ - From 0d97ed39fe5770d35d62f7aa19d3e832ae9731bd Mon Sep 17 00:00:00 2001 From: mmihye Date: Fri, 4 Apr 2025 12:12:02 +0900 Subject: [PATCH 3/4] =?UTF-8?q?[#264]=20feat:=20=EB=9D=BD=EC=97=90=20?= =?UTF-8?q?=EB=8C=80=ED=95=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94?= =?UTF-8?q?=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../category/service/CategoryServiceTest.java | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 linkmind/src/test/java/com/app/toaster/category/service/CategoryServiceTest.java diff --git a/linkmind/src/test/java/com/app/toaster/category/service/CategoryServiceTest.java b/linkmind/src/test/java/com/app/toaster/category/service/CategoryServiceTest.java new file mode 100644 index 00000000..d25802e9 --- /dev/null +++ b/linkmind/src/test/java/com/app/toaster/category/service/CategoryServiceTest.java @@ -0,0 +1,153 @@ +package com.app.toaster.category.service; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import org.checkerframework.checker.units.qual.A; +import org.checkerframework.checker.units.qual.C; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Profile; +import org.springframework.test.context.ActiveProfiles; + +import com.app.toaster.auth.controller.AuthController; +import com.app.toaster.auth.service.AuthService; +import com.app.toaster.auth.service.kakao.KakaoSignInService; +import com.app.toaster.category.controller.request.ChangeCategoryPriorityDto; +import com.app.toaster.category.domain.Category; +import com.app.toaster.category.infrastructure.CategoryRepository; +import com.app.toaster.user.domain.SocialType; +import com.app.toaster.user.domain.User; +import com.app.toaster.user.infrastructure.UserRepository; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.app.toaster.user.domain.User; +import com.app.toaster.user.domain.SocialType; +import com.app.toaster.user.infrastructure.UserRepository; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionTemplate; + +@SpringBootTest +@ActiveProfiles(profiles = "local") +class UserRepositoryTest { + + @Autowired + private UserRepository userRepository; + + @Autowired + private CategoryRepository categoryRepository; + + @Autowired + private CategoryService categoryService; + + @Autowired + private PlatformTransactionManager transactionManager; + + private User user; + private Category targetCategory; + + @BeforeEach + void setUp() { + user = userRepository.findByUserId(7L).orElseThrow(); + + Category category1 = new Category("category1", user, 1); + Category category2 = new Category("category2", user, 2); + Category category3 = new Category("category3", user, 3); + Category category4 = new Category("category4", user, 4); + Category category5 = new Category("category5", user, 5); + + categoryRepository.saveAll(List.of(category1, category2, category3, category4, category5)); + targetCategory = category3; + } + + @Test + @DisplayName("우선순위를 증가하여 수정힌다.") + void increaseCategoryPriority() { + // Given + + // When + categoryService.editCategoryPriority(new ChangeCategoryPriorityDto(targetCategory.getCategoryId(), 5)); + + // Then + List categoryList = categoryRepository.findAllByUserOrderByPriority(user); + assertThat(categoryList.get(0).getTitle()).isEqualTo("category1"); + assertThat(categoryList.get(1).getTitle()).isEqualTo("category2"); + assertThat(categoryList.get(2).getTitle()).isEqualTo("category4"); + assertThat(categoryList.get(3).getTitle()).isEqualTo("category5"); + assertThat(categoryList.get(4).getTitle()).isEqualTo("category3"); + } + + @Test + @DisplayName("우선순위를 감소하여 수정힌다.") + void decreaseCategoryPriority() { + // Given + + // When + categoryService.editCategoryPriority(new ChangeCategoryPriorityDto(targetCategory.getCategoryId(), 1)); + + // Then + List categoryList = categoryRepository.findAllByUserOrderByPriority(user); + assertThat(categoryList.get(0).getTitle()).isEqualTo("category3"); + assertThat(categoryList.get(1).getTitle()).isEqualTo("category1"); + assertThat(categoryList.get(2).getTitle()).isEqualTo("category2"); + assertThat(categoryList.get(3).getTitle()).isEqualTo("category4"); + assertThat(categoryList.get(4).getTitle()).isEqualTo("category5"); + } + + @Test + @DisplayName("SELECT FOR UPDATE가 동시에 접근할 경우 대기하거나 충돌하는지 확인한다.") + void testSelectForUpdateConcurrency() throws ExecutionException, InterruptedException { + // 트랜잭션 템플릿 설정 + TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager); + transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + + CompletableFuture transaction1 = CompletableFuture.runAsync(() -> { + transactionTemplate.execute(status -> { + categoryService.editCategoryPriority(new ChangeCategoryPriorityDto(targetCategory.getCategoryId(), 5)); + try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } + return null; + }); + }); + + CompletableFuture transaction2 = CompletableFuture.runAsync(() -> { + transactionTemplate.execute(status -> { + categoryService.editCategoryPriority(new ChangeCategoryPriorityDto(targetCategory.getCategoryId(), 1)); + return null; + }); + }); + + CompletableFuture.allOf(transaction1, transaction2).join(); + + // 결과 확인 + List categoryList = categoryRepository.findAllByUserOrderByPriority(user); + assertThat(categoryList.get(0).getTitle()).isEqualTo("category3"); + assertThat(categoryList.get(1).getTitle()).isEqualTo("category1"); + assertThat(categoryList.get(2).getTitle()).isEqualTo("category2"); + assertThat(categoryList.get(3).getTitle()).isEqualTo("category4"); + assertThat(categoryList.get(4).getTitle()).isEqualTo("category3"); + } + + @AfterEach + void finish(){ + categoryRepository.deleteAll(); + } + + +} + From de0e01978462df4e270fd01d7a63d50993d06712 Mon Sep 17 00:00:00 2001 From: mmihye Date: Fri, 4 Apr 2025 12:19:24 +0900 Subject: [PATCH 4/4] =?UTF-8?q?[#264]=20fix:=20resource=20=ED=8F=B4?= =?UTF-8?q?=EB=8D=94=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- linkmind/src/main/resources/data.sql | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 linkmind/src/main/resources/data.sql diff --git a/linkmind/src/main/resources/data.sql b/linkmind/src/main/resources/data.sql new file mode 100644 index 00000000..e69de29b