Skip to content
This repository has been archived by the owner on Aug 13, 2022. It is now read-only.

Commit

Permalink
#29 게시글 검색시 @Cacheable을 사용하여 RedisCache 적용
Browse files Browse the repository at this point in the history
- redisTemplate json 형식 받을시 값이 깨지지 않게 직렬화하고 클래스 여러개 사용 가능한 bean 등록
- CategoryDTO builder 적용
- ProductSearchController 검색시 초기 캐싱된 게시글로 조회되게 변경
- 사용하지않는 UserDTO RedisHash 삭제
- ProductSearchMapper selectProducts 인자 ProductDTO -> productStatus 변경
- ProductSearchServiceImpl init함수에서 게시글 최대 2000개 redisTemplate에 push
- addRedisKeys, findAllProductsByCacheId 게시글 추가시 캐시 추가하는 함수, 캐싱되어있는 게시글 반환 함수 추가
- RedisKeyFactory redisTemplate 에 key 저장시 종류에따라 key반환하는 클래스 생성
  • Loading branch information
junshock5 committed Sep 7, 2020
1 parent 6e543cd commit 54aa42e
Show file tree
Hide file tree
Showing 10 changed files with 172 additions and 23 deletions.
20 changes: 20 additions & 0 deletions src/main/java/com/market/server/config/RedisConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,26 @@ public RedisConnectionFactory redisConnectionFactory() {
return lettuceConnectionFactory;
}

@Bean
public RedisTemplate<String, Object> redisTemplate(ObjectMapper objectMapper) {
GenericJackson2JsonRedisSerializer serializer =
new GenericJackson2JsonRedisSerializer(objectMapper);

RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
// json 형식으로 데이터를 받을 때
// 값이 깨지지 않도록 직렬화한다.
// 저장할 클래스가 여러개일 경우 범용 JacksonSerializer인 GenericJackson2JsonRedisSerializer를 이용한다
// 참고 https://somoly.tistory.com/134
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(serializer);
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(serializer);
redisTemplate.setEnableTransactionSupport(true); // transaction 허용

return redisTemplate;
}

/**
* Redis Cache를 사용하기 위한 cache manager 등록.<br>
* 커스텀 설정을 적용하기 위해 RedisCacheConfiguration을 먼저 생성한다.<br>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
package com.market.server.controller;

import com.market.server.aop.LoginCheck;
import com.market.server.dto.CategoryDTO;
import com.market.server.service.Impl.CategoryServiceImpl;
Expand Down Expand Up @@ -40,7 +39,13 @@ public void registerCategory(String accountId, @RequestBody CategoryDTO category
public void updateCategories(String accountId,
@PathVariable(name = "categoryId") int categoryId,
@RequestBody CategoryRequest categoryRequest) {
CategoryDTO categoryDTO = new CategoryDTO(categoryId, categoryRequest.getName(), CategoryDTO.SortStatus.NEWEST,10,1);
CategoryDTO categoryDTO = CategoryDTO.builder()
.id(categoryId)
.name(categoryRequest.getName())
.sortStatus(CategoryDTO.SortStatus.NEWEST)
.searchCount(CategoryDTO.SEARCH_COUNT)
.pagingStartOffset(CategoryDTO.PAGING_OFFSET)
.build();
categoryService.update(categoryDTO);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
package com.market.server.controller;

import com.market.server.dto.CategoryDTO;
import com.market.server.dto.ProductDTO;
import com.market.server.service.Impl.ProductSearchServiceImpl;
Expand All @@ -25,6 +24,7 @@ public class ProductSearchController {

/**
* 중고물품 검색 메서드.
* 초기 bean 등록시 2000개의 최신 중고물품 캐싱해온 데이터를 검색
* @params date 물품 생성 날짜,
* price 물품 가격,
* accountId 계정 번호,
Expand All @@ -43,7 +43,12 @@ public class ProductSearchController {
*/
@GetMapping
public ProductSearchResponse search(ProductDTO productDTO,CategoryDTO categoryDTO) {
List<ProductDTO> productDTOList = productSearchService.getProducts(productDTO,categoryDTO);
String accountId = productSearchService.DEFAULT_PRODUCT_CACHE_KEY;
List<ProductDTO> productDTOList = productSearchService.findAllProductsByCacheId(accountId);

if(productDTOList.size() == 0)
productDTOList = productSearchService.getProducts(productDTO,categoryDTO);

return new ProductSearchResponse(productDTOList);
}

Expand Down
19 changes: 13 additions & 6 deletions src/main/java/com/market/server/dto/CategoryDTO.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
package com.market.server.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.*;

@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class CategoryDTO {
public static final int SEARCH_COUNT = 10;
public static final int PAGING_OFFSET = 0;
public enum SortStatus {
CATEGORIES, NEWEST, OLDEST, HIGHPRICE, LOWPRICE, GRADE
}
Expand All @@ -19,4 +17,13 @@ public enum SortStatus {
private int searchCount;
private int pagingStartOffset;

@Builder
public CategoryDTO(@NonNull int id, String name, SortStatus sortStatus, int searchCount, int pagingStartOffset) {
this.id = id;
this.name = name;
this.sortStatus = sortStatus;
this.searchCount = searchCount;
this.pagingStartOffset = pagingStartOffset;
}

}
3 changes: 0 additions & 3 deletions src/main/java/com/market/server/dto/UserDTO.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
package com.market.server.dto;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.data.redis.core.RedisHash;
import java.util.Date;

@Getter
@Setter
@ToString
@RedisHash("accounts")
public class UserDTO {
public enum Status {
DEFAULT, ADMIN, DELETED
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@

@Mapper
public interface ProductSearchMapper {
public List<ProductDTO> selectProducts(ProductDTO productDTO, CategoryDTO categoryDTO);
public List<ProductDTO> selectProducts(String productStatus, CategoryDTO categoryDTO);
}
Original file line number Diff line number Diff line change
@@ -1,27 +1,123 @@
package com.market.server.service.Impl;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.market.server.dto.CategoryDTO;
import com.market.server.dto.ProductDTO;
import com.market.server.mapper.ProductSearchMapper;
import com.market.server.service.ProductSearchService;
import com.market.server.utils.RedisKeyFactory;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@Service
@Log4j2
public class ProductSearchServiceImpl implements ProductSearchService {

private final ProductSearchMapper productSearchMapper;
private static final int DEFAULT_SEARCH_CATEGORY_ID = 1;
private static final int DEFAULT_PRODUCT_CACHE_COUNT = 2000;
private static final String DEFAULT_SEARCH_CATEGORY_NAME = CategoryDTO.SortStatus.CATEGORIES.toString();
public static final String DEFAULT_PRODUCT_CACHE_KEY = "noLogin";

@Autowired
private RedisTemplate<String, Object> redisTemplate;

@Autowired
private ProductSearchMapper productSearchMapper;
private ObjectMapper objectMapper;

@Value("${expire.products}")
private long productsExpireSecond;

public ProductSearchServiceImpl(ProductSearchMapper productSearchMapper)
{
this.productSearchMapper = productSearchMapper;

}

// 상위 2000개 게시글을 redisTemplate에 push
@PostConstruct
public void init() {
System.out.println("ProductSearchServiceImpl @PostConstruct init redisTemplate에 push");

CategoryDTO categoryDTO = CategoryDTO.builder()
.id(DEFAULT_SEARCH_CATEGORY_ID)
.name(DEFAULT_SEARCH_CATEGORY_NAME)
.sortStatus(CategoryDTO.SortStatus.NEWEST)
.searchCount(DEFAULT_PRODUCT_CACHE_COUNT)
.pagingStartOffset(CategoryDTO.PAGING_OFFSET)
.build();

List<ProductDTO> productDTOList = productSearchMapper.selectProducts(ProductDTO.Status.NEW.toString(),categoryDTO);

for (ProductDTO productDTO : productDTOList) {

final String key = RedisKeyFactory.generateProductKey(DEFAULT_PRODUCT_CACHE_KEY);

redisTemplate.watch(key); // 해당 키를 감시한다. 변경되면 로직 취소.

try {
if (redisTemplate.opsForList().size(key) >= 2000) {
throw new IndexOutOfBoundsException("최상단 중고물품 2O00개 이상 담을 수 없습니다.");
}

redisTemplate.multi();
redisTemplate.opsForList().rightPush(key, productDTO);
redisTemplate.expire(key, productsExpireSecond, TimeUnit.SECONDS);

redisTemplate.exec();
} catch (Exception e) {
redisTemplate.discard(); // 트랜잭션 종료시 unwatch()가 호출된다
throw e;
}
}
}

public void addRedisKeys(ProductDTO productDTO, String userId) {
final String key = RedisKeyFactory.generateProductKey(userId);

redisTemplate.watch(key); // 해당 키를 감시한다. 변경되면 로직 취소.

try {
if (redisTemplate.opsForList().size(key) >= 2000) {
throw new IndexOutOfBoundsException("최상단 중고물품 2O00개 이상 담을 수 없습니다.");
}

redisTemplate.multi();
redisTemplate.opsForList().rightPush(key, productDTO);
redisTemplate.expire(key, productsExpireSecond, TimeUnit.SECONDS);
redisTemplate.exec();

} catch (Exception e) {
redisTemplate.discard(); // 트랜잭션 종료시 unwatch()가 호출된다
throw e;
}
}

/**
* 최상단 중고물품들을 조회한다.
* @author topojs8
* @param useId 고객 아이디
* @return
*/
public List<ProductDTO> findAllProductsByCacheId(String useId) {
List<ProductDTO> items = redisTemplate.opsForList()
.range(RedisKeyFactory.generateProductKey(useId), 0, -1)
.stream()
.map(item -> objectMapper.convertValue(item, ProductDTO.class))
.collect(Collectors.toList());
return items;
}

@Cacheable(value="getProducts")
@Override
public List<ProductDTO> getProducts(ProductDTO productDTO, CategoryDTO categoryDTO) {
productDTO.setCategoryId(categoryDTO.getId());
List<ProductDTO> productDTOList = productSearchMapper.selectProducts(productDTO,categoryDTO);
List<ProductDTO> productDTOList = productSearchMapper.selectProducts(productDTO.getStatus().toString(),categoryDTO);
return productDTOList;
}
}
18 changes: 18 additions & 0 deletions src/main/java/com/market/server/utils/RedisKeyFactory.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.market.server.utils;

public class RedisKeyFactory {

public enum Key {
PRODUCT,
}

// 인스턴스화 방지
private RedisKeyFactory() {}

private static String generateKey(String id, Key key) { return id + ":" + key; }

public static String generateProductKey(String userId) {
return generateKey(userId, Key.PRODUCT);
}

}
5 changes: 3 additions & 2 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# mysql
spring.datasource.url=jdbc:mysql://localhost:3306/market?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Seoul
spring.datasource.url=jdbc:mysql://localhost:3307/market?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Seoul
spring.datasource.username=root
spring.datasource.password=wnstjr1026

Expand All @@ -16,6 +16,7 @@ market.server.redis.password=

# expire
expire.defaultTime=36288000
expire.products=3600

# tomcat
server.port=8888
server.port=9000
4 changes: 2 additions & 2 deletions src/main/resources/mybatis/mapper/productSearchMapper.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@
`categoryId`,
`fileId`
FROM `product`
<if test="productDTO.status != null">
WHERE status = #{productDTO.status}
<if test="productStatus != null">
WHERE status = #{productStatus}
</if>
</if>
<choose>
Expand Down

0 comments on commit 54aa42e

Please sign in to comment.