From 5b8734c51d3d8927adb91ac01fb02bd078b96766 Mon Sep 17 00:00:00 2001 From: koreanMike513 Date: Thu, 9 Jan 2025 18:40:47 +0000 Subject: [PATCH] update: Updated production code and tests - updated production code reflecting spring aop on retry - deleted FoodFacade class - added tests on retry aop --- build.gradle | 4 ++ .../facade/FoodRepositoryFacade.java | 47 --------------- .../la_planete/repository/FoodRepository.java | 2 + .../la_planete/service/OrderService.java | 12 ++-- .../LockRetryAspectTest.java} | 57 +++++++++++++------ 5 files changed, 55 insertions(+), 67 deletions(-) delete mode 100644 src/main/java/com/f_lab/la_planete/facade/FoodRepositoryFacade.java rename src/test/java/com/f_lab/la_planete/{facade/FoodRepositoryFacadeTest.java => aspect/LockRetryAspectTest.java} (58%) diff --git a/build.gradle b/build.gradle index d53221b7..8252faf7 100644 --- a/build.gradle +++ b/build.gradle @@ -34,6 +34,10 @@ dependencies { // swagger 패키지 implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0' + + // lombok 테스트에서도 사용 + testCompileOnly 'org.projectlombok:lombok' + testAnnotationProcessor 'org.projectlombok:lombok' } tasks.named('test') { diff --git a/src/main/java/com/f_lab/la_planete/facade/FoodRepositoryFacade.java b/src/main/java/com/f_lab/la_planete/facade/FoodRepositoryFacade.java deleted file mode 100644 index 7dd6e9f1..00000000 --- a/src/main/java/com/f_lab/la_planete/facade/FoodRepositoryFacade.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.f_lab.la_planete.facade; - -import com.f_lab.la_planete.domain.Food; -import com.f_lab.la_planete.repository.FoodRepository; -import jakarta.persistence.LockTimeoutException; -import jakarta.persistence.PessimisticLockException; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -import org.springframework.stereotype.Component; - -@Slf4j -@Component -@RequiredArgsConstructor -public class FoodRepositoryFacade { - - private static final int MAX_RETRY = 3; - - private final FoodRepository foodRepository; - - public void save(Food food) { - foodRepository.save(food); - } - - public Food findFoodWithLockAndRetry(Long foodId) { - int attempts = 0; - - while (attempts < MAX_RETRY) { - try { - Food food = foodRepository.findFoodByFoodIdWithPessimisticLock(foodId); - - if (food != null) - return food; - - } catch (PessimisticLockException | LockTimeoutException e) { - log.warn("시도 횟수={}, 다시 id={} 에 해당되는 food의 락을 얻기를 시도합니다", attempts, foodId); - } catch (Exception e) { - log.error("Error Occurred at FoodLockFacade.findFoodWithLockAndRetry", e); - throw e; - } - - attempts++; - } - - throw new RuntimeException("현재 너무 많은 요청을 처리하고 있습니다. 다시 시도해주세요"); - } -} diff --git a/src/main/java/com/f_lab/la_planete/repository/FoodRepository.java b/src/main/java/com/f_lab/la_planete/repository/FoodRepository.java index 3a8cc84e..62476c71 100644 --- a/src/main/java/com/f_lab/la_planete/repository/FoodRepository.java +++ b/src/main/java/com/f_lab/la_planete/repository/FoodRepository.java @@ -1,5 +1,6 @@ package com.f_lab.la_planete.repository; +import com.f_lab.la_planete.aspect.RetryOnLockFailure; import com.f_lab.la_planete.domain.Food; import jakarta.persistence.LockModeType; import jakarta.persistence.QueryHint; @@ -14,6 +15,7 @@ public interface FoodRepository extends JpaRepository { + @RetryOnLockFailure @Lock(LockModeType.PESSIMISTIC_WRITE) @Query("SELECT f FROM Food f WHERE f.id = :id") @QueryHints({ @QueryHint(name = "jakarta.persistence.lock.timeout", value = FIVE_SECONDS) }) diff --git a/src/main/java/com/f_lab/la_planete/service/OrderService.java b/src/main/java/com/f_lab/la_planete/service/OrderService.java index b09a62e4..93263bbb 100644 --- a/src/main/java/com/f_lab/la_planete/service/OrderService.java +++ b/src/main/java/com/f_lab/la_planete/service/OrderService.java @@ -5,7 +5,7 @@ import com.f_lab.la_planete.domain.Payment; import com.f_lab.la_planete.dto.request.OrderCreateRequestDTO; import com.f_lab.la_planete.dto.response.OrderCreateResponseDTO; -import com.f_lab.la_planete.facade.FoodRepositoryFacade; +import com.f_lab.la_planete.repository.FoodRepository; import com.f_lab.la_planete.repository.OrderRepository; import com.f_lab.la_planete.repository.PaymentRepository; import lombok.RequiredArgsConstructor; @@ -23,16 +23,16 @@ public class OrderService { private final OrderRepository orderRepository; - private final FoodRepositoryFacade foodRepositoryFacade; + private final FoodRepository foodRepository; private final PaymentRepository paymentRepository; @Transactional public OrderCreateResponseDTO createFoodOrder(OrderCreateRequestDTO request) { // 음식을 조회 후 요청한 수 만큼 빼기 - Food food = foodRepositoryFacade.findFoodWithLockAndRetry(request.getFoodId()); + Food food = findFoodWithLock(request.getFoodId()); food.minusQuantity(request.getQuantity()); - foodRepositoryFacade.save(food); + foodRepository.save(food); // 주문 생성 후 총 금액 계산 Order order = request.toEntity(food); @@ -51,5 +51,9 @@ public OrderCreateResponseDTO createFoodOrder(OrderCreateRequestDTO request) { return new OrderCreateResponseDTO("CREATED"); } + + public Food findFoodWithLock(Long foodId) { + return foodRepository.findFoodByFoodIdWithPessimisticLock(foodId); + } } diff --git a/src/test/java/com/f_lab/la_planete/facade/FoodRepositoryFacadeTest.java b/src/test/java/com/f_lab/la_planete/aspect/LockRetryAspectTest.java similarity index 58% rename from src/test/java/com/f_lab/la_planete/facade/FoodRepositoryFacadeTest.java rename to src/test/java/com/f_lab/la_planete/aspect/LockRetryAspectTest.java index e86c367d..660dc8b4 100644 --- a/src/test/java/com/f_lab/la_planete/facade/FoodRepositoryFacadeTest.java +++ b/src/test/java/com/f_lab/la_planete/aspect/LockRetryAspectTest.java @@ -1,27 +1,39 @@ -package com.f_lab.la_planete.facade; +package com.f_lab.la_planete.aspect; import com.f_lab.la_planete.domain.Food; import com.f_lab.la_planete.repository.FoodRepository; import jakarta.persistence.LockTimeoutException; import jakarta.persistence.PessimisticLockException; +import lombok.RequiredArgsConstructor; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; @SpringBootTest -class FoodRepositoryFacadeTest { +class LockRetryAspectTest { + + @MockitoBean + private FoodRepository foodRepository; + + @Autowired + private FoodService foodService; - @InjectMocks - FoodRepositoryFacade foodRepositoryFacade; - @Mock - FoodRepository foodRepository; + @TestConfiguration + static class LockRetryAspectTestConfig { + @Bean + public FoodService foodService(FoodRepository foodRepository) { + return new FoodService(foodRepository); + } + } @Test @DisplayName("락 없이 첫 시도에 성공") @@ -31,8 +43,10 @@ void test_find_food_lock_and_retry_success() { Food expectedFood = createFood(foodId); // when - when(foodRepository.findFoodByFoodIdWithPessimisticLock(anyLong())).thenReturn(expectedFood); - Food foundFood = foodRepositoryFacade.findFoodWithLockAndRetry(foodId); + when(foodRepository.findFoodByFoodIdWithPessimisticLock(foodId)) + .thenReturn(expectedFood); + + Food foundFood = foodService.findFood(foodId); // then assertThat(foundFood.getId()).isEqualTo(foodId); @@ -50,10 +64,11 @@ void test_find_food_lock_and_retry_fail_on_first_then_success() { .thenThrow(new PessimisticLockException()) .thenReturn(expectedFood); - Food foundFood = foodRepositoryFacade.findFoodWithLockAndRetry(foodId); + Food foundFood = foodService.findFood(foodId); // then assertThat(foundFood.getId()).isEqualTo(foodId); + verify(foodRepository, times(2)).findFoodByFoodIdWithPessimisticLock(foodId); } @Test @@ -68,15 +83,25 @@ void test_find_food_lock_and_retry_fail() { .thenThrow(new LockTimeoutException()) .thenThrow(new LockTimeoutException()); - assertThatThrownBy(() -> foodRepositoryFacade.findFoodWithLockAndRetry(foodId)) + // then + assertThatThrownBy(() -> foodService.findFood(foodId)) .isInstanceOf(RuntimeException.class) .hasMessage("현재 너무 많은 요청을 처리하고 있습니다. 다시 시도해주세요"); } - private Food createFood(Long foodId) { return Food.builder() .id(foodId) .build(); } -} \ No newline at end of file + + @RequiredArgsConstructor + static class FoodService { + private final FoodRepository foodRepository; + + @RetryOnLockFailure + public Food findFood(Long foodId) { + return foodRepository.findFoodByFoodIdWithPessimisticLock(foodId); + } + } +}