배달의 민족 B마트라는 서비스를 클론하는 프로젝트에서 신상품을 조회하는 API를 만들었다.
처음에는 DB에서 createdAt이 2주 이내인 상품들만 조회하도록 하였다.
@Override
public List<Item> findNewItemsOrderBy(Long lastIdx, Long lastItemId, ItemSortType sortType,
Pageable pageable) {
OrderSpecifier orderSpecifier = createOrderSpecifier(sortType);
Predicate predicate = item.createdAt.after(
LocalDateTime.now().minus(NEW_PRODUCT_REFERENCE_TIME, ChronoUnit.WEEKS));
return queryFactory
.selectFrom(item)
.leftJoin(item.reviews, review)
.leftJoin(item.likeItems, likeItem)
.leftJoin(item.orderItems, orderItem)
.where(predicate)
.groupBy(item)
.having(
getHavingCondition(lastIdx, lastItemId, sortType)
)
.orderBy(orderSpecifier, item.itemId.asc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
}
이후 2차 스프린트에서 Redis를 활용하여 신상품 조회를 하는 것으로 리팩토링하기로 했다!
build.gradle
// cache
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
RedisConfig.java
@EnableCaching
@Configuration
public class RedisConfig {
@Value("${spring.data.redis.host}")
private String host;
@Value("${spring.data.redis.port}")
private int port;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(host, port);
}
@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;
}
}
내가 사용하고 싶은 타입을 설정하여 Bean으로 등록하면 된다!
Redis에 DTO로 올리고 싶어 만들어줬다.
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()
);
}
}
@Service
@RequiredArgsConstructor
public class ItemCacheService {
private final RedisTemplate<String, ItemRedisDto> redisTemplate;
private static final String NEW_PRODUCTS_KEY = "new_products";
// 아이템을 등록할 때 해당 함수를 호출하여 Redis에도 생성한다!
public void saveNewItem(final ItemRedisDto itemRedisDto) {
redisTemplate.opsForList().rightPush(NEW_PRODUCTS_KEY, itemRedisDto);
}
// Redis에서 new_product라는 키로 등록된 데이터를 조회한다.
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);
}
// 자정마다 생성된지 2주가 지난 상품을 Redis에서 삭제한다.
@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--;
}
}
}
}