등록된 상품들을 최신순, 낮은 가격순, 주문 횟수 많은 순으로 조회하는 기능을 구현했다.
ProductInfo
private Long id;
private String name;
private int price;
private String description;
private Integer quantity;
private String image;
private String categoryName;
public static List<ProductInfo> toList(Page<Product> products) {
return products.stream()
.map(product -> ProductInfo.builder()
.id(product.getId())
.name(product.getName())
.price(product.getPrice())
.description(product.getDescription())
.quantity(product.getQuantity())
.image(product.getImage())
.categoryName(product.getCategory().getName())
.build())
.collect(Collectors.toList());
}
Product Controller
@GetMapping("/list")
public ResponseEntity<List<ProductInfo>> getLatestProducts(Pageable pageable) {
return ResponseEntity.ok(productService.getProducts(pageable));
}
Product Service
@Override
public List<ProductInfo> getProducts(Pageable pageable) {
Page<Product> products = productRepository.findAll(pageable);
return ProductInfo.toList(products);
}
Product Repository
public interface ProductRepository extends PagingAndSortingRepository<Product, Long> {
Page<Product> findAll(Pageable pageable);
}
최신순, 낮은 가격순, 주문 횟수 많은 순으로 조회하는 기능을 따로 구현하는 대신 Pageable로 요청을 받으면 그에 맞는 결과를 가져오는 방식을 사용했다.
예를 들어, 상품을 최신순으로 한 페이지에 10개씩 첫 번째 페이지를 조회하고 싶으면 다음으로 요청을 보내면 된다.
http://localhost:8080/product/search/?page=0&size=10&sort=createdDate,asc
ProductInfo 클래스에 Product로 이루어진 Page를 ProductInfo로 이루어진 List로 변환하는 메소드를 만들어주었다.
Redis를 이용한 캐싱 처리는 나중에 할 계획이다.
키워드를 받으면 상품 이름이나 상품 설명에 해당 키워드가 들어있는 상품들을 결과로 가져오는 기능을 구현했다. ElasticSearch를 사용했다.
먼저 ElasticSearch를 설치했다. 설치는 다음 주소에서 했고, 버전은 7.17.9 버전을 이용했다.
https://www.elastic.co/kr/downloads/past-releases#elasticsearch
그 다음, 해당 폴더에서 터미널을 실행한 뒤, ./bin/elasticsearch 명령어를 실행하여 ElasticSearch를 실행했다. 정상적으로 실행이 된건지 확인하기 위해 http://localhost:9200/ 에 접속하면 내 ElasticSearch에 대한 정보가 JSON 형식으로 담긴 결과를 볼 수 있다.
현재 프로젝트 데이터베이스에서 다루는 상품들 정보를 ElasticSearch에 동기화시키기 위해 ElasticSearch에 Product 인덱스를 등록했다. Postman으로 http://localhost:9200/products 에 Put 요청을 보내서 products 인덱스를 등록했다. Body는 다음과 같다.
{
"settings": {
"analysis": {
"analyzer": {
"ngram_analyzer": {
"type": "custom",
"tokenizer": "ngram_tokenizer",
"filter": ["lowercase"]
}
},
"tokenizer": {
"ngram_tokenizer": {
"type": "ngram",
"min_gram": 2,
"max_gram": 10,
"token_chars": ["letter", "digit"]
}
}
},
"number_of_shards": "1",
"number_of_replicas": "0",
"max_ngram_diff" : "8"
},
"mappings": {
"properties": {
"id": {"type": "long"},
"name": {
"type": "text",
"analyzer": "ngram_analyzer",
"search_analyzer": "standard"
},
"categoryName": {"type": "text"},
"price": {"type": "integer"},
"description": {
"type": "text",
"analyzer": "ngram_analyzer",
"search_analyzer": "standard"
},
"quantity": {"type": "integer"},
"image": {"type": "text"},
"orderedCnt": {"type": "integer"},
"deletedYn": {"type": "boolean"}
}
}
}
만약 해당 인덱스를 삭제하고 싶으면 http://localhost:9200/products 에 delete 요청을 보내면 된다.
먼저 build.gradle 파일의 dependencies에
implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch' 를 추가한다.
ElasticSearch에 저장하기 위한 ProductDocument를 설정했다.
ProductDocument
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Data
@Document(indexName = "products")
@Setting(settingPath = "elastic/product-setting.json")
public class ProductDocument {
@Id
private Long id;
@Field(type = FieldType.Text, analyzer = "ngram_analyzer")
private String name;
private String categoryName;
private int price;
@Field(type = FieldType.Text, analyzer = "ngram_analyzer")
private String description;
private Integer quantity;
private String image;
private Integer orderedCnt;
private boolean deletedYn;
public static ProductDocument from(Product product) {
return ProductDocument.builder()
.id(product.getId())
.categoryName(product.getCategory().getName())
.name(product.getName())
.price(product.getPrice())
.description(product.getDescription())
.quantity(product.getQuantity())
.image(product.getImage())
.orderedCnt(product.orderedCnt)
.deletedYn(product.isDeletedYn())
.build();
}
}
resources/elastic 폴더 안에 설정을 위한 product-setting.json파일을 생성했다.
product-setting.json
{
"settings": {
"analysis": {
"analyzer": {
"ngram_analyzer": {
"type": "custom",
"tokenizer": "ngram_tokenizer",
"filter": ["lowercase"]
}
},
"tokenizer": {
"ngram_tokenizer": {
"type": "ngram",
"min_gram": 2,
"max_gram": 10,
"token_chars": ["letter", "digit"]
}
}
}
},
"mappings": {
"properties": {
"id": {"type": "long"},
"name": {
"type": "text",
"analyzer": "ngram_analyzer",
"search_analyzer": "standard"
},
"categoryName": {"type": "text"},
"price": {"type": "integer"},
"description": {
"type": "text",
"analyzer": "ngram_analyzer",
"search_analyzer": "standard"
},
"quantity": {"type": "integer"},
"image": {"type": "text"},
"orderedCnt": {"type": "integer"},
"deletedYn": {"type": "boolean"}
}
}
}
ElasticSearch를 사용하는 Repository를 생성했다.
ElasticSearchProductRepository
public interface ElasticSearchProductRepository extends
ElasticsearchRepository<ProductDocument, Long> {
@Query("{\"bool\":{\"must\":{\"multi_match\":{\"query\":\"?0\",\"fields\":[\"name\", \"description\"]}}}}")
List<ProductDocument> findByNameOrDescription(String keyword, Pageable pageable);
}
그 후 Controller와 Service에 상품 검색하기 기능을 구현했다.
ProductController
@GetMapping("/search")
public ResponseEntity<List<ProductInfo>> searchProducts(
@RequestParam("keyword") String keyword,
@RequestParam("page") Integer page,
@RequestParam("size") Integer size
) {
return ResponseEntity.ok(productService.searchProducts(keyword, page, size));
}
ProductService
@Override
public List<ProductInfo> searchProducts(String keyword, Integer page, Integer size) {
Pageable pageable = PageRequest.of(page - 1, size);
List<ProductDocument> products = elasticSearchProductRepository.findByNameOrDescription(keyword,
pageable);
return ProductInfo.toListFromDocument(products);
}
상품 검색하기도 페이징 처리를 해서 원하는 크기 만큼 조회할 수 있도록 했다.
다음은 Postman으로 API 요청을 했을 때 결과이다.
ex) http://localhost:8080/product/search?keyword=크림&page=1&size=5
[
{
"id": 31,
"name": "죠스바",
"price": 1000,
"description": "상어 아이스크림",
"quantity": 100,
"image": "이미지",
"categoryName": "식품"
},
{
"id": 32,
"name": "스크류바",
"price": 1500,
"description": "딸기맛 아이스크림",
"quantity": 100,
"image": "이미지",
"categoryName": "식품"
},
{
"id": 33,
"name": "수박바",
"price": 1300,
"description": "수박맛 아이스크림",
"quantity": 100,
"image": "이미지",
"categoryName": "식품"
},
{
"id": 34,
"name": "더위사냥",
"price": 2000,
"description": "커피맛 아이스크림",
"quantity": 100,
"image": "이미지",
"categoryName": "식품"
},
{
"id": 35,
"name": "메로나",
"price": 700,
"description": "메론맛 아이스크림",
"quantity": 100,
"image": "이미지",
"categoryName": "식품"
}
]