Redis 기반 메뉴 캐싱과 장바구니 기능 구현

메뉴 데이터 캐싱을 통한 성능 최적화

1. Redis를 활용한 메뉴 정보 캐싱

사용자 앱에서 메뉴 목록을 요청할 때마다 데이터베이스 조회가 발생하면, 트래픽 증가 시 DB 부하가 심화되어 응답 지연이 발생할 수 있다. 이를 해결하기 위해 Redis를 사용하여 자주 조회되는 메뉴 데이터를 캐싱하는 전략을 도입한다.

캐싱 전략 설계

  • 카테고리별로 메뉴 목록을 별도의 키로 저장 (예: menu_category_101)
  • 메뉴 정보 변경(등록/수정/삭제) 시 관련 캐시 무효화

Controller 레이어 - 캐시 조회 로직 추가

@Autowired
private StringRedisTemplate redisTemplate;

@GetMapping("/list")
@ApiOperation("카테고리별 메뉴 목록 조회")
public Result<List<DishVO>> listByCategory(Long categoryId) {
    String key = "menu_category_" + categoryId;
    
    // Redis에서 캐시된 데이터 조회
    String cachedData = redisTemplate.opsForValue().get(key);
    if (cachedData != null) {
        List<DishVO> menuList = JsonUtil.toList(cachedData, DishVO.class);
        return Result.success(menuList);
    }

    // 캐시 미존재 시 DB 조회 및 캐싱
    Dish query = new Dish();
    query.setCategoryId(categoryId);
    query.setStatus(ENABLE);
    
    List<DishVO> menuList = dishService.listWithFlavor(query);
    redisTemplate.opsForValue().set(key, JsonUtil.toJson(menuList), Duration.ofMinutes(10));
    
    return Result.success(menuList);
}

공통 캐시 제거 유틸리티 메서드

/**
 * 와일드카드 패턴에 맞는 모든 캐시 항목 삭제
 */
private void invalidateCache(String pattern) {
    Set<String> keys = redisTemplate.keys(pattern);
    if (!CollectionUtils.isEmpty(keys)) {
        redisTemplate.delete(keys);
    }
}

데이터 변경 시 캐시 동기화 예시

@PostMapping
@ApiOperation("신규 메뉴 등록")
public Result addMenu(@RequestBody DishDTO dto) {
    dishService.saveWithFlavor(dto);
    
    // 해당 카테고리 캐시 삭제
    invalidateCache("menu_category_" + dto.getCategoryId());
    return Result.success();
}

@DeleteMapping
@ApiOperation("메뉴 일괄 삭제")
public Result batchDelete(@RequestParam List<Long> ids) {
    dishService.deleteBatch(ids);
    
    // 전체 메뉴 캐시 초기화
    invalidateCache("menu_category_*");
    return Result.success();
}

Spring Cache 어노테이션을 이용한 세트메뉴 캐싱

1. Spring Cache 개요

Spring에서 제공하는 선언적 캐싱 추상화 기능으로, 어노테이션만으로 캐시 논리를 구현할 수 있다. 백엔드로는 Redis, Caffeine 등 다양한 구현체를 사용 가능하다.

2. 주요 어노테이션

어노테이션설명
@EnableCaching프로젝트 전역 캐싱 기능 활성화 (부트스트랩 클래스에 적용)
@Cacheable메서드 실행 전 캐시 확인, 존재 시 반환, 없으면 실행 후 저장
@CacheEvict지정된 캐시 항목 제거
@CachePut메서드 결과를 강제로 캐시에 저장

3. 세트메뉴 캐싱 적용

부트스트랩 클래스에 @EnableCaching 추가 후, 다음과 같이 어노테이션을 적용:

@GetMapping("/list")
@ApiOperation("카테고리별 세트메뉴 조회")
@Cacheable(value = "setmeal", key = "#categoryId")
public Result<List<Setmeal>> getSetmeals(Long categoryId) {
    Setmeal condition = new Setmeal();
    condition.setCategoryId(categoryId);
    condition.setStatus(ENABLE);
    
    List<Setmeal> result = setmealService.list(condition);
    return Result.success(result);
}

4. 데이터 변경 시 캐시 무효화

@PostMapping
@ApiOperation("세트메뉴 등록")
@CacheEvict(value = "setmeal", key = "#dto.categoryId")
public Result create(@RequestBody SetmealDTO dto) {
    setmealService.saveWithDish(dto);
    return Result.success();
}

@DeleteMapping
@ApiOperation("세트메뉴 일괄 삭제")
@CacheEvict(value = "setmeal", allEntries = true)
public Result delete(@RequestParam List<Long> ids) {
    setmealService.deleteBatch(ids);
    return Result.success();
}

장바구니 기능 구현

1. 요구사항

  • 사용자는 메뉴 또는 세트메뉴를 장바구니에 담을 수 있음
  • 동일 상품은 수량 증가로 처리 (중복 레코드 방지)
  • 맛 선택이 필요한 메뉴는 옵션 포함 저장

2. 데이터 모델 (shopping_cart)

컬럼타입설명
user_idbigint소유자 사용자 ID
dish_idbigint선택한 메뉴 ID (null 가능)
setmeal_idbigint선택한 세트메뉴 ID (null 가능)
numberint수량
dish_flavorvarchar(50)맛 옵션

3. 핵심 비즈니스 로직

@Service
public class ShoppingCartServiceImpl implements ShoppingCartService {

    @Override
    public void addToCart(ShoppingCartDTO dto) {
        ShoppingCart cartItem = new ShoppingCart();
        BeanUtils.copyProperties(dto, cartItem);
        cartItem.setUserId(BaseContext.getCurrentId());

        // 중복 항목 존재 여부 확인
        List<ShoppingCart> existing = shoppingCartMapper.findByCriteria(cartItem);

        if (!existing.isEmpty()) {
            // 수량 증가
            ShoppingCart item = existing.get(0);
            item.setNumber(item.getNumber() + 1);
            shoppingCartMapper.updateQuantity(item);
        } else {
            // 신규 등록
            enrichCartItem(cartItem, dto);
            cartItem.setNumber(1);
            cartItem.setCreateTime(LocalDateTime.now());
            shoppingCartMapper.insert(cartItem);
        }
    }

    private void enrichCartItem(ShoppingCart cartItem, ShoppingCartDTO dto) {
        if (dto.getDishId() != null) {
            Dish dish = dishMapper.getById(dto.getDishId());
            cartItem.setName(dish.getName());
            cartItem.setImage(dish.getImage());
            cartItem.setAmount(dish.getPrice());
        } else {
            Setmeal meal = setmealMapper.getById(dto.getSetmealId());
            cartItem.setName(meal.getName());
            cartItem.setImage(meal.getImage());
            cartItem.setAmount(meal.getPrice());
        }
    }
}

4. 추가 기능

  • 장바구니 조회: 현재 사용자의 모든 항목 반환
  • 전체 비우기: 사용자 ID 기준 모든 항목 삭제
  • 단일 항목 감소: 수량 1 감소, 0이면 레코드 삭제
@DeleteMapping("/item")
public Result reduceItem(@RequestBody ShoppingCartDTO dto) {
    ShoppingCart criteria = new ShoppingCart();
    BeanUtils.copyProperties(dto, criteria);
    criteria.setUserId(BaseContext.getCurrentId());

    List<ShoppingCart> items = shoppingCartMapper.findByCriteria(criteria);
    if (items.isEmpty()) return Result.success();

    ShoppingCart item = items.get(0);
    if (item.getNumber() <= 1) {
        shoppingCartMapper.deleteById(item.getId());
    } else {
        item.setNumber(item.getNumber() - 1);
        shoppingCartMapper.updateQuantity(item);
    }
    return Result.success();
}

태그: Redis Spring Cache java MyBatis 장바구니

5월 20일 02:05에 게시됨