핫딜 페이징 조회 - ekdan38/HotDealService GitHub Wiki
- CPU : Intel i5-8250U 1.6GHz
- RAM : 8GB
- OS : Window10
- Databse : MySQL 8.0
- Test Tool : K6
테스트를 위해 1만개의 핫딜, 10만개의 상품에 대한 더미 데이터를 생성하였다.
테스트 시나리오
1. 60% 확률로 검색어 검색
2. 모든 사용자는 반드시 첫 페이지 로드
3. 첫 페이지 조회 후, 70%의 확률로 다음 페이지 조회
4. 최대 3페이지까지만 탐색
5. 다음 페이지 조회 시 페이지 조회 후 1~4초의 랜덤한 대기 시간 부여
개선 전 k6 테스트
k6 테스트 결과
-
VU 100
-
VU 200
-
VU 300
핫딜 페이징 조회 (3분) | 총 처리량 | Latency(mean) | Latency(P95) | TPS |
---|---|---|---|---|
VU 100 | 39834 | 450ms | 785ms | 220 |
VU 200 | 38074 | 947ms | 1.73s | 209 |
VU 300 | 34692 | 1.56s | 3.69s | 190 |
테스트 결과 분석
- VU 100 -> 200 증가 시, Latency(mean), Latency(P95) 모두 2배 이상 증가
- TPS는 VU 200에서 소폭 감소 후 VU 300에서 더 감소되는 모습을 보임
성능 개선 계획
- DTO Proejction : 여러 Row를 조회 하기에 DTO Projection을 적용하여 필요한 필드만 선택적으로 조회
- 인덱싱 : 조회 성능 향상을 위해 적절한 인덱스를 생성하여 쿼리의 탐색 비용 감소
- 캐싱 : 반복적으로 조회되는 데이터를 캐시에 저장하여 DB 부하를 줄여 성능 개선
여러 Row를 조회 하기에 DTO Projection을 적용하여 필요한 필드만 선택적으로 조회하여 성능 향상 기대
수정 전
@Query("SELECT h " +
"FROM HotDeal h " +
"WHERE h.deleted = false " +
"AND h.id < :cursor " +
"AND (:search IS NULL OR h.title LIKE %:search%) " +
"ORDER BY h.id DESC")
List<HotDeal> findByCursorAndSearchAndSizeHotDeals(@Param("cursor") Long cursor,
@Param("search") String search,
Pageable pageable);
수정 후
@Query("SELECT new com.hong.hotdealservice.dto.HotDealSimpleDto(h.id, h.title, h.description, h.startTime, h.endTime) " +
"FROM HotDeal h " +
"WHERE h.deleted = false " +
"AND h.id < :cursor " +
"AND (:search IS NULL OR h.title LIKE %:search%) " +
"ORDER BY h.id DESC")
List<HotDealSimpleDto> findByCursorAndSearchAndSizeHotDeals(@Param("cursor") Long cursor,
@Param("search") String search,
Pageable pageable);
수정 전 Entity의 모든 필드를 조회했지만, 수정 후에는 필요한 필드만 조회 (13개의 필드 -> 5개의 필드)
DTO Projection 적용 후 k6 테스트
k6 테스트 결과
-
VU 100
-
VU 200
-
VU 300
핫딜 페이징 조회 (3분) | 총 처리량 | Latency(mean) | Latency(P95) | TPS |
---|---|---|---|---|
VU 100 | 46523 | 385ms | 659ms | 257 |
VU 200 | 45566 | 791ms | 1.42s | 251 |
VU 300 | 41896 | 1.29s | 2.96s | 229 |
테스트 결과분석
Latency
-
Latency(mean)
- VU 100: 450ms → 385ms (약 14.4% 개선)
- VU 200: 947ms → 791ms (약 16.5% 개선)
- VU 300: 1.56s → 1.29s (약 17.3% 개선)
Latency(mean) : 약 14 ~ 17% 개선
-
Latency(P95)
- VU 100: 785ms → 659ms (약 16.1% 개선)
- VU 200: 1.73s → 1.42s (약 17.9% 개선)
- VU 300: 3.69s → 2.96s (약 19.8% 개선)
Latency(P95) : 약 16 ~ 20% 개선
TPS
- VU 100: 220 → 257 (약 16.8% 개선)
- VU 200: 209 → 251 (약 20.1% 개선)
- VU 300: 190 → 229 (약 20.5% 개선)
TPS : 약 17 ~ 20% 개선
기존 쿼리는 Entity의 모든 필드인 13개를 조회 했지만, DTO Projection을 통해 5개의 필드를 조회하기 때문에 위와 같은 성능 개선 효과를 보임
실제 실행되는 쿼리와 실행 계획을 통해 인덱스를 생성
2.1. 실행 계획 및 쿼리 실행 소요 시간
2.1.1. Title 미포함 조회
[실행 계획]
EXPLAIN SELECT hd1_0.hotdeal_id,hd1_0.title,hd1_0.description,hd1_0.start_time,hd1_0.end_time
FROM hot_deal hd1_0
WHERE hd1_0.deleted = 0
AND hd1_0.hotdeal_id<9912
AND (NULL IS NULL OR hd1_0.title LIKE NULL ESCAPE '')
ORDER BY hd1_0.hotdeal_id
DESC LIMIT 10;
[소요 시간]
소요 시간 : 약 36ms
2.1.2. Title 포함 조회
[실행 계획]
EXPLAIN SELECT hd1_0.hotdeal_id,hd1_0.title,hd1_0.description,hd1_0.start_time,hd1_0.end_time
FROM hot_deal hd1_0
WHERE hd1_0.deleted=0
AND hd1_0.hotdeal_id<9912
AND ('삼성' IS NULL OR hd1_0.title LIKE '%삼성%' ESCAPE '')
ORDER BY hd1_0.hotdeal_id
DESC LIMIT 10;
Title 포함 경우에도 미포함 조회와 같은 실행 계획을 보이고 있음
[소요 시간]
소요 시간 : 약 39ms
분석
- PK 클러스터링 인덱스를 사용, Using where, Backward index scan 이기에 인덱스를 통한 성능 개선 가능성 존재
2.2. 인덱스 생성
- deleted, id, title 에 대해 복합 인덱스가 존재한다면 성능 향상이 있을것으로 기대
- 다만, title은 양방향 와일드카드 조건이기에 title은 인덱스를 타지 않음
- 따라서 deleted, id 를 갖는 복합 인덱스를 통해 성능 향상 기대
2.2.1. 복합 인덱스 생성 시도
[인덱스 생성]
deleted, id 를 갖는 복합 인덱스 생성
CREATE INDEX idx_hotdeal_cursor_search
ON hot_deal (deleted, hotdeal_id DESC);
[실행 계획]
분석
- 옵티마이저는 PK 클러스터링 인덱스 선택
- deleted 의 카디널리티가 낮음(카디널리티 = 2)
- PK 클러스터링 인덱스 선택시 모든 필드 값이 리프 노드에 존재
- 따라서 옵티마이저는 복합 인덱스 대신 PK 클러스터링 인덱스 선택한것으로 추정
2.2.2. 단일 인덱스 생성
[인덱스 생성]
Backward index scan 이 발생 하기에 id 기준 DESC 정렬 인덱스 생성
CREATE INDEX idx_hotdeal_cursor_search
ON hot_deal (hotdeal_id DESC);
[실행 계획]
분석
- hotdeal_id 를 DESC 정렬하여 인덱스를 생성하면 해당 인덱스를 선택할것이라 기대
- 하지만, 옵티마이저는 리프 노드에 모든 컬럼 값이 존재하는 PK 인덱스 선택
따라서 인덱스는 기존 PK 클러스터링 인덱스를 그대로 사용
캐싱을 통해 DB에 직접 접근하는 대신, In-Memory 수준에서 빠르게 데이터 I/O 처리를 수행함으로써 응답 속도 향상 및 부하 감소 등 성능 개선 효과를 기대
로컬 캐시 vs Redis
- 로컬 캐시는 서비스 인스턴스 내부 메모리에 데이터를 저장하므로 응답 속도가 매우 빠름
- 본 프로젝트는 MSA 환경이기에 인스턴스별로 캐시가 분리되어 캐시 일관성 보장 어려움
-
Redis는 외부에 위치한 인메모리 데이터 저장소로, 여러 인스턴스 또는 서로 다른 서비스에서 공통으로 접근 가능
- 데이터 일관성, 확장성 측면에서 유리
위와 같은 이유로 Redis 선택
3.1. 캐싱 적용
@Cacheable(cacheNames = "getHotDeals"
, key = "'hotdeals:cursor:' + (#cursor == null ? '' : #cursor) + ':size:' + #size + ':search:' + (#search == null ? '' : #search)"
, cacheManager = "HotDealCacheManager")
@Cacheable 을 사용하여 Redis 캐싱 적용
- 핫딜은 자주 변경되는 데이터가 아니라고 판단, TTL 30분 적용
- 일관성을 위해 핫딜에 대한 수정 삭제 발생시 캐시 무효화 처리(@CacheEvict)
캐싱 적용 후 k6 테스트
k6 테스트 결과
-
VU 100
-
VU 200
-
VU 300
핫딜 페이징 조회 (3분) | 총 처리량 | Latency(mean) | Latency(P95) | TPS |
---|---|---|---|---|
VU 100 | 109569 | 157ms | 283ms | 608 |
VU 200 | 113627 | 308ms | 559ms | 629 |
VU 300 | 109205 | 489ms | 839ms | 604 |
테스트 결과 분석
Latency
-
Latency(mean)
- VU 100: 450ms → 157ms (약 65.1% 개선)
- VU 200: 947ms → 308ms (약 67.4% 개선)
- VU 300: 1.56s → 489ms (약 68.7% 개선)
Latency(mean) : 약 65 ~ 69% 개선
-
Latency(P95)
- VU 100: 785ms → 283ms (약 63.9% 개선)
- VU 200: 1.73s → 559ms (약 67.7% 개선)
- VU 300: 3.69s → 839ms (약 77.3% 개선)
Latency(mean) : 약 64 ~ 77% 개선
TPS
- VU 100: 220 → 608 (증가량 약 388 TPS, 약 176.4% 개선)
- VU 200: 209 → 629 (증가량 약 420 TPS, 약 201.0% 개선)
- VU 300: 190 → 604 (증가량 약 414 TPS, 약 217.9% 개선)
TPS: 약 176 ~ 218% 개선
캐싱을 통해 Latency가 모든 구간에서 약 65% 이상 대폭 감소되었으며, TPS는 약 175 ~ 218% 개선, 2.75배 향상
항목 | DTO Projection | 캐싱 적용 |
---|---|---|
평균 Latency 감소율 | 약 14~17% | 약 65~68% |
TPS 증가율 | 약 16~20% | 약 176~217% |
- Dto Projection 으로 미비한 성능 개선 효과를 보임
- 인덱스를 통한 성능 개선은 PK 클러스터링 인덱싱 사용이 가장 효율적, 따라서 별도 인덱스 생성하지 않음
- Redis 캐싱 적용을 통해 눈에 띄는 성능 향상을 보임