본문 바로가기
Project/StyleLab

StyleLab의 일곱 번째 노트: 상품 목록 조회 최적화를 위한 파티셔닝 - 2

by 규난 2024. 1. 30.
728x90

이전 포스팅에 이어서 애플리케이션에서 각 파티셔닝 테이블의 데이터를 조회하는 코드는 어떻게 구현하였는지 공유해 보도록 하겠습니다.

 

목차

  1. 상품 목록 조회 API의 Controller
  2. ProductCategoriesFacade의 findAllProductCategoryConditions() 메서드
  3. ProductCategotryType의 역할
  4. ProductCategoriesService의 findAllProductCategoryConditions() 메서드
  5. ProductCategoryQueryDslRepositoryStrategyMap과 ProductCategoryQueryDslRepository interface
  6. 몇 가지의 문제점
  7. 문제점 개선
  8. 느낀점

 

1. 상품 목록 조회 API Controller

밑의 Controller 코드를 보시면 상품 목록 조회 요청 시 pathVariable로 productCategoryPath를 받고 있습니다. 이때 받은 productCategoryPath에 매핑되는 partitioning category repository를 찾아 데이터를 조회할 수 있어야 합니다.

카테고리별 상품 목록 조회 API

 

2. ProductCategoriesFacade의 findAllProductCategoryConditions() 메서드

이어서 Controller가 의존하고 있는 Facade의 findAllProductCategoryConditions() 메서드를 보시면 파라미터로 전달받은 productCategoryPath를 ProductCategotryType의 static of() 메서드에 파라미터로 전달하고 호출하여 일치하는 ProductCategotryType을 찾게 됩니다.

ProductCategoriesFacade의 findAllProductCategoryConditions() 메서드

 

3. ProductCategotryType의 역할

여기서 등장하는 ProductCategotryType이 아주 중요한 역할을 맡고 있습니다.

바로 productCategoryPath에 매핑되는 partitioning category repository를 찾을 수 있는 Key 역할을 합니다.

 

Facade에서 호출한 ProductCategotryType의 static of() 메서드를 밑의 코드에서 자세히 보도록 하겠습니다.

ProductCategotryType에는 productCategoryPath라는 필드가 있습니다. of() 메서드는 파라미터인 productCategoryPath와 ProductCategotryType의 productCategoryPath 필드와 일치하는 ProductCategoryType을 찾아주는 역할을 합니다.

ProductCategotryType의 내부 코드

 

4. ProductCategoriesService의 findAllProductCategoryConditions() 메서드

이렇게 찾아준 ProductCategoryType을 ProductCategoriesService의 findAllProductCategoryConditions() 메서드에 파라미터로 전달하고, 해당 메서드 내에서는 전달받은 ProductCategoryType을 productCategoryRepositoryMap의 getProductCategoryRepository() 메서드 파라미터로 전달하고 호출하여 productCategoryPath에 매핑되는 partitioning category repository를 찾게 됩니다. productCategoryRepositoryMap은 어떻게 구성되어 있기에 productCategoryPath에 매핑되는 partitioning category repository를 찾을 수 있는지, 그 이유를 밑에서 자세하게 설명하겠습니다.

ProductCategoriesService의 findAllProductCategoryConditions() 메서드

 

5. ProductCategoryQueryDslRepositoryStrategyMap과 ProductCategoryQueryDslRepository interface

productCategoryRepositoryMap은 ProductCategoryQueryDslRepositoryStrategyMap 타입이며, EnumMap<ProductCategoryType, ProductCategoryQueryDslRepository>을 인스턴스 변수로 가집니다. 또한, ProductCategoryType을 파라미터로 받아 ProductCategoryQueryDslRepository를 반환하는 getProductCategoryRepository() 메서드를 포함합니다.

ProductCategoryQueryDslRepositoryStrategyMap

 

Value인 ProductCategoryQueryDslRepository는 interface로 productCategoryPath에 매핑되는 partitioning category repository에서 구현하게 됩니다.

ProductCategoryQueryDslRepository inteface와 구현체

 

ProductCategoryQueryDslConfiguration 클래스에서 ProductCategoryType과 repository 구현체를 이용해 ProductCategoryQueryDslRepositoryStrategyMap은 Bean으로 등록해 주었습니다.

구현체는 파티셔닝된 테이블 개수만큼 존재합니다. 상당히 많네요… 카테고리가 추가되면 더 많아지겠죠…

ProductCategoryQueryDslConfiguration

 

이렇게 Bean으로 등록된 ProductCategoryQueryDslRepositoryStrategyMap을 ProductCategoriesService에 주입해 줌으로써 findAllProductCategoryConditions() 메서드에서 productCategoryPath에 맵핑되는 partitioning category repository를 찾을 수 있게 되는 겁니다.

 

 

6. 몇 가지 문제점

파티셔닝을 도입하여 성능 개선을 하였지만 코드를 구현하면서 느낀 몇 가지 문제점는 다음과 같습니다.

(취소선은 QueryDsl JPA -> JdbcClient로 변경하여 해결)

  1. 카테고리가 추가될 때마다 ProductCategoryType을 수정해 주어야 합니다.
  2. 카테고리가 추가될 때마다 ProductCategoryQueryDslRepository의 구현체를 만들고 ProductCategoryQueryDslConfiguration을 수정해 주어야 합니다.
  3. 상위 클래스인 ProductCategoryQueryDslRepository의 메서드가 변경되거나 추가되면 2번에서 언급한 많은 구현체들의 메서드도 변경하거나 추가해 줘야 합니다. 

 

7. 문제점 개선

 

JdbcClient를 사용하여 위에서 언급한 문제점 2, 3번을 개선하였습니다.

JdbcClient는 스프링 6.1에 등장하였고, 더 편리하게 JDBC 사용할 수 있게 도와주는 JDBC access facade입니다.

아래 사진에서 수정된 ProductCategoriesService의 findAllProductCategoryConditions() 메서드를 보시면 ProductCategoryQueryDslRepository를 찾는 부분이 제거됐고, ProductCategoryQueryDslRepository 대신 ProductCategoryJdbcRepository를 호출하는 것을 보실 수 있습니다.

ProductCategoriesService의 findAllProductCategoryConditions() 메서드

 

다음으로 ProductCategoryJdbcRepository 내부를 살펴보겠습니다.

파라미터로 전달 받은 ProductCategoryCondition의 ProductCategoryType을 사용하여 조회할 파티셔닝 테이블을 찾아 select from 절을 만들어 주는 방식을 사용하였습니다.

ProductCategoryJdbcRepository의 findAllProductCategoryConditions() 메서드

 

이 방식을 통해 문제점에서 언급한 파티셔닝된 테이블마다 entity와 repository를 만들어야 하는 문제점과 그에 따른 변경포인트가 많아진 다는 문제점을 개선하였습니다.

 

위 과정을 모두 거치게 되면 최종적으로 client가 받게 되는 응답 형태는 다음과 같습니다.

{
    "code": "20000",
    "message": "Success",
    "lastPage": false,
    "page": 0,
    "size": 40,
    "nextToken": 5568867,
    "items": [
        {
            "productId": 5568946,
            "storeId": 87927,
            "storeName": "프로스펙스 원스타 캔버스 신발",
            "productCategoryPath": "001004005001",
            "productCategoryName": "캔버스",
            "productMainImage": "https://image1",
            "productMainImageType": "PRODUCT_ENTRY_MAIN",
            "name": "프로스펙스 원스타 캔버스 신발",
            "price": 140054,
            "discountPrice": 119000,
            "discountRate": 15,
            "soldOut": false,
            "deleted": false,
            "createdAt": "2024-01-24 13:33:39"
        },
        {
            "productId": 5568944,
            "storeId": 87927,
            "storeName": "나이키 척테일러 캔버스 신발",
            "productCategoryPath": "001004005001",
            "productCategoryName": "캔버스",
            "productMainImage": "https://image1",
            "productMainImageType": "PRODUCT_ENTRY_MAIN",
            "name": "나이키 척테일러 캔버스 신발",
            "price": 47010,
            "discountPrice": 36000,
            "discountRate": 23,
            "soldOut": false,
            "deleted": false,
            "createdAt": "2024-01-24 13:33:39"
        },
				.
				.
				.
		]
}

 

8. 느낀점

이번 상품 목록 조회 성능 개선을 위해 파티셔닝을 도입하기까지 여러 어려움을 겪었습니다.

하나의 상품 테이블에 여러 카테고리의 상품 데이터를 대량으로 넣으면서 조회 성능이 저하되는 문제를 발견하였고, 며칠 동안 상품 목록 조회 쿼리 최적화와 테이블에 인덱스를 추가하여 조회 성능을 개선하려 했지만 실패하였습니다.

 

실패 후 며칠을 또 고민한 끝에 샤딩과 파티셔닝을 통해 해결할 수 있다는 것을 깨달았고, 현재 프로젝트에는 파티셔닝을 통해 해결하는 것이 가장 적합하다고 판단하여 파티셔닝을 도입하였습니다.

 

파티셔닝을 통해 성능 개선을 한 부분이 정답인지는 확신할 수 없지만, 성능 개선과 더불어 새로운 경험을 하게 되어서 매우 유익한 시간이었다고 생각합니다.

728x90