장바구니 24시간 유지? RDB의 한계와 Redis로의 전환 고민기

한잔해·2025년 4월 30일

TroubleShooting

목록 보기
4/4

🚀 Redis란 무엇인가?

https://www.notion.so/Redis-1e5859344fa4800f9485e2130c05d5ca?pvs=4

📘 “장바구니를 RDB로 만들었는데, 왜 Redis가 자꾸 생각났을까?”

🧩 왜 이걸 고민하게 됐나?

처음에는 "장바구니"도 그냥 DB에 저장하면 된다고 생각했다.
어차피 유저가 담는 정보고, 주문할 땐 필요하니까 영속성이 있어야 한다고 판단.
그래서 당연하게 MySQL(RDB)에 Cart, CartItem 테이블을 만들고 구현했는데...

하지만 몇 가지 문제가 눈에 밟히기 시작했다.


🚧 문제 상황

  • ✅ 장바구니는 유저가 잠깐 사용하는 데이터인데...
    → 하루 뒤엔 없어져야 할 데이터를 DB에 영원히 저장하고 있음
    → 스케줄러 짜야 하고, 삭제 조건도 복잡해짐

  • ✅ 변동이 많고, 쓰기/삭제도 빈번
    → 장바구니는 담았다 뺐다 하는 게 기본인데, 매번 DB 트랜잭션으로 처리하자니 부담됨

  • ✅ 읽기/쓰기도 생각보다 많다
    → 사용자 수가 많아지면 디스크 I/O가 부담스러워질 수도 있다는 걱정


💡 그럼 어떤 데이터베이스가 좋을까? (Redis 등장)

이런 조건을 정리해봤다:

  • 오래 보관할 필요 없음 → 휘발성

  • 빠르게 읽고 써야 함 → 속도

  • 일정 시간 뒤 자동 삭제되면 좋겠음 → TTL 지원

이걸 만족하는 게 바로 Redis였다.


✅ 데이터 저장 전략 요약 (Redis vs MySQL)

테이블역할 설명저장 위치비고
User사용자 정보 및 권한MySQL보안 및 회원 관리
Store가게 정보MySQL고정 정보, 자주 변경되지 않음
Menu메뉴 정보 (이름, 가격 등)MySQL고정 데이터, ID로 접근
Cart사용자별, 가게별 장바구니 (24시간 보관)✅ RedisTTL 설정, 휘발성 데이터
CartItem장바구니 내 메뉴와 수량✅ RedisCart에 포함하여 함께 저장
OrderTable주문 정보 (주문 상태, cart 참조)MySQL영속성 유지 필요
Review주문에 대한 사용자 후기MySQL통계/조회용, 변경 거의 없음

🔨 수정 사항

0. RedisConfig

  • 단순히 Redis를 사용하는 정도면 RedisConfig 없어도 됨

  • 저장 형식을 JSON 등으로 바꾸고 싶거나, 성능 튜닝, 보안, 캐싱 전략 추가 등을 하고 싶다면 RedisConfig를 따로 만들어주는 게 좋다.

1. Cart와 CartItem DTO 객체로 변경

Cart와 CartItem 엔티티를 Redis에 저장할 수 있는 DTO 형태로 변경해야 합니다.

public class CartDto {
    private Long storeId;
    private Long userId;
    private Map<Long, Integer> items; // 메뉴 ID와 수량의 맵
}

public class CartItemDto {
    private Long menuId;
    private int quantity;
}

2. Cart Redis Service 추가

Redis에 저장하기 위해서는, Cart를 Redis에 저장하고 불러오는 방식으로 변경해야 합니다. 이를 위해 RedisTemplate을 사용합니다.

@Service
@RequiredArgsConstructor
public class CartRedisService {

    private final RedisTemplate<String, Object> redisTemplate;

    private String getCartKey(Long userId, Long storeId) {
        return "CART:" + userId + ":" + storeId;
    }

    // 장바구니 저장 (TTL 24시간)
    public void saveCart(Long userId, Long storeId, CartDto cartDto) {
        String key = getCartKey(userId, storeId);
        redisTemplate.opsForValue().set(key, cartDto, Duration.ofHours(24));
    }

    // 장바구니 조회
    public CartDto getCart(Long userId, Long storeId) {
        String key = getCartKey(userId, storeId);
        return (CartDto) redisTemplate.opsForValue().get(key);
    }

    // 장바구니 삭제
    public void deleteCart(Long userId, Long storeId) {
        String key = getCartKey(userId, storeId);
        redisTemplate.delete(key);
    }

    // 장바구니 아이템 추가
    public void addItemToCart(Long userId, Long storeId, CartItemDto cartItemDto) {
        CartDto cartDto = getCart(userId, storeId);
        if (cartDto == null) {
            cartDto = new CartDto();
            cartDto.setUserId(userId);
            cartDto.setStoreId(storeId);
        }
        cartDto.getItems().merge(cartItemDto.getMenuId(), cartItemDto.getQuantity(), Integer::sum);
        saveCart(userId, storeId, cartDto);
    }

    // 장바구니 아이템 삭제
    public void removeItemFromCart(Long userId, Long storeId, Long menuId) {
        CartDto cartDto = getCart(userId, storeId);
        if (cartDto != null) {
            cartDto.getItems().remove(menuId);
            saveCart(userId, storeId, cartDto);
        }
    }
}

3. CartService 수정

CartService에서 Redis를 사용하여 장바구니 추가, 조회, 삭제를 처리합니다. 기존의 CartRepository와 CartItemRepository는 이제 RedisService로 대체했다.

Redis에 저장된 장바구니 목록 조회

@Transactional(readOnly = true)
public List<CartsResponseDto> getMyCarts(Long userId) {
    // Redis에서 장바구니 목록 조회
    List<CartDto> cartDtos = new ArrayList<>();
    
    // 여러 가게에 대한 장바구니 정보를 조회
    for (Long storeId : storeRepository.findAllStoreIds()) {
        CartDto cartDto = cartRedisService.getCart(userId, storeId);
        if (cartDto != null) {
            // CartDto에서 CartItemDto 리스트로 변환하여 응답 생성
            List<CartItemDto> cartItemDtos = cartDto.getItems().entrySet().stream()
                    .map(entry -> new CartItemDto(entry.getKey(), entry.getValue()))
                    .collect(Collectors.toList());
            cartDtos.add(new CartsResponseDto(cartDto, cartItemDtos));
        }
    }
    
    return cartDtos;
}

특정 카트 상세 정보 조회

@Transactional(readOnly = true)
public CartDetailResponseDto getCartDetail(Long userId, Long cartId) {
    // Redis에서 카트 정보 조회
    CartDto cartDto = cartRedisService.getCart(userId, cartId);
    
    if (cartDto == null) {
        throw new CustomException(ErrorCode.CART_NOT_FOUND);
    }
    
    // CartDto에서 CartItemDto 리스트로 변환
    List<CartItemDto> cartItemDtos = cartDto.getItems().entrySet().stream()
            .map(entry -> new CartItemDto(entry.getKey(), entry.getValue()))
            .collect(Collectors.toList());

    return new CartDetailResponseDto(cartDto, cartItemDtos);
}

장바구니 추가

@Transactional
public void createCart(Long userId, Long storeId) {
    // Redis에서 카트 존재 여부 확인
    CartDto existingCart = cartRedisService.getCart(userId, storeId);
    
    if (existingCart != null) {
        throw new CustomException(ErrorCode.CART_ALREADY_EXISTS);
    }
    
    // 새로운 Cart 생성
    CartDto newCart = new CartDto();
    newCart.setUserId(userId);
    newCart.setStoreId(storeId);
    newCart.setItems(new HashMap<>());
    
    // Redis에 장바구니 저장 (TTL 24시간 설정)
    cartRedisService.saveCart(userId, storeId, newCart);
}

장바구니에 메뉴 수량을 더하거나 빼고, 수량이 0일 경우 메뉴 삭제

@Transactional
public void updateItemToCart(Long userId, Long storeId, CartItemRequestDto requestDto) {
    // Redis에서 장바구니 조회
    CartDto cartDto = cartRedisService.getCart(userId, storeId);
    
    if (cartDto == null) {
        throw new CustomException(ErrorCode.CART_NOT_FOUND);
    }

    Menu menu = menuRepository.findById(requestDto.getMenuId())
            .orElseThrow(() -> new CustomException(ErrorCode.MENU_NOT_FOUND));

    // 메뉴가 이미 장바구니에 존재하면 수량을 갱신하고, 0이면 삭제
    if (cartDto.getItems().containsKey(menu.getId())) {
        int currentQuantity = cartDto.getItems().get(menu.getId());
        int newQuantity = currentQuantity + requestDto.getQuantity();
        
        if (newQuantity <= 0) {
            // 수량이 0 이하로 떨어지면 아이템 삭제
            cartRedisService.removeItemFromCart(userId, storeId, menu.getId());
        } else {
            // 수량 갱신
            cartDto.getItems().put(menu.getId(), newQuantity);
            cartRedisService.saveCart(userId, storeId, cartDto);
        }
    } else {
        // 장바구니에 해당 메뉴가 없으면 새로 추가
        cartDto.getItems().put(menu.getId(), requestDto.getQuantity());
        cartRedisService.saveCart(userId, storeId, cartDto);
    }
}

📝 결론

이번에 장바구니 구현을 Redi로 변경하면서 얻을 수 있는 주요 장점은 빠른 데이터 처리와 효율적인 리소스 관리였다.RDBMS와 달리 Redis메모리 기반의 데이터 저장소라, 빠른 읽기/쓰기가 가능해서 장바구니 정보 조회와 업데이트가 훨씬 빨라졌다. 또한 Redis데이터 만료 기능을 활용해 장바구니 데이터의 휘발성을 자연스럽게 관리할 수 있었고.

특히, CartCartItems 데이터를 Redis에 저장하면서 DB 부하를 줄이고 애플리케이션 응답 속도가 개선된거 같다. 수량을 추가/삭제하고 장바구니가 비어 있으면 Redis에서 해당 데이터를 삭제하는 방식으로 장바구니 관리가 간소화되고 효율적이다.

profile
항상 배우고 탐구하는 노력하는 개발자

0개의 댓글