인덱스 적용
브랜드 Id만 인덱스를 거는 경우는 어떨까? 생각보다 속도는 비슷하게 나오는듯하다.
요렇게 나왔는데 이건 안 거는거랑 비슷한거 같다라는 생각이 들었다.
그래서 좋아요를 기준으로 시도해보자.
좋아요를 하니 생각보다 빨라졌긴했지만 그렇다고해서 의미있는건지는 잘 모르겠다.
복합 인덱스로 설정하고 다시 설정해보자.
놀랍게도 product_status에 대한 rows값이 1로 줄었다는것을 알 수 있었다.
그러면 더 나아가서 stock도 최적화를 한다 생각하면 stock도 productId를 인덱스로 걸면 어떨까?
rows가 반토막 난거말고는 딱히 특징이 없었다.
하지만
생각만큼 쿼리 속도는 빠르지 않았다.
이유가 뭘까? (신기하군.. _. ;;)
음.. 근데 신기하게도
api로 쐈을때는 속도는 빨라졌다.
계속 날려보니 0.5~1.5초 사이로 성공이 된다는것을 확인 할 수 있다.
이제 k6를 사용해서 결과를 도출해보자.
█ TOTAL RESULTS
checks_total.......................: 20 0.634917/s
checks_succeeded...................: 100.00% 20 out of 20
checks_failed......................: 0.00% 0 out of 20
✓ status is 200
HTTP
http_req_duration.......................................................: avg=572.63ms min=547.13ms med=566.19ms max=699.2ms p(90)=576.37ms p(95)=590.58ms
{ expected_response:true }............................................: avg=572.63ms min=547.13ms med=566.19ms max=699.2ms p(90)=576.37ms p(95)=590.58ms
http_req_failed.........................................................: 0.00% 0 out of 20
http_reqs...............................................................: 20 0.634917/s
EXECUTION
iteration_duration......................................................: avg=1.57s min=1.55s med=1.56s max=1.7s p(90)=1.58s p(95)=1.59s
iterations..............................................................: 20 0.634917/s
vus.....................................................................: 1 min=1 max=1
vus_max.................................................................: 1 min=1 max=1
NETWORK
data_received...........................................................: 17 kB 550 B/s
data_sent...............................................................: 1.9 kB 61 B/s
확실히 이전 시간 보다
전반적으로 HTTP 요청 시간은 약 70~74%, 반복 소요 시간은 약 49~51% 단축되어 상당한 성능 향상이 있었음을 알 수 있습니다.
이정도가 성능 향상이 되었다는 것을 알 수 가 있었다.
그러면 인덱스로 설정하게 되면 무조건 성능이 좋아질까?
위에서 확인했듯이 잘 못사용하는 경우 성능 향상에는 아무런 영향이 없었다. 심한 경우에는 오히려 성능이 떨어질 수 도 있지 않을까?
인덱스를 설정할때 대분류 > 중분류 > 소분류를 통해 인덱스를 만들게 된다.
하지만 이렇게 만들게 되면, 대분류의 데이터가 적어 인덱스가 걸리지 않을 수 있다.
그럼에도 불구하고 이렇게 설정해야 하는 이유는 뭘까?
대분류부터 인덱스를 먼저 설정해야 하는 이유는
소분류에서 데이터가 많아지는 것보다
대분류에서 데이터가 많아지는 것이 더 치명적이라 생각한다.
위에서 4건인 경우에 상품의 예상 row수는 1로 나왔다. 이는 상품에는 성능 저하가 심하지 않다는 뜻으로 해석이 되어진다.
한번 성능을 측정해보자.
9.86초가 걸렸으며
█ TOTAL RESULTS
checks_total.......................: 4 0.111219/s
checks_succeeded...................: 100.00% 4 out of 4
checks_failed......................: 0.00% 0 out of 4
✓ status is 200
HTTP
http_req_duration.......................................................: avg=7.98s min=6.22s med=7.6s max=10.51s p(90)=9.95s p(95)=10.23s
{ expected_response:true }............................................: avg=7.98s min=6.22s med=7.6s max=10.51s p(90)=9.95s p(95)=10.23s
http_req_failed.........................................................: 0.00% 0 out of 4
http_reqs...............................................................: 4 0.111219/s
EXECUTION
iteration_duration......................................................: avg=8.99s min=7.23s med=8.6s max=11.51s p(90)=10.95s p(95)=11.23s
iterations..............................................................: 4 0.111219/s
vus.....................................................................: 1 min=1 max=1
vus_max.................................................................: 1 min=1 max=1
NETWORK
data_received...........................................................: 3.4 kB 96 B/s
data_sent...............................................................: 384 B 11 B/s
라는 결과가 나왔다.
이제 인덱스를 걸어보자.
라는 결과가 나왔으며
█ TOTAL RESULTS
checks_total.......................: 13 0.432496/s
checks_succeeded...................: 100.00% 13 out of 13
checks_failed......................: 0.00% 0 out of 13
✓ status is 200
HTTP
http_req_duration.......................................................: avg=1.3s min=1.22s med=1.28s max=1.6s p(90)=1.41s p(95)=1.5s
{ expected_response:true }............................................: avg=1.3s min=1.22s med=1.28s max=1.6s p(90)=1.41s p(95)=1.5s
http_req_failed.........................................................: 0.00% 0 out of 13
http_reqs...............................................................: 13 0.432496/s
EXECUTION
iteration_duration......................................................: avg=2.31s min=2.23s med=2.28s max=2.6s p(90)=2.41s p(95)=2.5s
iterations..............................................................: 13 0.432496/s
vus.....................................................................: 1 min=1 max=1
vus_max.................................................................: 1 min=1 max=1
NETWORK
data_received...........................................................: 11 kB 373 B/s
data_sent...............................................................: 1.2 kB 42 B/s
확인 결과
전반적으로 HTTP 요청 시간은 80% 이상, 반복 소요 시간은 70% 이상 감소하여 매우 큰 성능 개선이 이뤄졌다.
참고로 브랜드ID만 지정했을때 5.7초 정도 걸렸으며
좋아요만 지정했을때는 7.8초 정도 걸렸다.
캐시 적용
나는 totalCount를 캐싱을 해서 보여주는 것으로 결정하였다.
그 이유는 현재
List<ProductProjection> content = query.select(new QProductProjection
(product.id,
product.brandId,
brand.name.name,
product.name.name,
product.price.price,
product.description,
status.likeCount,
product.createdAt,
product.updatedAt))
.from(product)
.leftJoin(brand).on(product.brandId.eq(brand.id))
.leftJoin(status).on(status.productId.eq(product.id))
.where(where)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.orderBy(
switch (sort) {
case LATEST -> product.createdAt.desc();
case PRICE_ASC -> product.price.price.asc();
case LIKES_DESC -> status.likeCount.desc();
})
.orderBy(product.id.asc())
.fetch();
// 갯수
Long totalCount = query.select(product.count())
.from(product)
.leftJoin(brand).on(product.brandId.eq(brand.id))
.where(where)
.fetchOne();
요렇게 쿼리문을 사용하고 있다. 그렇다면,
Long totalCount = query.select(product.count())
.from(product)
.leftJoin(brand).on(product.brandId.eq(brand.id))
.where(where)
.fetchOne();
이거에 대한 데이터를 미리 캐싱해둔다면, count를 쿼리문을 통해 계산하지 않기 때문에 더 빠른 성능을 기대해볼 수 있지 않을까?
하지만 여기에는 치명적인 문제가 존재한다.
그것은 totalCount같은 경우, 상품이 추가되거나 제거될때마다 totalCount에 대한 처리도 해줘야 할지도 모른다.
그럼에도 불구하고 이렇게 선택한 이유는 순전히 읽기 성능을 올리고 싶었기때문에 이런 결정을 하게 되었다.
전략 선택하기
위에서 확인했듯이 다양한 전략들이 존재했다.
나는 cache aside방법으로 진행해볼 예정이다.
그 이유는 안정적이면서, 빠르게 적용해볼 수 있는 전략이라 생각했기 때문이다.
코드를 만들고 최초로 등록하였다.
2s가 걸렸다.
분명 캐시를 사용하는걸로 코드를 짰는데 캐시를 이용하지 않고 DB를 이용하게 되어진다.
이런 현상을 없애기 위해서는
warm up
warm up이라는 과정을 거쳐야 한다.
직역하자면, 따뜻하게 해주는 작업이라는건데 쉽게 말해서 데이터를 미리미리 캐시에 넣어주는 행위를 뜻한다.
그렇게 되면 캐시를 사용하게 되니 속도가 빨라지지 않을까?
캐시 미스상황이니 캐시쪽으로 데이터를 넣게 되어진다.
데이터가 잘 들어가졌다.
이제 실행해보니..
빨라지긴했는데 생각만큼 빨라지지 않았다. 0.5s가량 빨라진거 같은데 totalCount가 그렇게까지 영향력이 낮은걸까?
확인해보니
List<ProductProjection> content = query.select(new QProductProjection
(product.id,
product.brandId,
brand.name.name,
product.name.name,
product.price.price,
product.description,
status.likeCount,
product.createdAt,
product.updatedAt))
.from(product)
.leftJoin(brand).on(product.brandId.eq(brand.id))
.leftJoin(status).on(status.productId.eq(product.id))
.where(where)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.orderBy(
switch (sort) {
case LATEST -> product.createdAt.desc();
case PRICE_ASC -> product.price.price.asc();
case LIKES_DESC -> status.likeCount.desc();
})
.fetch();
확인해보니 여기서 쿼리가 느린것을 확인하였다.
그전에 k6로 tps를 확인해보자.
█ TOTAL RESULTS
checks_total.......................: 11 0.354698/s
checks_succeeded...................: 100.00% 11 out of 11
checks_failed......................: 0.00% 0 out of 11
✓ status is 200
HTTP
http_req_duration.......................................................: avg=1.81s min=1.71s med=1.8s max=1.92s p(90)=1.88s p(95)=1.9s
{ expected_response:true }............................................: avg=1.81s min=1.71s med=1.8s max=1.92s p(90)=1.88s p(95)=1.9s
http_req_failed.........................................................: 0.00% 0 out of 11
http_reqs...............................................................: 11 0.354698/s
EXECUTION
iteration_duration......................................................: avg=2.81s min=2.71s med=2.8s max=2.92s p(90)=2.89s p(95)=2.9s
iterations..............................................................: 11 0.354698/s
vus.....................................................................: 1 min=1 max=1
vus_max.................................................................: 1 min=1 max=1
NETWORK
data_received...........................................................: 9.2 kB 298 B/s
data_sent...............................................................: 1.5 kB 49 B/s
그러면 몇 가지를 확인 생각할 수 있다.
1. 인덱스 적용
2. 일정 분량을 캐시로 만들고 그것을 사용하는 방법
일단 적용이 먼저니까. 인덱스를 지정해보자.
(빠르게 확인하기 위해서는 인덱스로 지정해보는것이 더 낫다고 판단했다.)
인덱스를 거니 속도는 다음과 같았다.
생각보다 감소가 되지 않았다.
█ TOTAL RESULTS
checks_total.......................: 10 0.300404/s
checks_succeeded...................: 100.00% 10 out of 10
checks_failed......................: 0.00% 0 out of 10
✓ status is 200
HTTP
http_req_duration.......................................................: avg=2.32s min=2.14s med=2.22s max=2.99s p(90)=2.57s p(95)=2.78s
{ expected_response:true }............................................: avg=2.32s min=2.14s med=2.22s max=2.99s p(90)=2.57s p(95)=2.78s
http_req_failed.........................................................: 0.00% 0 out of 10
http_reqs...............................................................: 10 0.300404/s
EXECUTION
iteration_duration......................................................: avg=3.32s min=3.14s med=3.22s max=3.99s p(90)=3.58s p(95)=3.78s
iterations..............................................................: 10 0.300404/s
vus.....................................................................: 1 min=1 max=1
vus_max.................................................................: 1 min=1 max=1
NETWORK
data_received...........................................................: 8.4 kB 252 B/s
data_sent...............................................................: 1.4 kB 41 B/s
인덱싱을 걸어도 크게 바뀌지 않는것았기 때문에
다른 걸 캐싱하는것이 좋을거 같다.
사실 totalCount같은 경우, 자주 변경되지 않고 조회가 많이 발생하는 것이 어떤 부분인지 생각할 필요가 있을거 같다.
인기 상품을 캐싱을 해봤다.
좋아요 순으로 설정해서 탑 10을 캐싱하기로 결정하였다.
결과는 다소 충격적이다.
27ms가 나왔다.
TOTAL RESULTS
checks_total.......................: 30 0.971533/s
checks_succeeded...................: 100.00% 30 out of 30
checks_failed......................: 0.00% 0 out of 30
✓ status is 200
HTTP
http_req_duration.......................................................: avg=26.3ms min=8.77ms med=16.32ms max=308.3ms p(90)=21.07ms p(95)=23.14ms
{ expected_response:true }............................................: avg=26.3ms min=8.77ms med=16.32ms max=308.3ms p(90)=21.07ms p(95)=23.14ms
http_req_failed.........................................................: 0.00% 0 out of 30
http_reqs...............................................................: 30 0.971533/s
EXECUTION
iteration_duration......................................................: avg=1.02s min=1s med=1.02s max=1.31s p(90)=1.02s p(95)=1.02s
iterations..............................................................: 30 0.971533/s
vus.....................................................................: 1 min=1 max=1
vus_max.................................................................: 1 min=1 max=1
NETWORK
data_received...........................................................: 6.9 kB 223 B/s
data_sent...............................................................: 2.9 kB 93 B/s
구분평균 응답 시간 (avg)p(90) 응답 시간p(95) 응답 시간초당 요청 수 (TPS)
1. 아무것도 안 했을 때 (Baseline) | 2180 ms | 2240 ms | 2250 ms | 0.31 |
2. 인덱스 적용 | 572.63 ms | 576.37 ms | 590.58 ms | 0.63 |
3. 100만 건 (인덱스 X) | 7980 ms | 9950 ms | 10230 ms | 0.11 |
4. 100만 건 (인덱스 O) | 1300 ms | 1410 ms | 1500 ms | 0.43 |
5. 반 정규화 적용 | 813.35 ms | 893.17 ms | 897.51 ms | 0.55 |
6. 반 정규화 + 인덱스 | 580.17 ms | 582.15 ms | 596.40 ms | 0.63 |
7. 캐시 적용 (totalCount) | 1810 ms | 1880 ms | 1900 ms | 0.35 |
8. 캐시 + 인덱스 (totalCount) | 1750 ms | 1760 ms | 1850 ms | 0.36 |
9. 좋아요 탑10 캐싱 (성공)🚀 | 26.3 ms | 21.07 ms | 23.14 ms | 0.97 |
* 표는 다시 찍을 예정
'루퍼스 > 5주차' 카테고리의 다른 글
어떤 방법으로 성능 개선하는것이 좋을까? (4) | 2025.08.15 |
---|