Framework/프로젝트로 스프링 이해하기

[이커머스 프로젝트] 장바구니 구현하기 (RedisTemplate)

simDev1234 2022. 12. 15. 23:56

|  개요

- RedisTemplate을 사용하여 각 회원들에 대한 장바구니를 저장

- serialize과 deserialize를 String으로 쓰는 StringRedisTemplate을 사용

- Redis에 저장되는 형태

구분 데이터 형태
key cart:hash_id
value 각 회원의 장바구니 객체를 Json 타입으로 저장

 

|  절차

1. Build.gradle에 Redis 추가

// redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
testImplementation('it.ozimov:embedded-redis:0.7.3'){
    exclude group: 'org.slf4j', module: 'slf4j-simple'
}

 

2. application.properties 호스트와 포트 작성

spring.redis.host=localhost
spring.redis.port=6379

 

3. RedisConfig 작성

@Configuration
@EnableRedisRepositories
public class RedisConfig {

    @Value("${spring.redis.host}")
    private String redisHost;

    @Value("${spring.redis.port}")
    private int redisPort;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(redisHost, redisPort);
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {

        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setKeySerializer(stringRedisSerializer);
        redisTemplate.setValueSerializer(stringRedisSerializer);
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        redisTemplate.setHashValueSerializer(stringRedisSerializer);

        return redisTemplate;

    }

}

 

4. Redis에 저장할 장바구니 객체

- 한 회원 당 하나의 장바구니가 저장되고, 각각의 장바구니는 서로 독립적인 개체이다. 

- @RedisHash와 @Id는 Redis의 Key를 설정할 때 사용한다.

  > 예를 들어, @RedisHash(value = "fund")이고, @Id 로 Long id 가 주어진다고 할 때,

  > 아래의 그림과 같이 "fund: 1"로 키값이 설정된다.

  > 이렇게되면 Redis에서 "get fund:1" 명령어를 칠 때 해당 값이 추출된다.

- 이번 프로젝트의 경우에는 "cart: 1"과 같이 값이 저장될 예정이다.

출처:https://velog.io/@sa1341/Redis%EB%9E%80

@Data
@NoArgsConstructor
@RedisHash("cart")
public class Cart {

  @Id
  private Long customId;

  private List<Product> products = new ArrayList<>();

  @Data
  @Builder
  @AllArgsConstructor
  @NoArgsConstructor
  public static class Product{
    private Long id;
    private Long sellerId;
    private String name;
    private String description;
    private List<ProductItem> items = new ArrayList();
  }

  @Data
  @Builder
  @AllArgsConstructor
  @NoArgsConstructor
  public static class ProductItem{
    private Long id;
    private String name;
    private Integer count;
    private Integer price;
  }

}

 

5. RedisClient

- RedisTemplate을 유용하게 사용하기 위해서 RedisClient를 썼다.

- 제너릭을 통해서 키와 값을 받으면 이를 redis에 저장하는 방식이다.

- 수업에는 @Service를 사용했는데, redis에 데이터를 저장하고 조회하는 메소드를 사용하기 때문에 이 어노테이션을 사용한 것 같다. 찾아보니, 여기서 한 발짜국 더 나아가면 RedisTemplate를 쓰지 않고 RedisRepository를 사용할 수도 있다고 한다.

 

※ RedisTemplate 메서드

- 출처 : https://sabarada.tistory.com/105

메서드 설명
opsForValue Strings를 쉽게 Serialize / Deserialize 해주는 Interface
opsForList List를 쉽게 Serialize / Deserialize 해주는 Interface
opsForSet Set를 쉽게 Serialize / Deserialize 해주는 Interface
opsForZSet ZSet를 쉽게 Serialize / Deserialize 해주는 Interface
opsForHash Hash를 쉽게 Serialize / Deserialize 해주는 Interface

- RedisTemplate은 각 자료구조에 맞는 메소드를 제공한다.

- String의 경우, opsForValue를 사용한다.

@Service
@RequiredArgsConstructor
@Slf4j
public class RedisClient {

  private final RedisTemplate<String, Object> redisTemplate;

  private static final ObjectMapper mapper = new ObjectMapper();

  public <T> T get(Long key, Class<T> classType) {
    return get(key.toString(), classType);
  }

  private <T> T get(String key, Class<T> classType) {

    String redisValue = (String) redisTemplate.opsForValue().get(key);

    if (ObjectUtils.isEmpty(redisValue)) {
      return null;
    } else {
      try {
        return mapper.readValue(redisValue, classType);
      } catch (JsonProcessingException e) {
        log.error("Parsing error", e);
        return null;
      }
    }
  }

  public void put(Long key, Cart cart) {
    put(key.toString(), cart);
  }

  private void put(String key, Cart cart) {
    try {
      redisTemplate.opsForValue().set(key, mapper.writeValueAsString(cart));
    } catch (JsonProcessingException e) {
      throw new CustomException(ErrorCode.CART_CHANGE_FAIL);
    }
  }

}

 

6. 서비스 클래스 작성

package org.zerobase.cms.order.service;

import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.zerobase.cms.order.client.RedisClient;
import org.zerobase.cms.order.domain.product.AddProductCartForm;
import org.zerobase.cms.order.domain.redis.Cart;
import org.zerobase.cms.order.domain.redis.Cart.ProductItem;

@Service
@Slf4j
@RequiredArgsConstructor
public class CartService {

  private RedisClient redisClient;

  public Cart addCart(Long customerId, AddProductCartForm form) {

    Cart cart = redisClient.get(customerId, Cart.class);

    if (cart == null) {
      cart = new Cart();
      cart.setCustomerId(customerId);
    }

    // 이전에 같은 상품이 있는지 확인
    Optional<Cart.Product> productOptional = cart.getProducts().stream()
        .filter(product1 -> product1.getId().equals(form.getId()))
        .findFirst();

    if (productOptional.isPresent()) {
      Cart.Product redisProduct = productOptional.get();
      // requested
      List<Cart.ProductItem> items = form.getItems().stream().map(Cart.ProductItem::from).collect(
          Collectors.toList());
      Map<Long, ProductItem> redisItemMap = redisProduct.getItems().stream().collect(Collectors.toMap(it -> it.getId(), it -> it));

      if (!redisProduct.getName().equals(form.getName())) {
        cart.addMessage(redisProduct.getName() + "의 정보가 변경이 되었습니다. 확인 부탁드립니다.");
      }

      for (Cart.ProductItem item : items) {
        Cart.ProductItem redisItem = redisItemMap.get(item.getId());

        if (redisItem == null) {
          // happy case
          redisProduct.getItems().add(item);
        } else {
          if (redisItem.getPrice().equals(item.getPrice())) {
            cart.addMessage(redisProduct.getName() + item.getName() + "의 가격이 되었습니다. 확인 부탁드립니다.");
          }
          redisItem.setCount(redisItem.getCount() + item.getCount());
        }

      }

    } else {
      Cart.Product product = Cart.Product.from(form);
      cart.getProducts().add(product);
    }

    redisClient.put(customerId, cart);
    return cart;
  }

}

 

 

[ 출처 & 참고 ]

부트캠프 내용 정리

https://velog.io/@sa1341/Redis%EB%9E%80

https://sabarada.tistory.com/105