Skip to content

Commit

Permalink
feat: Added FoodRepositoryFacade
Browse files Browse the repository at this point in the history
- added FoodRepositoryFacade that handles RETRY logic on the food locks
  • Loading branch information
koreanMike513 committed Jan 9, 2025
1 parent f5d32b8 commit f0bebbc
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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("현재 너무 많은 요청을 처리하고 있습니다. 다시 시도해주세요");
}
}
12 changes: 4 additions & 8 deletions src/main/java/com/f_lab/la_planete/service/OrderService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.repository.FoodRepository;
import com.f_lab.la_planete.facade.FoodRepositoryFacade;
import com.f_lab.la_planete.repository.OrderRepository;
import com.f_lab.la_planete.repository.PaymentRepository;
import lombok.RequiredArgsConstructor;
Expand All @@ -23,16 +23,16 @@
public class OrderService {

private final OrderRepository orderRepository;
private final FoodRepository foodRepository;
private final FoodRepositoryFacade foodRepositoryFacade;
private final PaymentRepository paymentRepository;

@Transactional
public OrderCreateResponseDTO createFoodOrder(OrderCreateRequestDTO request) {

// 음식을 조회 후 요청한 수 만큼 빼기
Food food = findFoodWithLock(request.getFoodId());
Food food = foodRepositoryFacade.findFoodWithLockAndRetry(request.getFoodId());
food.minusQuantity(request.getQuantity());
foodRepository.save(food);
foodRepositoryFacade.save(food);

// 주문 생성 후 총 금액 계산
Order order = request.toEntity(food);
Expand All @@ -51,9 +51,5 @@ public OrderCreateResponseDTO createFoodOrder(OrderCreateRequestDTO request) {

return new OrderCreateResponseDTO("CREATED");
}

private Food findFoodWithLock(Long foodId) {
return foodRepository.findFoodByFoodIdWithPessimisticLock(foodId);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
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 org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.springframework.boot.test.context.SpringBootTest;

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;

@SpringBootTest
class FoodRepositoryFacadeTest {

@InjectMocks
FoodRepositoryFacade foodRepositoryFacade;
@Mock
FoodRepository foodRepository;

@Test
@DisplayName("락 없이 첫 시도에 성공")
void test_find_food_lock_and_retry_success() {
// given
Long foodId = 1L;
Food expectedFood = createFood(foodId);

// when
when(foodRepository.findFoodByFoodIdWithPessimisticLock(anyLong())).thenReturn(expectedFood);
Food foundFood = foodRepositoryFacade.findFoodWithLockAndRetry(foodId);

// then
assertThat(foundFood.getId()).isEqualTo(foodId);
}

@Test
@DisplayName("첫 번째 시도는 실패하고 두 번째 시도에 성공")
void test_find_food_lock_and_retry_fail_on_first_then_success() {
// given
Long foodId = 1L;
Food expectedFood = createFood(foodId);

// when
when(foodRepository.findFoodByFoodIdWithPessimisticLock(anyLong()))
.thenThrow(new PessimisticLockException())
.thenReturn(expectedFood);

Food foundFood = foodRepositoryFacade.findFoodWithLockAndRetry(foodId);

// then
assertThat(foundFood.getId()).isEqualTo(foodId);
}

@Test
@DisplayName("락 타임아웃으로 최대 재시도 후 실패")
void test_find_food_lock_and_retry_fail() {
// given
Long foodId = 1L;

// when
when(foodRepository.findFoodByFoodIdWithPessimisticLock(anyLong()))
.thenThrow(new PessimisticLockException())
.thenThrow(new LockTimeoutException())
.thenThrow(new LockTimeoutException());

assertThatThrownBy(() -> foodRepositoryFacade.findFoodWithLockAndRetry(foodId))
.isInstanceOf(RuntimeException.class)
.hasMessage("현재 너무 많은 요청을 처리하고 있습니다. 다시 시도해주세요");
}


private Food createFood(Long foodId) {
return Food.builder()
.id(foodId)
.build();
}
}

0 comments on commit f0bebbc

Please sign in to comment.