์๋ฐ๋ฏน ์๋์ ๊ฑด๊ฐ ํธ๋ ๋์ ๋ฐ ๋ง์ถฐ ํฌ์ ํ๋ ์ (Healthy Pleasure)๋ฌธํ๊ฐ ๋ค๊ฐ์ด์ ๋ฐ๋ผ ๋จ์ํ ์ํ์ ํ๋งคํ๋ ๊ฒ์ด ์๋, ๊ฑด๊ฐํ ์ถ์ ๋ฐฉ์์ ์ ์ํ๊ณ ๊ณ ๊ฐ์ด ์์ ์ ๊ฑด๊ฐ์ ์ค์ค๋ก '๊ด๋ฆฌ'ํ ์ ์๋ ํ๊ฒฝ์ ์กฐ์ฑํ๋ ๊ฒ์ ๋๋ค.
ํฌ์ ํ๋ ์ (Healthy Pleasure)๋ Healthy(๊ฑด๊ฐํ)์ Pleasure(๊ธฐ์จ)๊ฐ ๊ฒฐํฉํ ๋จ์ด๋ก, ๊ฑด๊ฐ ๊ด๋ฆฌ์ ์ฆ๊ฑฐ์์ ์๋ฏธํฉ๋๋ค.
๐ก E-commerce ํ๋ซํผ์ ์ฃผ์ ํธ๋ํฝ ๋ฐ์ ์ด๋ฒคํธ์ธ ํ ์ธ ์ด๋ฒคํธ๋ฅผ ๊ฐ์ ํ์ฌ ์ฌ์ฉ์๋ค์ ์ฃผ๋ฌธ์ด ๊ธ๊ฒฉํ ์ฆ๊ฐํ์์ ๋ ์ฃผ๋ฌธ ์ค๋ฅ ์๋ ์์ ์ ์ธ ์๋น์ค๊ฐ ๊ฐ๋ฅํ ์๋ฒ๋ฅผ ๊ตฌ์ถํ๊ณ ์ ํ์์ต๋๋ค.
1๏ธโฃ ์กฐํ ์ฑ๋ฅ ์ต์ ํ
- ๋์ฉ๋ ํธ๋ํฝ ์ํฉ์์ ์ ์ ์ ์กฐํ ์์ฒญ์ 2์ด ์ด๋ด๋ก ์๋ต
2๏ธโฃ ์ฃผ๋ฌธ ์ฑ๋ฅ ์ต์ ํ
- ๋์ฉ๋ ํธ๋ํฝ ์ํฉ์์ ์ ์ ์ ์ฃผ๋ฌธ ์์ฒญ์ 3์ด ์ด๋ด๋ก ์๋ต
3๏ธโฃ ๋์ฉ๋ ํธ๋ํฝ ์ํฉ์์ ์ํ ์ฃผ๋ฌธ ๋์์ฑ ์ ์ด
- ์ฃผ๋ฌธ ์ ์ค์๊ฐ ์ฌ๊ณ ์กฐํ๋ฅผ ํตํ ์ฃผ๋ฌธ ์๋ฌ ๋ฐ์์จ 0%
4๏ธโฃ ์ค์๊ฐ ์๋ฒ ๋ชจ๋ํฐ๋ง ๋ฐ ๋ก๊ทธ ์์ง
- ๊ฐ๋ฐ์๊ฐ ๋ฐ ๋ป๊ณ ์ ์ ์๋ ๋ชจ๋ํฐ๋ง ์์คํ ๊ตฌ์ถ
๐ ์ฅ๋ฐ๊ตฌ๋ ์ถ๊ฐ
- ๊ฐ๋ณ ์ํ ์ฅ๋ฐ๊ตฌ๋ ์ถ๊ฐ
- ๋ ์ํผ ๊ด๋ จ ์ํ ์ฅ๋ฐ๊ตฌ๋ ์ถ๊ฐ
-
๐ก ์ด๋ฒคํธ ์ํ ์๋ฆผ
-
โ ๊ด๋ฆฌ์ ์ํ/๋ ์ํผ CRUD
๋์์ฑ ์ ์ด๋ฅผ ์ํด ๋น๊ด์ ๋ฝ ์ ์ฉ
๐ก ์ฌ๋ฌ ์ ์ ๊ฐ ๊ฐ์ ์ํ์ ์ฃผ๋ฌธํ๋ ๊ฒฝ์ฐ ๋ฐ์ํ ๋์์ฑ ๋ฌธ์ ํด๊ฒฐ์ ์ํด ์ฑ๋ฅ ๋น๊ต๋ฅผ ํตํด DB ๋น๊ด์ ๋ฝ ์ ์ฉ
์์ฌ ๊ฒฐ์ ๊ณผ์
Lettuce์ ์ด์ฉํ ๋ถ์ฐ๋ฝ VS Redisson์ ์ด์ฉํ ๋ถ์ฐ๋ฝ
- Lettuce๋ฅผ ์ด์ฉํ ๋ถ์ฐ๋ฝ
- spring-data-jpa์ ๊ธฐ๋ณธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ด์ฉํด์ ๋ณ๋์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ค์นํ์ง ์์๋ ๋จ
- spin lock ๋ฐฉ์์ผ๋ก ๋์ํด ๋์์ ๋ง์ ์ฐ๋ ๋๊ฐ ๋ฝ์ ํ๋ํ๋ ค๊ณ ๋๊ธฐํ๋ ๊ฒฝ์ฐ์ redis ์๋ฒ์ ๋ถํ๊ฐ ๊ฐ ์ ์๋ค.
- retry ๋ก์ง์ ์ง์ ๊ตฌํํด์ผ ํจ
- Redisson์ ์ด์ฉํ ๋ถ์ฐ๋ฝ
- spring์์ ์ฌ์ฉํ๊ธฐ ์ํด ๋ณ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ค์น ํ์
- redis pub-sub ๋ฐฉ์์ด๋ผ lettuce์ ๋นํด redis์ ๊ฐํด์ง๋ ๋ถํ๊ฐ ์ ์
โ ๋ถ์ฐ๋ฝ์ ์ฌ์ฉํ๋ค๋ฉด Redisson์ ์ฌ์ฉํ๊ธฐ๋ก ๊ฒฐ์
Redis์ ๋ถํ๊ฐ ์ ์ redisson์ ์ด์ฉํ ๋ถ์ฐ๋ฝ๊ณผ ๋น๊ด์ ๋ฝ์ ์ฌ์ฉํ ๊ฒฝ์ฐ ๋น๊ต
- 10000๊ฐ์ ๋์ ์์ฒญ์ด ๋ค์ด์ค๋ ๊ฒฝ์ฐ๋ฅผ ์ํ test์ฝ๋๋ฅผ ์ํํ ๊ฒฝ์ฐ Redis ๋ถ์ฐ๋ฝ๋ณด๋ค ๋น๊ด์ ๋ฝ์ด ํ๊ท 1๋ถ ์ ๋ ๋ ๋น ๋ฅด๊ฒ ๋์ด.
Gradle ํ ์คํธ ํ๊ท ์๋ | redis ๋ถ์ฐ๋ฝ(redisson) | ๋น๊ด์ ๋ฝ |
---|---|---|
๋์์ 10000๊ฐ ์์ฒญ | 5min 9sec | 3min 50sec |
CI/CD๋ฅผ ์ํด Github Actions ์ ์ฉ
๐ก ๋ ํผ๋ฐ์ค๋ ์ ์ง๋ง, ๋น ๋ฅด๊ฒ CI/CD๋ฅผ ๊ตฌ์ถํ ์ ์๊ณ , ํ์ฌ ํ๋ก์ ํธ ๊ท๋ชจ๊ฐ ํฌ์ง ์์์ Github Actions๋ฅผ ์ฌ์ฉํ๊ธฐ๋ก ๊ฒฐ์
Github Actions:
- ๋ณ๋์ ์๋ฒ ์์ด Github์์ ๋ฐ๋ก ์คํ ๊ฐ๋ฅํ๊ณ ์ด๊ธฐ ์ค์ ์ด ์ฌ์.
- Jenkins์ ๋นํด ํ๋ฌ๊ทธ์ธ์ด๋ ๋ ํผ๋ฐ์ค๊ฐ ์ ์.
- ์์ ๊ท๋ชจ์ ํ๋ก์ ํธ ๋๋ ๊ฐ๋จํ ์ํฌํ๋ก์ฐ์ ์ ํฉ.
Jenkins:
- ๋ค์ํ ํ๋ฌ๊ทธ์ธ์ ์ง์ํ๊ณ ์๋ํ ํ ์คํธ๋ฅผ ์ํํจ.
- ๋๊ท๋ชจ ํ๋ก์ ํธ์์ ๋น๋ ํ์ดํ๋ผ์ธ์ ๊ฐ๋จํ ๊ตฌ์ฑํ ์ ์์.
- ๋ ํผ๋ฐ์ค๊ฐ ๋ค์ํจ.
- ์ด๊ธฐ ์ค์ ์ด ๋ณต์กํ๊ณ , ๋ณ๋์ ์๋ฒ๋ฅผ ๊ตฌ์ฑํด์ผ ํ๋ฉฐ ๋ฌ๋์ปค๋ธ๊ฐ ์๋์ ์ผ๋ก ๊ฐํ๋ฆ.
๋ชจ๋ํฐ๋ง ํด๋ก Pinpoint์ prometheus/grafana ์ฌ์ฉ
๐ก ์ด๊ธฐ Prometheus/grafana๋ฅผ ์ฌ์ฉํ๋ค๊ฐ ์ดํ๋ฆฌ์ผ์ด์ ์ ๋ณ๋ชฉ ์ง์ ํ์ ์ ์ํด Pinpoint๋ฅผ ์ถ๊ฐ ๋์
prometheus/grafana:
- exporter๋ก ๋ชจ๋ํฐ๋ง ๋์ ์์คํ ์ผ๋ก๋ถํฐ pull ๋ฐฉ์์ผ๋ก ๋ฉํธ๋ฆญ์ ๋ฐ์์ค๋ ๋ฐฉ์์ผ๋ก ๋์
- ํ๋์ ์๋น์ค๋ง ๋ชจ๋ํฐ๋งํ๋ ๊ฒ์ด ์๋๋ผ ์ฐ๊ฒฐ๋ ๋ค๋ฅธ ์๋น์ค๋ค์ ๋ํ ๋ชจ๋ํฐ๋ง
- HA๋ฅผ ์ํ ์ด์คํ๋ ํด๋ฌ์คํฐ๋ง์ด ๋ถ๊ฐ๋ฅํด์, thanos๋ฅผ ์ถ๊ฐ ์ค์นํด์ผํจ.
- ์ฝ๋ ๋ ๋ฒจ๋ก ์๋น์ค์ ๋ณ๋ชฉ ์ง์ ์ ์๋ ค์ฃผ์ง ์์.
Pinpoint:
- ์ดํ๋ฆฌ์ผ์ด์ ์ ๋ณ๋ชฉ ์ง์ ์ ์ฝ๋ ๋ ๋ฒจ๋ก ํ์ ํ์ฌ, ์ฑ๋ฅ ์ ํ ์์ ๋ฐ ๋ฌธ์ ์์ธ์ ์ถ์ ๊ฐ๋ฅํจ.
- ๋ถ์ฐ ์์คํ ์ ๊ตฌ์ฑ ๋งต๊ณผ ๋ ธ๋ ๊ฐ์ ํธ๋์ญ์ ์๋ฅผ ํ ๋์ ํ์ ๊ฐ๋ฅํจ.
- ํน์ ํธ๋์ญ์ ์์ ์คํ๋ ๋ฉ์๋์ ์๋ต์๊ฐ์ ํ์ธํ ์ ์๊ณ ์ดํ๋ฆฌ์ผ์ด์ ์ ์ค๋ฅ๋ ์์ธ ์ ๋ณด๋ ํ์ธ ๊ฐ๋ฅ
์ด๋ฒคํธ ํ๋ก Kafka ์ฌ์ฉ
๐ก ํ ์ธ ์ด๋ฒคํธ ๋ฐ์ ์ ์ด๋ฒคํธ ์ฒ๋ฆฌ API๋ค์ ์๋ฒ ๋ถ๋ฆฌ๋ฅผ ํตํ์ฌ ์๋ฒ์ ์ฒ๋ฆฌ ์ฑ๋ฅ์ ๊ฐ์ ํ๊ธฐ ์ํด Kafka ์ฌ์ฉ
Kafka | RabbitMQ | Redis | |
---|---|---|---|
์ฃผ๋ฌธ ๋ด์ญ ์ด๋ฒคํธ ๋ณด์กด ํ์์ฑ | ์์ ํ์ธ ๋ฐ ์ด๋ฒคํธ ๋ณ๋ ์ ์ฅ | ์์ ํ์ธ | ์์ ํ์ธ ์์ด ์ญ์ |
๋ค์ API์ ๋์ ์ฒ๋ฆฌ ๊ฐ๋ฅ์ฑ | Pub/Sub ๋ชจ๋ธ | ๊ธฐ๋ณธ์ ์ผ๋ก ๋จ์ผ ์์ ์ ๋์ | Pub/Sub ๋ชจ๋ธ |
Redis๋ก ์บ์ ์ ์ฉ
๐ก ๋๋ฉ ํ์ด์ง์ ์กฐํ ์ฑ๋ฅ ๊ฐ์ ์ ์ํด Redis์ caching ๊ธฐ๋ฅ ์ฌ์ฉ
Redis | Memcached | |
---|---|---|
์ฌ์ฉ๋ชฉ์ ๋ถํฉ์ฑ (Refresh Token, Caching) | ์ฌ์ฉ ๋ชฉ์ ์ ๋ถํฉํ๋ฉฐ ๋ค์์ ํ์ฉ ์ฌ๋ก ์์ | ์ฌ์ฉ ๋ชฉ์ ์ ๋ถํฉํ๋ฉฐ AWS Elasticache์์ ์ง์ |
๊ณ ๊ฐ์ฉ์ฑ | Replication ๊ตฌ์ถ ๊ฐ๋ฅ | Replication ๋ถ๊ฐ |
์ฌ์ฉ์ฑ | ์ฌ์ฉ ๊ฒฝํ ์์ | ์ฌ์ฉ ๊ฒฝํ ์์, ์คํฐ๋ ํ์ |
๋ก๋๋ฐธ๋ฐ์
๐ก ํธ๋ํฝ ์ฆ๊ฐ๋ฅผ ๋๋นํด ์๋น ์๋ฒ ์ถ๊ฐ๋ฅผ ํตํ Scale out์ ๋ก๋๋ฐธ๋ฐ์๋ก AWS ALB ์ฌ์ฉ
ALB | Nginx | |
---|---|---|
Scale-out ํธ์์ฑ | AWS์ Auto-scaling ํ์ฉ ๊ฐ๋ฅ Server ์ถ๊ฐ ์ target group์ ์ถ๊ฐ๋ก ๊ฐํธํ๊ฒ scale-out ๊ฐ๋ฅ |
์ด๋ฒคํธ ์ ์๋ Scale-out ํ์ server ์ถ๊ฐ ์์ ์ค์ ํ์ผ ์ง์ ์์ ํ์ |
์ ์ ๋ฆฌ์์ค ํ ๋น์ฑ | ํธ๋ํฝ์ ๋ฐ๋ผ ์์ ํ ๋น | ์์ ํธ๋ํฝ๊ณผ ๋ค๋ฅผ ์ ๋ฆฌ์์ค ์ด๊ณผ ๋๋ ๋ถ์กฑ ๋ฐ์ ๊ฐ๋ฅ |
HTTPS ์ ์ฉ | Certificate Manger๋ก HTTPS ์ ์ฉ ๊ฐ๋ฅ | HTTPS ์ ์ฉ์ ์ํ ์ธ์ฆ์ ๋ฐ๊ธ ๋ฐ ์ค์ ํ์ผ ์ง์ ์์ ํ์ |
Redis ์บ์๋ก ์กฐํ ์ฑ๋ฅ ๊ฐ์
๋๋ฉํ์ด์ง์ ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ฌ ๋ Redis ์บ์๋ฅผ ์ ์ฉํจ์ผ๋ก์จ ํํ์ด์ง ์กฐํ ์ฑ๋ฅ์ ๊ฐ์ .
ํ ์คํธ ํ๊ฒฝ:
- BE ์๋ฒ: t3.2xlarge
- DB ์๋ฒ: db.t3.micro
- Thread: 1000๋ช
- Ramp-up time: 1
- Loop count: 1
ํ ์คํธ ๊ฒฐ๊ณผ:
- Redis Caching ์ ์ฉ ์ : ์๋๋ฆฌ์ค ํ ์คํธ ๊ฒฐ๊ณผ ํ๊ท ์๋ต์๊ฐ 9445ms, TPS 59.31
- Redis Caching ์ ์ฉ ํ: ์๋๋ฆฌ์ค ํ ์คํธ ๊ฒฐ๊ณผ ํ๊ท ์๋ต์๊ฐ 1241ms, TPS 286.69
1000๊ฐ์ ์ฐ๋ ๋ ์์ฒญ์ ๋ํ ํ๊ท ์๋ต์๊ฐ 87% ๊ฐ์ ๋จ.
ํ๊ท ์๋ต์๊ฐ | TPS | |
---|---|---|
Redis Cache X | 9445ms | 59.31 |
Redis Cache O | 1241ms | 286.69 |
Kafka๋ฅผ ํตํ ์ฃผ๋ฌธ ์ฑ๋ฅ ๊ฐ์
์นดํ์นด๋ฅผ ํตํด ์ฃผ๋ฌธ์ ๋ฐ์ผ๋ฉด ์ฐ์ ์ ์ผ๋ก ์ฌ๊ณ ๋ฅผ ์ฐจ๊ฐํ๊ณ , ์ฃผ๋ฌธ ๊ฒฐ๊ณผ ์ ์ฅ, ์ฅ๋ฐ๊ตฌ๋ ์ญ์ ๋ฑ์ ์์ ์ ๋น๋๊ธฐ์ ์ผ๋ก ์นดํ์นด consumer์์ ์ฒ๋ฆฌํจ์ผ๋ก ์ฃผ๋ฌธ ๋ก์ง์ ๋ํ ์ฑ๋ฅ ๊ฐ์ .
์นดํ์นด ์ ์ฉ ์ /ํ ์ฑ๋ฅ ์งํ
ํ ์คํธ ํ๊ฒฝ:
- BE ์๋ฒ: t3.2xlarge
- DB ์๋ฒ: db.t3.micro
- Thread: 1000๋ช
- Ramp-up time: 1
- Loop count: 1
ํ ์คํธ ๊ฒฐ๊ณผ:
- ์นดํ์นด ์ ์ฉ ์ : ์๋๋ฆฌ์ค ํ ์คํธ ๊ฒฐ๊ณผ ํ๊ท ์๋ต์๊ฐ 5480ms
- ์นดํ์นด ์ ์ฉ ํ: ์๋๋ฆฌ์ค ํ ์คํธ ๊ฒฐ๊ณผ ํ๊ท ์๋ต์๊ฐ 2100ms
1000๊ฐ์ ์ฐ๋ ๋ ์์ฒญ์ ๋ํ ํ๊ท ์๋ต์๊ฐ 61% ๊ฐ์ ๋จ.
ํ๊ท ์๋ต์๊ฐ | TPS | |
---|---|---|
์นดํ์นด ์ ์ฉ ์ | 5480ms | 32.54 |
์นดํ์นด ์ ์ฉ ํ | 2100ms | 68.1 |

N+1 ๋ฌธ์ ํด๊ฒฐ
๐ก Product ํ ์ด๋ธ ์กฐํ ์ ์ฐ๊ด๋ Image, Purchase, Cart ํ ์ด๋ธ์์ N+1 ๋ฌธ์ ๋ฐ์
Product - Image
๐ก @BatchSize๋ฅผ ์ด์ฉํ์ฌ N+1 ๋ฌธ์ ํด๊ฒฐ
- Product์ Image๋ 1:N ์๋ฐฉํฅ ๊ด๊ณ
- ์ํ๋ค์ ์กฐํํ๋ ํ์ด์ง์์ ์กฐํ๋ ์ํ ์๋งํผ ์ฐ๊ด๋ image๋ฅผ ์กฐํํ๋ N+1 ๋ฌธ์ ๋ฐ์
ํด๊ฒฐ ๋ฐฉ๋ฒ
- Fetch Join, @BatchSize, @Fetch(FetchMode.SUBSELECT) ๋น๊ต
ํ ์คํธ ํ๊ท ์๋ต์๋(1๋ฒ) | ํน์ด ์ฌํญ | |
---|---|---|
fetch join | 617ms | ๊ธฐ์กด Pagination ์์ ์ฌ์ฉํ๋ LIMIT ๊ตฌ๋ฌธ์ด ๋ฑ์ฅํ์ง ์์ firstResult/maxResults specified with collection fetch; applying in memory ๊ฒฝ๊ณ ๋ก๊ทธ ๋ฐ์ Fetch join ํ๋ image๊ฐ collection ๋ฐ์ดํฐ๋ผ ์กฐํํ๋ ๋ฐ์ดํฐ๊ฐ ๋งค๋ฒ ๋ฌ๋ผ์ง๊ธฐ ๋๋ฌธ์ limit ๊ตฌ๋ฌธ์ ์ฌ์ฉํ ์ ์๊ณ ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ์กฐํํ ํ์ memory์์ ํ์ด์ง ์ฒ๋ฆฌํ๊ธฐ ๋๋ฌธ์ ๋ฉ๋ชจ๋ฆฌ ๋ฌธ์ ๋ฐ์ ๊ฐ๋ฅ |
@BatchSize() | 652ms | ํญ์ @BatchSize(size = n)์์ ์ค์ ํ ์ซ์๋งํผ์ product์ ๊ด๋ จ๋ image๋ฅผ ์กฐํํจ. purebasket ์๋ฒ์ค์์๋ ๋ ๋ฉ ํ์ด์ง๋ ๋ ์ํผ ํ์ด์ง ๋ฑ์์ ๋ณด์ฌ์ฃผ๋ ์ํ์ ๊ฐ์๊ฐ ๋ฌ๋ผ์, ํญ์ n๊ฐ์ ์ํ์ ๋ํ image๋ฅผ ์กฐํํ๋ ๊ฒ์ ์ ํฉํ์ง ์์ ์๋ ์๋ค. |
@Fetch(FetchMode.SUBSELECT) | 622ms | Subquery๋ฅผ ํฌํจํ๋ฉด 8๊ฐ์ ์ฟผ๋ฆฌ๋ก @BatchSize๋ฅผ ์ฌ์ฉํ์ ๋๋ณด๋ค ์ฟผ๋ฆฌ๊ฐ ๋ง์. |
-
FetchJoin์ collection์ fetch join ํ๋ ๊ฒฝ์ฐ ๋ฉ๋ชจ๋ฆฌ ์ฑ๋ฅ ์ด์๊ฐ ๋ฐ์ํ ์ ์์ผ๋ฏ๋ก @BatchSize๋ @Fetch(FetchMode.SUBSELECT)๋ฅผ ์ด์ฉํ๊ธฐ๋ก ํจ.
-
@BatchSize()์ @Fetch(FetchMode.SUBSELECT) ๋น๊ต
ํ๊ท ์๋ต ์๋(ms) | 100 ๋ช ๋์ ์์ฒญ | 500๋ช ๋์ ์์ฒญ | 1000๋ช ๋์ ์์ฒญ |
---|---|---|---|
@BatchSize() | 534.2 | 2544.0 | 6309.39 |
@Fetch(FetchMode.SUBSELECT) | 651.7 | 3144.4 | 6724.22 |
- ํ์ด์ง๋ง๋ค ์กฐํํด์ผ ํ๋ ์ํ์ ๊ฐ์๊ฐ ๋ฌ๋ผ์ @BatchSize๋ฅผ ์ฌ์ฉํ๋ฉด ํ์ ์ด์์ ์ํ ์กฐํ๊ฐ ์์ ์ ์์ง๋ง, ํ ์คํธ ๊ฒฐ๊ณผ @Fetch(FetchMode.SUBSELECT)๋ณด๋ค ์ฑ๋ฅ์ด ์ข์์ @BatchSize๋ฅผ ์ด์ฉํ๊ธฐ๋ก ํจ.
@Entity
@Getter
@Table(name = "product")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Product {
@BatchSize(size = 21)
@OneToMany(mappedBy = "product", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Image> images = new ArrayList<>();
}
Product - Purchase
- Product์ Purchase๋ 1:N ๋จ๋ฐฉํฅ ๊ด๊ณ(Purchase์์๋ง Product ์กฐํ ๊ฐ๋ฅ) - ์ ์ ๋ณ ์ฃผ๋ฌธ ๋ด์ญ์ ์กฐํํ๋ ํ์ด์ง์์ ์กฐํ๋ ์ฃผ๋ฌธ ๋ด์ญ ์๋งํผ ์ฐ๊ด๋ product๋ฅผ ์กฐํํ๋ N+1 ๋ฌธ์ ๋ฐ์
ํด๊ฒฐ ๋ฐฉ๋ฒ
-
Fetch Join
purchase ์๋น์ค๋ pagination์ ์ฌ์ฉํ๊ณ ์์ง๋ง, fetch join ๋๋ ๋์์ธ Purchase๊ฐ collection์ด ์๋๊ธฐ ๋๋ฌธ์ Fetch Join์ ์ฌ์ฉํด๋ ๋ฌธ์ ์์(OutOfMemoryError ๋ฐ์ํ์ง ์์)
@Repository
public interface PurchaseRepository extends JpaRepository<Purchase, Long> {
@Query("SELECT p FROM Purchase p JOIN FETCH p.product WHERE p.member = :member")
Page<Purchase> findAllByMember(Member member, Pageable pageable);
}
Product - Cart
- Product์ Cart๋ 1:N ๋จ๋ฐฉํฅ ๊ด๊ณ(Cart์์๋ง Product ์กฐํ ๊ฐ๋ฅ) - ์ ์ ๋ณ ์ฅ๋ฐ๊ตฌ๋ ๋ด์ญ์ ์กฐํํ๋ ํ์ด์ง์์ ์กฐํ๋ ์ฅ๋ฐ๊ตฌ๋ ๋ด์ญ ์๋งํผ ์ฐ๊ด๋ product๋ฅผ ์กฐํํ๋ N+1 ๋ฌธ์ ๋ฐ์
ํด๊ฒฐ ๋ฐฉ๋ฒ
-
Fetch Join
cart ์๋น์ค๋ pagination์ ์ฌ์ฉํ๊ณ ์์ง ์์
pagination์ ์ฌ์ฉํ๋ค๊ณ ํด๋, fetch join ๋๋ ๋์์ธ Product๊ฐ collection์ด ์๋๊ธฐ ๋๋ฌธ์ Fetch Join์ ์ฌ์ฉํด๋ ๋ฌธ์ ์์(OutOfMemoryError ๋ฐ์ํ์ง ์์)
@Repository
public interface CartRepository extends JpaRepository<Cart, Long> {
@Query("SELECT c FROM Cart c JOIN FETCH c.product WHERE c.member = :member")
List<Cart> findAllByMember(Member member);
}
DB Connection Pool์ ๋๊ธฐ ์๊ฐ์ด ์ ์ฒด ์๋ต ์๊ฐ์ 90% ์ด์์ ์ ์
๐ก ์ด๋น 1000๊ฐ์ ์ฃผ๋ฌธ ์์ฒญ ํ ์คํธ ๊ฒฐ๊ณผ ๋ณ๋ชฉ๊ตฌ๊ฐ์ด HikariCP์ getConnection() ๋ฉ์๋์ธ ๊ฒ์ ๋ฐ๊ฒฌํ๊ณ ๋ณ๋ชฉ๊ตฌ๊ฐ ํด์ ์๋
-
ํ ์คํธ ํ๊ฒฝ:
- BE ์๋ฒ: 1EA x t3.xlarge
- DB ์๋ฒ: 1EA x db.t3.micro (connection pool = 60)
- Thread: 1000๋ช / Ramp-up time: 1s / loop count: 1
-
ํธ๋ฌ๋ธ ์ํ ๊ณผ์ :
-
์คํ๋ง์ ๊ธฐ๋ณธ ์ค์ ์ผ๋ก max pool size์ minimum idle์ด 10์ธ ๊ฒ์ ํ์ธ
-
AWS RDS ์๋ฒ ํ์ธ ๊ฒฐ๊ณผ DB์ ์ต๋ connection์ 60์ธ ๊ฒ์ ํ์ธ
-
BE ์๋ฒ์ connection pool ์ฌ์ด์ฆ ์ฆ๊ฐ์ ๋ํ trade-off๋ ๋ฉ๋ชจ๋ฆฌ ์ ์ ์ธ ๊ฒ์ ํ์ธ
-
BE ์๋ฒ ์์คํ ์ ๋ฉ๋ชจ๋ฆฌ๋ ์ฌ์ ๊ฐ ์๋ ๊ฒ์ ํ์ธํ๊ณ connection pool ์ฌ์ด์ฆ ํ์ฅ
spring.datasource.hikari.maximum-pool-size=60 spring.datasource.hikari.minimum-idle=60
-
-
๊ฒฐ๋ก :
- HikariCP์์ ๋ณ๋ชฉ๊ตฌ๊ฐ ์ง์๋จ
- ํ์ฌ ์๋ฒ ๊ตฌ์ฑ์ ํ๊ณ์ธ 60๊ฐ์ connection์ผ๋ก๋ ์ด๋น 1000๊ฐ์ ์ฃผ๋ฌธ์ ์ฒ๋ฆฌํ ์ ์์์ ์ธ์ โ BE/DB ์๋ฒ Scale up ๊ฒฐ์
Scale-up / Scale-out ํด๋ ์ฃผ๋ฌธ ์ฑ๋ฅ ํฅ์ ์ ๋๋ ๋ฌธ์
๐ก **DB ์๋ฒ scale-up ๋ฐ BE ์๋ฒ๋ฅผ ๋ ๋๋ก scale-outํ์ง๋ง ์ฃผ๋ฌธ ์ฑ๋ฅ์ด ํฌ๊ฒ ํฅ์๋์ง ์์**scale up ์ ํ ํ ์คํธ ๊ฒฐ๊ณผ
ํ ์คํธ ํ๊ฒฝ:
- BE ์๋ฒ - 2EA x t3.2xlarge
- DB ์๋ฒ - 1EA x db.t3.xlarge (connection pool = 1600)
- Thread : 2000๋ช / Ramp-up time : 1s / loop count : 1
ํ ์คํธ ๊ฒฐ๊ณผ
ํ๊ท ์๋ต์๊ฐ | TPS | |
---|---|---|
BE ์๋ฒ 1๋ | 4410ms | 70.62 |
BE ์๋ฒ 2๋ | 3010ms | 83.16 |
- ํ์ฌ ์ฌ๊ณ ๊ด๋ฆฌ๋ ์นดํ์นด์ ์ด๋ฒคํธ๋ฅผ ์ ๋ฌํ๊ธฐ ์ ์ ์ฒ๋ฆฌํ๊ณ ์ฌ๊ณ ์ฐจ๊ฐ์ ์ํด ๋น๊ด์ ๋ฝ ์ฌ์ฉ์ค
- Pinpoint ํ์ธ ๊ฒฐ๊ณผ HikariCP ์ getConnection() ๋ฉ์๋ ์คํ ์๊ฐ์ด 2~3์ด ์ ๋ ๊ฑธ๋ฆผ
- Connection pool ์ฌ์ด์ฆ ์กฐ์ ํ๋ฉด์ test๋ฅผ ์ํํ์ง๋ง connection pool ์ปค์ง๋ฉด getConnection() ์ ๊ฑธ๋ฆฌ๋ ์๊ฐ์ ์ค์ง๋ง, ๋ฝ์ ์ป๊ธฐ ์ํด ๋๊ธฐํ๋ ์๊ฐ์ด ์ฆ๊ฐํด์ ์ฑ๋ฅ์ด ํฌ๊ฒ ํฅ์ ๋์ง ์์
HikariCP Connection Pool size ๋ณ ํ ์คํธ ๊ฒฐ๊ณผ
ํ ์คํธ ํ๊ฒฝ:
- ์๋ฒ ๊ตฌ์ฑ - 2EA t3.2xlarge | rds - db.t3.xlarge
- Thread : 2000๋ช / Ramp-up time : 1s / loop count : 1
Connection Pool size | 10 | 50 | 100 | 150 | 200 |
---|---|---|---|---|---|
TPS | 86.76 | 75.11 | 70.89 | 80.86 | 69.24 |
ํ๊ท ์๋ต์๊ฐ | 3.00sec | 3.38sec | 3.41sec | 2.81sec | 3.18sec |
Connection Pool์ด 10์ธ ๊ฒฝ์ฐ
DB Connection Pool์ ๋๊ธฐ ์๊ฐ์ 4684ms๋ก ์ ์ฒด ์คํ ์๊ฐ์ 97%, ๋ฝ์ ์ป๊ธฐ ์ํ stock์ ์กฐํํ๋ sql์ ์คํํ ๋ ๊ฑธ๋ฆฌ๋ ์๊ฐ์ด 154ms๋ก ์ ์ฒด์ 3%๋ฅผ ์ฐจ์งํจ.
Connection Pool์ด 100์ธ ๊ฒฝ์ฐ
DB Connection Pool์ ๋๊ธฐ ์๊ฐ์ 2730ms๋ก ์ ์ฒด์ 57%, ๋ฝ์ ์ป๊ธฐ ์ํ stock์ ์กฐํํ๋ sql์ ์คํํ ๋ ๊ฑธ๋ฆฌ๋ ์๊ฐ์ด 2014ms๋ก 42%๋ฅผ ์ฐจ์งํจ.
Connection Pool์ด 200์ธ ๊ฒฝ์ฐ
DB Connection Pool์ ๋๊ธฐ ์๊ฐ์ ์์ง๋ง, ๋ฝ์ ์ป๊ธฐ ์ํ stock์ ์กฐํํ๋ sql์ ์คํํ ๋ ๊ฑธ๋ฆฌ๋ ์๊ฐ์ด 3933ms๋ก ์ ์ฒด์ 99%๋ฅผ ์ฐจ์งํจ.
- ๊ฒฐ๋ก Connection Pool์ ์ฆ๊ฐ์ํค๋ฉด BE ์๋ฒ์ DB Connection ๋๊ธฐ์๊ฐ์ 0ms๊ฐ ๋์์ผ๋, ๋น๊ด์ ๋ฝ์ด ์ ์ฉ๋ stock ํ ์ด๋ธ ์กฐํ ์ฟผ๋ฆฌ์ ์๋ต ์๊ฐ ์ฆ๊ฐํจ. connection pool์ด ์ถฉ๋ถํด๋ ๋น๊ด์ ๋ฝ์ ์ป๊ธฐ ์ํด ๋๊ธฐํ๋ ์๊ฐ์ด ๋ณ๋ชฉ์ ์์ธ์. ํด๊ฒฐํ๊ธฐ ์ํด์ ์ฌ๊ณ ๊ด๋ฆฌ๋ฅผ ์ํด ๋ฝ์ ์ป๋ ๋ก์ง์ consumer์์ ๊ตฌํํด์ผ ํจ.
Redis Cache ์ ์ฉ ์ SerializationException
๋๋ฉ ํ์ด์ง ์ํ ์กฐํ API์ redis cache๋ฅผ ์ ์ฉํ๊ณ redis ์ ์ ์ฅ๋ ๊ฐ์ ๋ถ๋ฌ์ฌ ๋ Serialization ๋ฌธ์ ๋ฐ์Could not read JSON:Cannot construct instance of `org.springframework.data.domain.PageImpl` (no Creators, like default constructor, exist): cannot deserialize from Object value
๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ ์ด์ ๋ Jackson์ด๋ Gson ๊ฐ์ ์ง๋ ฌํ/์ญ์ง๋ ฌํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค์ด ๋ณดํต ๊ธฐ๋ณธ ์์ฑ์๋ getter/settter๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์ง๋ ฌํ/์ญ์ง๋ ฌํ๋ฅผ ์ํํ๋๋ฐ, PageImpl์ Creator๊ฐ ์์ด์ Object๋ก deserializeํ ์ ์์ด์ ์๊น
@JsonCreator๋ก PageImpl์ ๋ํ Constructor๋ฅผ ๋ง๋ค์ด ์ญ์ง๋ ฌํ์์ ์ฌ์ฉํ ์ ์๋๋ก ํด์ ํด๊ฒฐ
@JsonIgnoreProperties(ignoreUnknown = true, value = {"pageable"})
public class RestPageImpl<T> extends PageImpl<T> {
@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
private RestPageImpl(@JsonProperty("content") List<T> content,
@JsonProperty("number") int number,
@JsonProperty("size") int size,
@JsonProperty("totalElements") Long totalElements) {
super(content, PageRequest.of(number, size), totalElements);
}
private RestPageImpl(Page<T> page) {
super(page.getContent(), page.getPageable(), page.getTotalElements());
}
public static<T> RestPageImpl<T> from(Page<T> page) {
return new RestPageImpl<T>(page);
}
}
Kafka ์ ์ฉ ์ ClassNotFoundException
์นดํ์นด ์ปจ์๋จธ ์๋ฒ๋ฅผ ๋ถ๋ฆฌํ ํ ์ปจ์๋จธ์์ ClassNotFoundException ๋ฐ์failed to resolve class name. Class not found
[com.example.purebasketbe.domain.purchase.dto.KafkaPurchaseDto]
์ง๋ ฌํ/์ญ์ง๋ ฌํ ๊ณผ์ ์์๋ package ์ด๋ฆ๊น์ง ํฌํจํ๊ธฐ ๋๋ฌธ์, producer์์ serializeํ ๋ class์ fullname์ ์ฌ์ฉํ๋ฏ๋ก consumer์์ deserializeํ ๋ class is not in the trusted packages ์๋ฌ๊ฐ ๋ฐ์ํ๋ ๊ฒ
ํด๊ฒฐ๋ฐฉ๋ฒ
JsonDeserializer์ setRemoveTypeHeaders ์ถ๊ฐ
์นดํ์นด์์ ๋ฉ์์ง๋ฅผ ์ ์กํ ๋ headers์ metadata๋ฅผ ๋ด์์ ๋ณด๋ด๋๋ฐ, ์ด ๋ ๋ด๊ธฐ๋ metadata์ target type์ ํฌํจํจ.(target type์ ์ ์กํ๊ณ ์ํ๋ ๊ฐ์ฒด์ ํจํค์ง๋ช )
consumerFactory์์ useHeadersIfPresent ๊ฐ์ false๋ก ์ง์ ํ์ฌ ์ญ์ง๋ ฌํ์์ header์ ๋ด๊ธด ํจํค์ง๋ช ์ ์ฌ์ฉํ์ง ์๋๋ก ์ค์ ํ์ฌ ํด๊ฒฐ
@Bean
public ConsumerFactory<String, KafkaPurchaseDto> consumerFactory() {
Map<String, Object> configs = new HashMap<>();
configs.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
return new DefaultKafkaConsumerFactory<>(configs, new StringDeserializer(),
new JsonDeserializer<>(KafkaPurchaseDto.class, false)
);
}
Category | Technologies |
---|---|
Tech Stack | Spring Boot, Spring JPA, Spring Security, Kafka, Elastic Search |
TEST | Junit5, Jmeter |
CI/CD | Github Action |
DB | AWS RDS (MySQL), Redis |
DevOps | Docker, AWS EC2, AWS S3 |
Logging & Monitoring | Logstash/Kibana, Prometheus/Grafana, Pinpoint |
Category | Technologies |
---|---|
Tech Stack | React |
์ญํ | ์ด๋ฆ | github | ๊ธฐ์ ๋ธ๋ก๊ทธ | |
---|---|---|---|---|
ํ์ฅ | ํ์ค์ | [email protected] | https://github.com/hjunyoung | https://hjunyoung.github.io/ |
ํ์ | ์ด์ํด | [email protected] | https://github.com/hyouoo | https://devlog1921.tistory.com/ |
ํ์ | ๋ฐ์์ค | [email protected] | https://github.com/SeoYoonP | https://velog.io/@wideskyinme/posts |