Skip to content

Commit

Permalink
Merge pull request #112 from prgrms-be-devcourse/feature/NAYB-139
Browse files Browse the repository at this point in the history
[NAYB-139] feat : 신상품 조회 시 Redis에서 가져온다.
  • Loading branch information
funnysunny08 authored Sep 18, 2023
2 parents c352079 + c01c1a4 commit f8914bd
Show file tree
Hide file tree
Showing 10 changed files with 314 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.prgrms.nabmart.domain.item.controller;

import com.prgrms.nabmart.domain.item.ItemSortType;
import com.prgrms.nabmart.domain.item.controller.request.RegisterItemRequest;
import com.prgrms.nabmart.domain.item.controller.request.UpdateItemRequest;
import com.prgrms.nabmart.domain.item.service.ItemService;
Expand All @@ -11,6 +12,7 @@
import com.prgrms.nabmart.domain.item.service.request.UpdateItemCommand;
import com.prgrms.nabmart.domain.item.service.response.FindItemDetailResponse;
import com.prgrms.nabmart.domain.item.service.response.FindItemsResponse;
import com.prgrms.nabmart.domain.item.service.response.FindNewItemsResponse;
import jakarta.validation.Valid;
import java.net.URI;
import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -70,6 +72,13 @@ public ResponseEntity<FindItemsResponse> findNewItems(
return ResponseEntity.ok(itemService.findNewItems(findNewItemsCommand));
}

@GetMapping("/new-items")
public ResponseEntity<FindNewItemsResponse> findNewItemsWithRedis(
@RequestParam(defaultValue = "NEW") String sort
) {
return ResponseEntity.ok(itemService.findNewItemsWithRedis(ItemSortType.valueOf(sort)));
}

@GetMapping("/hot")
public ResponseEntity<FindItemsResponse> findHotItems(
@RequestParam(defaultValue = DEFAULT_PREVIOUS_ID) Long lastIdx,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.prgrms.nabmart.domain.item.service;

import com.prgrms.nabmart.domain.item.service.response.ItemRedisDto;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.ListOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class ItemCacheService {

private final RedisTemplate<String, ItemRedisDto> redisTemplate;
private static final String NEW_PRODUCTS_KEY = "new_products";

public void saveNewItem(final ItemRedisDto itemRedisDto) {
redisTemplate.opsForList().rightPush(NEW_PRODUCTS_KEY, itemRedisDto);
}

public List<ItemRedisDto> getNewItems() {
ListOperations<String, ItemRedisDto> listOperations = redisTemplate.opsForList();
Long itemCount = listOperations.size(NEW_PRODUCTS_KEY);
if (itemCount == null || itemCount == 0) {
return null;
}

return listOperations.range(NEW_PRODUCTS_KEY, 0, -1);
}

@Scheduled(cron = "0 0 * * * *")
public void deleteOldProducts() {
LocalDateTime twoWeeksAgo = LocalDateTime.now().minus(2, ChronoUnit.WEEKS);

ListOperations<String, ItemRedisDto> items = redisTemplate.opsForList();
Long itemCount = items.size(NEW_PRODUCTS_KEY);

if (itemCount == null || itemCount == 0) {
return;
}

for (int i = 0; i < itemCount; i++) {
ItemRedisDto item = items.index(NEW_PRODUCTS_KEY, i);
if (item != null && item.createdAt().isBefore(twoWeeksAgo)) {
items.remove(NEW_PRODUCTS_KEY, 1, item);
i--;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@
import com.prgrms.nabmart.domain.item.service.request.UpdateItemCommand;
import com.prgrms.nabmart.domain.item.service.response.FindItemDetailResponse;
import com.prgrms.nabmart.domain.item.service.response.FindItemsResponse;
import com.prgrms.nabmart.domain.item.service.response.FindNewItemsResponse;
import com.prgrms.nabmart.domain.item.service.response.FindNewItemsResponse.FindNewItemResponse;
import com.prgrms.nabmart.domain.item.service.response.ItemRedisDto;
import com.prgrms.nabmart.domain.order.repository.OrderItemRepository;
import com.prgrms.nabmart.domain.review.service.RedisCacheService;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -34,7 +40,11 @@ public class ItemService {
private final OrderItemRepository orderItemRepository;
private final MainCategoryRepository mainCategoryRepository;
private final SubCategoryRepository subCategoryRepository;
private static final int NEW_PRODUCT_REFERENCE_WEEK = 2;
private final ItemCacheService itemCacheService;
private final RedisCacheService redisCacheService;

private static final String REVIEW_COUNT_CACHE_KEY = "reviewCount:Item:";
private static final String AVERAGE_RATE_CACHE_KEY = "averageRating:Item:";

@Transactional
public Long saveItem(RegisterItemCommand registerItemCommand) {
Expand All @@ -55,6 +65,7 @@ public Long saveItem(RegisterItemCommand registerItemCommand) {
.build();

Item savedItem = itemRepository.save(item);
itemCacheService.saveNewItem(ItemRedisDto.from(savedItem));
return savedItem.getItemId();
}

Expand Down Expand Up @@ -98,6 +109,33 @@ public FindItemsResponse findNewItems(FindNewItemsCommand findNewItemsCommand) {
findNewItemsCommand.pageRequest()));
}

@Transactional(readOnly = true)
public FindNewItemsResponse findNewItemsWithRedis(ItemSortType sortType) {
List<ItemRedisDto> itemRedisDtos = itemCacheService.getNewItems();
List<FindNewItemResponse> items = itemRedisDtos.stream().map(item -> FindNewItemResponse.of(
item.itemId(),
item.name(),
item.price(),
item.discount(),
redisCacheService.getTotalNumberOfReviewsByItemId(item.itemId(), REVIEW_COUNT_CACHE_KEY + item.itemId()),
redisCacheService.getAverageRatingByItemId(item.itemId(), AVERAGE_RATE_CACHE_KEY + item.itemId())
)).toList();

return FindNewItemsResponse.from(sortNewItems(items, sortType));
}

private List<FindNewItemResponse> sortNewItems(List<FindNewItemResponse> items, ItemSortType sortType) {
List<FindNewItemResponse> sortedItems = new ArrayList<>(items);
switch (sortType) {
case LOWEST_AMOUNT -> sortedItems.sort(Comparator.comparingInt(FindNewItemResponse::price));
case HIGHEST_AMOUNT -> sortedItems.sort(Comparator.comparingInt(FindNewItemResponse::price).reversed());
case NEW -> sortedItems.sort(Comparator.comparingLong(FindNewItemResponse::itemId).reversed());
case DISCOUNT -> sortedItems.sort(Comparator.comparingInt(FindNewItemResponse::discount).reversed());
default -> sortedItems.sort(Comparator.comparingLong(FindNewItemResponse::reviewCount).reversed());
}
return sortedItems;
}

@Transactional
public void updateItem(UpdateItemCommand updateItemCommand) {
Long itemId = updateItemCommand.itemId();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.prgrms.nabmart.domain.item.service.response;

import java.util.List;
import java.util.stream.Collectors;

public record FindNewItemsResponse(List<FindNewItemResponse> items) {

public static FindNewItemsResponse from(List<FindNewItemResponse> items) {
List<FindNewItemResponse> findNewItemResponses = items.stream()
.map(item -> FindNewItemResponse.of(item.itemId, item.name, item.price, item.discount,
item.reviewCount, item.rate))
.collect(Collectors.toList());
return new FindNewItemsResponse(findNewItemResponses);
}

public record FindNewItemResponse(Long itemId, String name, int price, int discount,
Long reviewCount, double rate) {

public static FindNewItemResponse of(final Long itemId, final String name, final int price, final int discount,
final Long reviewCount, final double rate) {
return new FindNewItemResponse(itemId, name, price, discount, reviewCount, rate);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.prgrms.nabmart.domain.item.service.response;

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.prgrms.nabmart.domain.item.Item;
import java.time.LocalDateTime;

public record ItemRedisDto(Long itemId, String name, int price, int discount,
@JsonSerialize(using = LocalDateTimeSerializer.class)
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
LocalDateTime createdAt
) {

public static ItemRedisDto from(final Item item) {
return new ItemRedisDto(
item.getItemId(), item.getName(), item.getPrice(), item.getDiscount(),
item.getCreatedAt()
);
}
}
11 changes: 11 additions & 0 deletions src/main/java/com/prgrms/nabmart/global/config/RedisConfig.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.prgrms.nabmart.global.config;

import com.prgrms.nabmart.domain.item.service.response.ItemRedisDto;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
Expand All @@ -9,6 +10,7 @@
import org.springframework.data.redis.core.ListOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericToStringSerializer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@EnableCaching
Expand Down Expand Up @@ -41,4 +43,13 @@ public ListOperations<String, String> listOperations(
RedisTemplate<String, String> redisStringTemplate) {
return redisStringTemplate.opsForList();
}

@Bean
public RedisTemplate<String, ItemRedisDto> itemRedisDtoRedisTemplate() {
RedisTemplate<String, ItemRedisDto> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(ItemRedisDto.class));
redisTemplate.setConnectionFactory(redisConnectionFactory());
return redisTemplate;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import com.prgrms.nabmart.domain.item.service.request.FindItemsByCategoryCommand;
import com.prgrms.nabmart.domain.item.service.response.FindItemDetailResponse;
import com.prgrms.nabmart.domain.item.service.response.FindItemsResponse;
import com.prgrms.nabmart.domain.item.service.response.FindNewItemsResponse;
import com.prgrms.nabmart.domain.item.support.ItemFixture;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
Expand Down Expand Up @@ -366,4 +367,55 @@ public void deleteItem() throws Exception {
);
}
}

@Nested
@DisplayName("신상품 조회 Api with redis")
class FindNewItemsWithRedis {

FindNewItemsResponse findNewItemsResponse = ItemFixture.findNewItemsResponse();

@Test
@DisplayName("성공")
public void findNewItemsWithRedis() throws Exception {
// Given
given(itemService.findNewItemsWithRedis(any())).willReturn(findNewItemsResponse);


// When
ResultActions resultActions = mockMvc.perform(
get("/api/v1/items/new-items")
.queryParam("lastIdx", "-1")
.queryParam("lastItemId", "-1")
.queryParam("size", "3")
.queryParam("sort", "NEW")
.accept(MediaType.APPLICATION_JSON));

// Then
resultActions.andExpect(status().isOk())
.andDo(document("Find New Items with Redis",
queryParameters(
parameterWithName("lastIdx").description("마지막에 조회한 아이템의 특성값"),
parameterWithName("lastItemId").description("마지막에 조회한 아이템 ID"),
parameterWithName("size").description("조회할 아이템 수"),
parameterWithName("sort").description("정렬 기준명")
),
responseFields(
fieldWithPath("items").type(ARRAY)
.description("List of items"),
fieldWithPath("items[].itemId").type(NUMBER)
.description("상품 ID"),
fieldWithPath("items[].name").type(STRING)
.description("상품 이름"),
fieldWithPath("items[].price").type(NUMBER)
.description("상품 가격"),
fieldWithPath("items[].discount").type(NUMBER)
.description("상품 할인"),
fieldWithPath("items[].reviewCount").type(NUMBER)
.description("리뷰 수"),
fieldWithPath("items[].rate").type(NUMBER)
.description("평점")
)
));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package com.prgrms.nabmart.domain.item.service;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;

import com.prgrms.nabmart.base.RedisTestContainerConfig;
import com.prgrms.nabmart.domain.category.MainCategory;
import com.prgrms.nabmart.domain.category.SubCategory;
import com.prgrms.nabmart.domain.category.fixture.CategoryFixture;
import com.prgrms.nabmart.domain.item.Item;
import com.prgrms.nabmart.domain.item.service.response.ItemRedisDto;
import com.prgrms.nabmart.domain.item.support.ItemFixture;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@Transactional
@SpringBootTest
class ItemCacheServiceTest extends RedisTestContainerConfig {

@Autowired
private ItemCacheService itemCacheService;

@Autowired
private RedisTemplate<String, ItemRedisDto> redisTemplate;

private static final String NEW_PRODUCTS_KEY = "new_products";
Item item;
MainCategory mainCategory;
SubCategory subCategory;

@BeforeEach
void setUp() {
mainCategory = CategoryFixture.mainCategory();
subCategory = CategoryFixture.subCategory(mainCategory);
item = ItemFixture.item(mainCategory, subCategory);
}

@AfterEach
public void cleanupRedis() {
redisTemplate.execute((RedisCallback<Object>) connection -> {
connection.flushDb();
return null;
});
}

@Nested
@DisplayName("새롭게 등록된 상품을 Redis에도 등록한다.")
class SaveItemRedisTest {

@Test
@DisplayName("성공")
void saveNewItem() {
// Given
ItemRedisDto itemRedisDto = ItemRedisDto.from(item);

// When
itemCacheService.saveNewItem(itemRedisDto);

// Then
assertThat(redisTemplate.opsForList().size(NEW_PRODUCTS_KEY)).isEqualTo(1);
}
}

@Nested
@DisplayName("Redis에서 신상품을 조회한다.")
class getNewItemsTest {

@Test
@DisplayName("성공")
void getNewItems() {
// Given
ItemRedisDto itemRedisDto = ItemRedisDto.from(item);
List<ItemRedisDto> expectedItems = List.of(itemRedisDto);

itemCacheService.saveNewItem(itemRedisDto);

// When
List<ItemRedisDto> result = itemCacheService.getNewItems();

// Then
assertThat(result).isEqualTo(expectedItems);
}
}
}
Loading

0 comments on commit f8914bd

Please sign in to comment.