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/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 fd7ffbd2..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 @@ -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,24 +140,26 @@ public GetCategoryResponseDto getCategory(final Long userId, final Long category //순서 업데이트 @Transactional - public void editCategoryPriority(ChangeCateoryPriorityDto changeCateoryPriorityDto) { + public void editCategoryPriority(ChangeCategoryPriorityDto changeCategoryPriorityDto) { + val newPriority = changeCategoryPriorityDto.newPriority(); - val newPriority = changeCateoryPriorityDto.newPriority(); - - Category category = categoryRepository.findById(changeCateoryPriorityDto.categoryId()) - .orElseThrow(() -> new NotFoundException(Error.NOT_FOUND_CATEGORY_EXCEPTION, - Error.NOT_FOUND_CATEGORY_EXCEPTION.getMessage())); + 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()); - if (currentPriority < newPriority) - categoryRepository.decreasePriorityByOne(changeCateoryPriorityDto.categoryId(), currentPriority, - newPriority, category.getUser().getUserId()); - else if (currentPriority > newPriority) - categoryRepository.increasePriorityByOne(changeCateoryPriorityDto.categoryId(), currentPriority, - newPriority, category.getUser().getUserId()); + if (currentPriority < newPriority) { + categoryRepository.findAllByPriorityBetweenForUpdate(currentPriority, newPriority); + categoryRepository.decreasePriorityByOne(changeCategoryPriorityDto.categoryId(), currentPriority, + newPriority, category.getUser().getUserId()); + } else { + categoryRepository.findAllByPriorityBetweenForUpdate(newPriority, currentPriority); + categoryRepository.increasePriorityByOne(changeCategoryPriorityDto.categoryId(), currentPriority, + 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 index 8b137891..e69de29b 100644 --- a/linkmind/src/main/resources/data.sql +++ b/linkmind/src/main/resources/data.sql @@ -1 +0,0 @@ - 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(); + } + + +} +