스프링 쇼핑몰 프로젝트 2주차

윤장원·2023년 5월 24일
0

쇼핑몰프로젝트

목록 보기
2/5

상품 조회하기

등록된 상품들을 최신순, 낮은 가격순, 주문 횟수 많은 순으로 조회하는 기능을 구현했다.

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 설정

먼저 ElasticSearch를 설치했다. 설치는 다음 주소에서 했고, 버전은 7.17.9 버전을 이용했다.
https://www.elastic.co/kr/downloads/past-releases#elasticsearch
그 다음, 해당 폴더에서 터미널을 실행한 뒤, ./bin/elasticsearch 명령어를 실행하여 ElasticSearch를 실행했다. 정상적으로 실행이 된건지 확인하기 위해 http://localhost:9200/ 에 접속하면 내 ElasticSearch에 대한 정보가 JSON 형식으로 담긴 결과를 볼 수 있다.

Product 인덱스 등록

현재 프로젝트 데이터베이스에서 다루는 상품들 정보를 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 요청을 보내면 된다.

프로젝트 ElasticSearch 적용

먼저 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": "식품"
    }
]

0개의 댓글