https://www.notion.so/Redis-1e5859344fa4800f9485e2130c05d5ca?pvs=4
📘 “장바구니를 RDB로 만들었는데, 왜 Redis가 자꾸 생각났을까?”
처음에는 "장바구니"도 그냥 DB에 저장하면 된다고 생각했다.
어차피 유저가 담는 정보고, 주문할 땐 필요하니까 영속성이 있어야 한다고 판단.
그래서 당연하게 MySQL(RDB)에 Cart, CartItem 테이블을 만들고 구현했는데...
하지만 몇 가지 문제가 눈에 밟히기 시작했다.
✅ 장바구니는 유저가 잠깐 사용하는 데이터인데...
→ 하루 뒤엔 없어져야 할 데이터를 DB에 영원히 저장하고 있음
→ 스케줄러 짜야 하고, 삭제 조건도 복잡해짐
✅ 변동이 많고, 쓰기/삭제도 빈번
→ 장바구니는 담았다 뺐다 하는 게 기본인데, 매번 DB 트랜잭션으로 처리하자니 부담됨
✅ 읽기/쓰기도 생각보다 많다
→ 사용자 수가 많아지면 디스크 I/O가 부담스러워질 수도 있다는 걱정
이런 조건을 정리해봤다:
오래 보관할 필요 없음 → 휘발성
빠르게 읽고 써야 함 → 속도
일정 시간 뒤 자동 삭제되면 좋겠음 → TTL 지원
이걸 만족하는 게 바로 Redis였다.
| 테이블 | 역할 설명 | 저장 위치 | 비고 |
|---|---|---|---|
| User | 사용자 정보 및 권한 | MySQL | 보안 및 회원 관리 |
| Store | 가게 정보 | MySQL | 고정 정보, 자주 변경되지 않음 |
| Menu | 메뉴 정보 (이름, 가격 등) | MySQL | 고정 데이터, ID로 접근 |
| Cart | 사용자별, 가게별 장바구니 (24시간 보관) | ✅ Redis | TTL 설정, 휘발성 데이터 |
| CartItem | 장바구니 내 메뉴와 수량 | ✅ Redis | Cart에 포함하여 함께 저장 |
| OrderTable | 주문 정보 (주문 상태, cart 참조) | MySQL | 영속성 유지 필요 |
| Review | 주문에 대한 사용자 후기 | MySQL | 통계/조회용, 변경 거의 없음 |
단순히 Redis를 사용하는 정도면 RedisConfig 없어도 됨
저장 형식을 JSON 등으로 바꾸고 싶거나, 성능 튜닝, 보안, 캐싱 전략 추가 등을 하고 싶다면 RedisConfig를 따로 만들어주는 게 좋다.
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;
}
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);
}
}
}
CartService에서 Redis를 사용하여 장바구니 추가, 조회, 삭제를 처리합니다. 기존의 CartRepository와 CartItemRepository는 이제 RedisService로 대체했다.
@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);
}
@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의 데이터 만료 기능을 활용해 장바구니 데이터의 휘발성을 자연스럽게 관리할 수 있었고.
특히, Cart와 CartItems 데이터를 Redis에 저장하면서 DB 부하를 줄이고 애플리케이션 응답 속도가 개선된거 같다. 수량을 추가/삭제하고 장바구니가 비어 있으면 Redis에서 해당 데이터를 삭제하는 방식으로 장바구니 관리가 간소화되고 효율적이다.