[쇼핑몰 만들기 프로젝트] - 장바구니(2)

yeom yaloo·2023년 8월 6일
0

쇼핑몰

목록 보기
15/19
post-thumbnail

[들어가기에 앞서...]

  • spring-data-redis 그중에서도 redisTemplate을 사용하는 나는 여러 자료구조 중 어떤 자료구조를 사용해 데이터를 저장하고 관리하고 있을까?
  • 또 레디스의 만료기간 지정과 함께 어떻게 해당 자료구조의 데이터를 가져와서 사용하고 있을까?
  • 여러 곳에서 공통으로 사용되는 데이터를 어떻게 코드의 중복 없이 사용가능할까?

[요구 사항]

[header section]

1. 해당 header 섹션 장바구니 안에 담긴 상품의 갯수를 노출시키고 싶다.

2. 이때 header 자체는 thymeleaf의 fragments로 만들어서 사용하고 있으며 이는 여러곳에서 사용된다.

3. 여러곳에서 같은 데이터를 위해서 같은 코드를 중복하고 싶지 않아 이를 한 번에 처리하였으면 한다.

해당 프로젝트는 위와 같은 요구사항이 있다고 가정한 뒤 해당 작업을 진행하였습니다.

[hash를 사용하여 레디스에 저장한 이유]

1. 레디스가 제공하는 많고 많은 자료구조

Spring Data Redis provides support for various Redis data structures, allowing you to interact with Redis in a more convenient way. Here are the main Redis data structures supported by Spring Data Redis:

  1. String Redis Template (RedisTemplate<String, String>):
    • Represents the simplest data structure in Redis.
    • Allows you to store and retrieve simple key-value pairs (strings).
    1. List Redis Template (ListOperations<String, String>):
    • Represents Redis Lists.
    • Allows you to store and manipulate lists of strings.
    • Provides operations like leftPush, rightPush, leftPop, rightPop, etc.
    1. Set Redis Template (SetOperations<String, String>):
    • Represents Redis Sets.
    • Allows you to store and manipulate sets of strings.
    • Provides operations like add, remove, intersect, union, etc.
    1. Hash Redis Template (HashOperations<String, String, String>):
    • Represents Redis Hashes.
    • Allows you to store and retrieve maps of key-value pairs.
    • Provides operations like put, get, delete, entries, etc.
    1. ZSet Redis Template (ZSetOperations<String, String>):
    • Represents Redis Sorted Sets (ZSets).
    • Allows you to store elements with associated scores, sorted by the scores.
    • Provides operations like add, remove, range, rank, etc.
    1. Value Redis Template (ValueOperations<String, String>):
    • Represents an abstraction for common Redis operations using String keys and String values.
    • Provides simple methods like set, get, increment, etc.

2. 그중 왜 hash인가?

  • 하나의 키에 여러 필드를 저장할 수 있어서 uuid, loginid와 같은 단일 key로 producId, quantity등을 저장해서 사용하고 빠르게 접근할 수 있기때문에 이를 선택해 사용했습니다.
  • 삽입, 삭제, 수정, 조회 등 연산 작업이 hash는 모두 O(1) 시간복잡도를 가지고 있기 때문에 많은 양의 데이터가 저장되어도 빠르게 해당 작업들을 수행할 수 있을 것이라 생각하여 hash를 선택했습니다.
  • 하나의 키로 여러 데이터를 묶어서 저장할 수 있기 때문에 메모리 효율성이 뛰어나 hash를 선택했습니다.
  • 필요할 때 전체 데이터를 가져오는 String(문자열)에 비해서 hash는 필드를 직접 지정하여 조회, 수정이 가능하기 때문에 더 효율적인 작업이 가능하여 hash를 선택하여 장바구니 정보를 저장했습니다.

[spring-data-redis]

1. 내 프로젝트에서 레디스는 어떻게 사용되고 있을까?

  • spring에서 redis를 좀 더 사용하기 쉽게 하려고 지원하는 라이브러리이다.
  • 이때 lettuce를 구현체로 사용하고 있다.
  • spring 내에서 redis는 redisRepository OR redisTemplate을 사용할 수 있다.
  • 이때 나의 프로젝트는 redisTemplate을 사용해서 레디스에 연산을 진행하고 있다.(연산 = 데이터 저장, 수정, 삭제 등..)

2. 장바구니 기능은 어떻게 구현되었을까?

  • 장바구니 구현에 있어서 cookie와 redis를 사용한다.
  • cookie 이름은 CART_NO으로 지정해주었다. cookie엔 회원의 경우 회원 로그인 아이디를 비회원의 경우엔 랜덤으로 발급한 uuid를 쿠키의 값으로 넣어주었다.
  • redis를 사용해서 해당 회원, 비회원의 쿠키 값을 키값으로 사용하여 장바구니 정보를 저장했다.
  • 쿠키와 레디스의 장바구니 상품 만료 기한을 회원, 비회원에 따라 다르게 두어 설정해 사용한다. (비회원 3일/회원 30일)

3. RedisTemplate을 사용한 장바구니 정보를 어떻게 저장하고 있을까?

[쿠키를 사용해서 회원 여부 확인, 장바구니에 담긴 상품이 있는지 확인]

public String addToCart(@CookieValue(name = "CART_NO") Cookie cookie, 
						@CookieValue(name = "HEADER_UUID") Cookie member, 
                        HttpServletResponse response,
                       	@RequestBody request){
                        
  // HEADER_UUID라는 쿠키가 있으면 로그인한 회원으로 인식하여 loginId를 쿠키 값으로 넣어준 뒤 쿠키 생성을 하고 응답 객체에 보낸다. 
  if(Objects.nonNull(member)){
      String loginId = SecurityContextHolder.getContext().getAuthentication().getName();
      cookie = cookieUtils.createCookie("CART_NO", loginId,60*60*24*30);

      response.addCookie(cookie);
  }

  // 비회원이면서 장바구니에 어떤 상품도 담겨있지 않은 상태라면 비회원용 장바구니 UUID를 발급해서 넣어준다.
  if(Objects.isNull(cookie)){
      String uuid = String.valueOf(UUID.randomUUID());
      cookie = cookieUtils.createCookie("CART_NO", uuid,  60 * 60 * 24 * 3);
      response.addCookie(cookie);
  }
}

[RedisTemplate을 사용해 해당 장바구니에 담고자 한 정보를 저장하기]

redisTemplate.opsForHash().put(cookie.getValue(), request.getProductId(), quantity);
  • hash를 사용해서 해당 데이터를 저장한다.
  • cookie에 저장한 값(회원이라면 login id, 비회원이라면 발급해준 uuid)을 redis 키로 저장한다. key:{HashKey, HashValue}
  • key(uuid or loginId): {productId = quantity} 형식으로 레디스에 장바구니 정보를 저장한다.

4. 해시 이외에도 많은 자료구조를 지원하는 레디스

5. 만료기간 저장


if(Objects.isNull(member)){
	// 비회원의 경우 3일 뒤 쿠키 만료 -> redis 데이터도 3일뒤 삭제
	redisTemplate.expire(cookie.getValue(), 3, TimeUnit.DAYS);
} else{ 
	//회원의 경우 30일 뒤 장바구니 쿠키 삭제 -> redis에 저장된 데이터도 삭제
	redisTemplate.expire(cookie.getValue(), 30, TimeUnit.DAYS);
}
  • redisTemplate에서 지원하는 expire() 메서드를 사용해서 해당 만료 정보를 설정해준다.
  • 이때 쿠키의 경우 생성할 때 만료 기간을 지정하는 부분이 있기 때문에 이를 사용한다. (= Cookie.setMaxAge())

6. hash를 가져와서 장바구니에 들어간 상품의 개수를 노출시키기

6-1. opsForHash()를 사용해서 해쉬로 만들기

  • 해당 작업으로 해쉬를 사용해 key: {hashKey:hashValue} 형태로 데이터를 저장한다.

6-2. redis hash 자료구조에 데이터 넣기

redisTemplate.opsForHash().put(cookie.getValue(), request.getProductId(), quantity);
  • put() 메서드를 사용해서 key: {hashKey=hashValue}형태로 값 저장

6-3. 저장된 데이터 가져오기

package org.springframework.data.redis.core;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.springframework.lang.Nullable;

public interface HashOperations<H, HK, HV> {
    Long delete(H key, Object... hashKeys);

    Boolean hasKey(H key, Object hashKey);

    @Nullable
    HV get(H key, Object hashKey);

    List<HV> multiGet(H key, Collection<HK> hashKeys);

    Long increment(H key, HK hashKey, long delta);

    Double increment(H key, HK hashKey, double delta);

    @Nullable
    HK randomKey(H key);

    @Nullable
    Map.Entry<HK, HV> randomEntry(H key);

    @Nullable
    List<HK> randomKeys(H key, long count);

    @Nullable
    Map<HK, HV> randomEntries(H key, long count);

    Set<HK> keys(H key);

    @Nullable
    Long lengthOfValue(H key, HK hashKey);

    Long size(H key);

    void putAll(H key, Map<? extends HK, ? extends HV> m);

    void put(H key, HK hashKey, HV value);

    Boolean putIfAbsent(H key, HK hashKey, HV value);

    List<HV> values(H key);

    Map<HK, HV> entries(H key);

    Cursor<Map.Entry<HK, HV>> scan(H key, ScanOptions options);

    RedisOperations<H, ?> getOperations();
}

  • entries()를 사용해서 해당 key값으로 저장된 Map<HashKey, HashValue>형태의 데이터를 모두 가져온다.

6-4. 장바구니에 든 상품 개수 구하기

  • 상품 개수를 구하기 위해서 Map에 있는 메서드 중 하나인 size()를 사용했다.
  • map의 특성상 key 중복이 허용되지 않고 value의 중복만 허용되는데 이를 이용해 size()를 가져오면 사용자가 장바구니에 담은 상품의 개수가 구해진다.
  • 1개의 상품을 여러개 담더라도 상품 종류 하나당 1개로 생각해 몇가지의 상품이 담겼는지만 생각했다.

[뷰 페이지에서 공통으로 여러번 사용되는 데이터 처리]

1. 코드 중복

  • 여러페이지에서 여러번 사용되어야 하는 장바구니 개수 정보는 컨트롤러 부분에서 해당 데이터가 필요할때마다 넘겨주어야하는 등의 문제가 있었다.

2. 여러 곳에서 사용되는 데이터 처리(절망)

  • 필요할때마다 데이터를 받아서 Model에 넣어주고 그걸 받아서 사용하는 방법.. 사실 이 방법은 굉장히 싫었고, 실제 이 방법을 사용하지 않기 위해서 머리를 굴렸다.
  • 찾아보니 타임리프 관련해서 global data 사용을 위한 방법이 있었다.
  • 우선 AbstractDialect를 상속받아 구현한 클래스에 AbstractAttributeTagProcessor를 상속 받은 클래스를 추가해서 사용하라는데 자료가 많이 없어서 뭔 말인지 모르고 따라하다 안 되어서 다른 방법을 강구했다.
  • 나에겐 @ModelAttribute라는 방법이 있었고 처음엔 메서드 단에 해당 애너테이션을 붙인 메서드를 필요 컨틀롤러마다 복붙 해줬다.
  • 실질적으로 코드 중복을 줄이려고 계속 생각했었고 그러다 갑자기 생각난 해결법이 있었다.

3. 여러 곳에서 사용되는 데이터 처리(희망)

  • ControllerAdvice가 떠올랐고 이는 좋은 대안이 되었다.
    • 해당 애너테이션은 모든 컨트롤러에 적용(=반응)된다.
  • @controllerAdvice를 달고 있는 클래스에 @HandlerException를 메서드 단에서 달고 있는 것을 사용하면 HandlerException에서 지정한 예외가 특정 컨트롤러가아닌 모든 컨트롤러에서 이를 다룰 수 있게 된다.
  • @ModelAttribute를 메서드 단에서 사용한 경우도 ControllerAdvice의 사용이 가능했다.
    • @ModelAttribute는 메서드단에서 사용하기, 메서드의 파라미터에서 사용하는 방법이 있다 이 두개의 차이점은 다음에 간단하게 포스팅해볼 예정.

4. 실제 코드

import com.yaloostore.front.auth.exception.InvalidHttpHeaderException;
import com.yaloostore.front.common.exception.*;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ModelAttribute;

import java.util.Map;
import java.util.Objects;


/**
 * 에러를 공통으로 처리하기 위해 사용되는 어드바이스 클래스입니다.
 * */
@ControllerAdvice
@Slf4j
@RequiredArgsConstructor
public class WebControllerAdvice {
    private final RedisTemplate redisTemplate;
    /**
     * 장바구니에 담긴 상품의 개수를 처리할 때 사용하는 메서드입니다.
     * 이는 ControllerAdvice 클래스 하위에 두어 필요한 곳에서 사용할 수 있게 하였습니다.
     *
     * @param cartNo redis에 저장한 key를 가지고 있는 쿠키입니다.(회원의 경우 로그인 아이디, 비회원의 경우 랜덤 발급한 uuid
     * @return 회원 장바구니에 담긴 상품의 개수
     * */
    @ModelAttribute(name = "cartProductCounting")
    public int setViewHeaderFrag(@CookieValue(required = false, value = "CART_NO") Cookie cartNo){
        //장바구니가 비어 있는 경우라면?
        if (Objects.isNull(cartNo)){
            return 0;
        }

        if (Objects.nonNull(cartNo)){
            String uuid = cartNo.getValue();
            log.info("uuid = {}", uuid);

            Map<String, String> o = redisTemplate.opsForHash().entries(uuid);
            log.info("size??!?! {}", o.size());
            return o.size();
        }
        return 0;
    }
}

[코드 적용 결과]

1. 빈 장바구니

  • 장바구니에 어떤 상품도 담겨있지 않는 경우엔 해당하는 쿠키도 없고 당연히 redis에도 어떤 데이터도 저장되어 있지 않다.

2.장바구니 추가 후 - 쿠키

  • 비회원이 장바구니에 상품 추가를 한 경우를 예시로 들면 해당 장바구니에 담긴 상품 갯수만큼 화면에 숫자가 나온다.
  • 또한 uuid를 가지고 있는 쿠키가 생성되는 것을 볼수 있다.

3. 장바구니 추가 후 - 레디스에 저장된 데이터

  • 해당 데이터는 설정해준 3일을 기점으로 만료되게 설정된 채로 저장되어 있다.
  • 참고: 위의 redis 관련 GUI로 medis라는 프로그램이다.
profile
즐겁고 괴로운 개발😎

0개의 댓글