[Redis] 로그인 없는 장바구니 실습

RyECODING·2024년 8월 18일

MSA

목록 보기
10/15

로그인 없는 장바구니 기능을 만들어보자. => Spring Security 없이 간단하게 실습

  1. 데이터 타입은 Hash를 사용한다.
  2. 특정 사용자의 장바구니가 사용된 지 3시간이 지나면 삭제되도록 조정한다. // expire
  3. 장바구니에 물품 조정, 장바구니 조회 기능이 존재한다.
    1. 특별한 Entity의 추가 구현 없이, 대상 물품과 수량은 클라이언트가 정해서 전달한다고 가정한다.
    2. 만약 수량을 줄이고 싶다면 음수가 전달된다.
    3. 수량이 0 이하가 되면 장바구니에서 제거된다.
  4. 여러 애플리케이션 인스턴스에 걸쳐 부하가 분산됨을 가정한다. // Session clustering

✔️ application.yml

spring:
  data:
    redis:
      host: localhost
      port: 설정한 port 번호
      username: 설정한 username
      password: 설정한 password

✔️ cartDto.class

package com.example.redis;

import lombok.*;

import java.util.Date;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CartDto {
    private Set<CartItemDto> items;
    private Date expireAt;

    public static CartDto fromHashPairs(
            Map<String, Integer> entries,
            Date expireAt
    ) {
        return CartDto.builder()
                .items(entries.entrySet().stream()
                        .map(entry -> CartItemDto.builder()
                                .item(entry.getKey())
                                .count(entry.getValue())
                                .build())
                        .collect(Collectors.toUnmodifiableSet()))
                .expireAt(expireAt)
                .build();
    }
}

✔️ CartItemDto.class

package com.example.redis;

import lombok.*;

@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CartItemDto {
    private String item;
    private Integer count;
}

✔️ CartController.class

package com.example.redis;

import jakarta.servlet.http.HttpSession;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

@Slf4j
@RestController
@RequestMapping("cart")
public class CartController {
    private final CartService cartService;
    public CartController(CartService cartService) {
        this.cartService = cartService;
    }

    @PostMapping
    public CartDto modifyCart(
            @RequestBody
            CartItemDto itemDto,
            HttpSession session
    ) {
        cartService.modifyCart(session.getId(), itemDto);
        return cartService.getCart(session.getId());
    }

    @GetMapping
    public CartDto getCart(
            HttpSession session
    ) {
        log.info(session.getId());
        return cartService.getCart(session.getId());
    }
}

✔️ CartService.class

package com.example.redis;

import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.Optional;

@Slf4j
@Service
public class CartService {
    private final String keyString = "cart:%s";
    private final RedisTemplate<String, String> cartTemplate;
    private final HashOperations<String, String, Integer> hashOps;

    public CartService(RedisTemplate<String, String> cartTemplate) {
        this.cartTemplate = cartTemplate;
        this.hashOps = this.cartTemplate.opsForHash();
    }

    public void modifyCart(String sessionId, CartItemDto dto) {
        hashOps.increment(
                keyString.formatted(sessionId),
                dto.getItem(),
                dto.getCount()
        );
        int count = Optional.ofNullable(hashOps.get(keyString.formatted(sessionId), dto.getItem()))
                .orElse(0);
        if (count <= 0) {
            hashOps.delete(keyString.formatted(sessionId), dto.getItem());
        }
    }

    public CartDto getCart(String sessionId) {
        boolean exists = Optional.ofNullable(cartTemplate.hasKey(keyString.formatted(sessionId)))
                .orElse(false);
        if (!exists)
            throw new ResponseStatusException(HttpStatus.NOT_FOUND);
        Date expireAt = Date.from(Instant.now().plus(30, ChronoUnit.SECONDS));
        cartTemplate.expireAt(
                keyString.formatted(sessionId),
                expireAt
        );
        return CartDto.fromHashPairs(
                hashOps.entries(keyString.formatted(sessionId)),
                expireAt
        );
    }
}

✔️ RedisConfig.class

package com.example.redis.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericToStringSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;

@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, String> cartTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setKeySerializer(RedisSerializer.string());
        redisTemplate.setHashKeySerializer(RedisSerializer.string());
        redisTemplate.setHashValueSerializer(new GenericToStringSerializer<>(Integer.class));
        return redisTemplate;
    }
}
profile
례코드

0개의 댓글