PookyBlog๋ ๋จ์ํ CRUD ๊ธฐ๋ฅ์ ๋์ด, ๋์ฉ๋ ํธ๋ํฝ ํ๊ฒฝ์์๋ ์์ ์ ์ผ๋ก ๋์ํ๋ ๊ณ ์ฑ๋ฅ ๋ฐฑ์๋ ์์คํ ์ ๊ตฌ์ถํ๊ธฐ ์ํ ๊ฐ์ธ ํ๋ก์ ํธ์ ๋๋ค. ๋๋ฉ์ธ ๊ธฐ๋ฐ์ ๋ฉํฐ ๋ชจ๋ ๊ตฌ์กฐ๋ฅผ ๋ฐํ์ผ๋ก CQRS(๋ช ๋ น๊ณผ ์กฐํ ์ฑ ์ ๋ถ๋ฆฌ), ์ด๋ฒคํธ ๊ธฐ๋ฐ ์ํคํ ์ฒ ๋ฑ ํ๋์ ์ธ ์ค๊ณ ์์น์ ์ ์ฉํ์ฌ ์ค์ ์๋น์ค ์์ค์ ๊ธฐ์ ์ ๊ณผ์ ๋ฅผ ํด๊ฒฐํ๋ ๋ฐ ์ง์คํ์ต๋๋ค.
์ด ํ๋ก์ ํธ๋ฅผ ํตํด ๊ธฐ์ ์ ์๋ฆฌ๋ฅผ ๊น์ด ํ๊ตฌํ๊ณ , ์ฑ๋ฅ์ ์ธก์ ํ๋ฉฐ, ์์คํ ์ ์ ์ง์ ์ผ๋ก ๊ฐ์ ํด ๋๊ฐ๋ ๊ณผ์ ์ ๊ธฐ๋กํ๊ณ ์์ต๋๋ค.
Java Spring Boot Spring Security JPA QueryDSL
๋๋ฉ์ธ ์ค์ฌ์ ๋ฉํฐ ๋ชจ๋ ์ค๊ณ๋ฅผ ํตํด ๊ฐ ๋ชจ๋์ ์ฑ ์๊ณผ ์์กด์ฑ์ ๋ช ํํ ๋ถ๋ฆฌํ์ฌ ์ ์ง๋ณด์์ฑ๊ณผ ํ์ฅ์ฑ์ ๋์์ต๋๋ค.
.
โโโ pookyBlog (root)
โ
โโโ ๐ common # Outbox, Snowflake ๋ฑ ์ฌ๋ฌ ๋ชจ๋์์ ๊ณตํต์ผ๋ก ์ฌ์ฉํ๋ ์ ํธ๋ฆฌํฐ
โ
โโโ ๐ services # ํต์ฌ ๋น์ฆ๋์ค ๋ก์ง๊ณผ ๋๋ฉ์ธ ๋ชจ๋ธ์ ํฌํจํ๋ ์๋น์ค ๊ณ์ธต
โ โโโ ๐ comment
โ โโโ ๐ hot-post
โ โโโ ๐ like
โ โโโ ๐ post
โ โโโ ๐ post-read # CQRS์ ์กฐํ(Query) ์ฑ
์ ๋ชจ๋
โ โโโ ๐ user
โ โโโ ๐ view
โ
โโโ ๐ web # API End-point๋ฅผ ์ ๊ณตํ๊ณ ์ธ๋ถ ์์ฒญ์ ์ฒ๋ฆฌํ๋ ์น ๊ณ์ธต
โ
โโโ build.gradle
CQRS ํจํด์ ๊ธฐ๋ฐ์ผ๋ก, ์์คํ ์ ์ฐ๊ธฐ ์ฑ ์(Command)๊ณผ ์ฝ๊ธฐ ์ฑ ์(Query)์ ๋ถ๋ฆฌํ์ฌ ํ์ฅ์ฑ๊ณผ ์ฑ๋ฅ์ ๊ทน๋ํํ์ต๋๋ค.
graph TD
%% ๋
ธ๋ ์ ์
subgraph Client
UserRequest[์ฌ์ฉ์ ์์ฒญ]
end
subgraph "Command Service (์ฐ๊ธฐ/์์ ์ฑ
์)"
direction LR
A[API Controller]
B(Business Logic)
C{JPA & Outbox}
D[MySQL]
E[Outbox Table]
end
subgraph "Query Service (์กฐํ ์ฑ
์)"
direction LR
G[Kafka Consumer]
H[Data Projection]
I[Redis]
end
F[Message Queue - Kafka]
%% ํ์ดํ ์ฐ๊ฒฐ
UserRequest -- "CUD Request" --> A
A --> B
B -- "@Transactional" --> C
C --> D
C --> E
E -- "Kafka Connect / Scheduler" --> F
F --> G
G --> H
H --> I
UserRequest -- "Read Request" --> A
A -- "1. Read from Cache" --> I
A -- "2. Cache-Miss ์ Fallback" --> B
- **์ฐ๊ธฐ/์์ /์ญ์ ์์ฒญ(Command)**์ Command Service๋ฅผ ํตํด ์ฒ๋ฆฌ๋๋ฉฐ, ๋ฐ์ดํฐ ๋ณ๊ฒฝ์ ์ฃผ DB์ Outbox ํ ์ด๋ธ์ ํ๋์ ํธ๋์ญ์ ์ผ๋ก ์ปค๋ฐ๋์ด ๋ฐ์ดํฐ ์ ํฉ์ฑ์ ๋ณด์ฅํฉ๋๋ค.
- Outbox์ ์์ธ ์ด๋ฒคํธ๋ Kafka๋ก ์์ ํ๊ฒ ๋ฐํ๋ฉ๋๋ค.
- **์กฐํ ์์ฒญ(Query)**์ 1์ฐจ์ ์ผ๋ก Redis์์ ์ฒ๋ฆฌ๋์ด ๋งค์ฐ ๋น ๋ฅธ ์๋ต ์๋๋ฅผ ๋ณด์ฅํฉ๋๋ค. Redis์ ๋ฐ์ดํฐ๊ฐ ์๋ ๊ฒฝ์ฐ์๋ง DB๋ฅผ ์กฐํํ๋ ํด๋ฐฑ(Fallback) ๋ก์ง์ผ๋ก ์์ ์ฑ์ ํ๋ณดํ์ต๋๋ค.
- Query Service์ Kafka Consumer๋ ์ด๋ฒคํธ๋ฅผ ๊ตฌ๋ ํ์ฌ ์กฐํ ๋ชจ๋ธ์ ์ค์๊ฐ์ผ๋ก ์ต์ ์ํ๋ก ์ ์งํฉ๋๋ค.
erDiagram
USER {
bigint id PK "์ ์ ID"
string username
string password
string email
string nickname
Role role
}
POST {
bigint id PK "๊ฒ์๊ธ ID"
bigint user_id FK
string title
text content
string writer
int view
}
COMMENT {
bigint id PK "๋๊ธ ID"
bigint post_id FK
bigint user_id FK
text comments
}
POST_LIKE {
bigint id PK "์ข์์ ID"
bigint post_id FK
bigint user_id FK
}
LIKE_COUNT {
bigint id PK "๊ฒ์๊ธ ID์ ๋์ผ"
long likeCount
long version "๋๊ด์ ๋ฝ์ ์ํ ๋ฒ์ "
}
USER ||--o{ POST : "์์ฑ"
USER ||--o{ COMMENT : "์์ฑ"
USER ||--o{ POST_LIKE : "ํด๋ฆญ"
POST ||--o{ COMMENT : "ํฌํจ"
POST ||--o{ POST_LIKE : "ํฌํจ"
POST ||--|{ LIKE_COUNT : "๊ฐ์ง"
| ๊ตฌ๋ถ | ๋ฌธ์ ์ ์ (Problem) | ํด๊ฒฐ ๋ฐฉ์ (Solution) & ์ฑ๊ณผ |
|---|---|---|
| CQRS & ์ฑ๋ฅ ์ต์ ํ | ๊ฒ์๊ธ ์์ธ ์กฐํ ์ ๋ฐ์ํ๋ ๋ค์ค JOIN์ผ๋ก ์ธํ DB ๋ถํ์ ์๋ต ์๋ ์ ํ | CQRS ํจํด์ ๋์ ํ์ฌ ์กฐํ ์ ์ฉ ๋ชจ๋ธ์ Redis์ ๊ตฌ์ถ. DB ์กฐ์ธ ์์ด O(1) ์๊ฐ ๋ณต์ก๋๋ก ๋ฐ์ดํฐ๋ฅผ ์กฐํํ๋๋ก ์์คํ ์ค๊ณ. |
| ์ด๋ฒคํธ ๊ธฐ๋ฐ ์ํคํ ์ฒ | ์ค์๊ฐ ์ธ๊ธฐ๊ธ ์ง๊ณ ๋ก์ง์ด ์ฃผ์ ๋น์ฆ๋์ค์ ๊ฐํ๊ฒ ๊ฒฐํฉ๋์ด ํ์ฅ์ฑ์ด ๋จ์ด์ง๋ ๋ฌธ์ | Outbox ํจํด๊ณผ Kafka๋ฅผ ํ์ฉํด ์ด๋ฒคํธ ๋ฐํ์ ์ ํฉ์ฑ์ ๋ณด์ฅํ๊ณ ์์คํ ๊ฐ ๊ฒฐํฉ๋๋ฅผ ๋ฎ์ถค. Redis์ Sorted Set์ ์ด์ฉํด ์ธ๊ธฐ๊ธ ๋ญํน ์กฐํ ์ฑ๋ฅ ์ต์ ํ. |
| ๋์์ฑ ์ ์ด | ๋ค์์ ์ฌ์ฉ์๊ฐ ๋์์ '์ข์์' ์์ฒญ ์ ๋ฐ์ํ๋ ๊ฒฝ์ ์ํ(Race Condition)๋ก ์ธํ ๋ฐ์ดํฐ ์ ์ค | ๋น๊ด์ /๋๊ด์ ๋ฝ์ ์ง์ ์ ์ฉํ๊ณ , CountDownLatch ๊ธฐ๋ฐ์ ํ
์คํธ ์ฝ๋๋ก ์ฑ๋ฅ์ ๋น๊ต ๋ถ์ํ์ฌ ๊ฐ ๊ธฐ์ ์ ํธ๋ ์ด๋์คํ๋ฅผ ์ฒด๋. |
| API ์๋ต ์๋ ๊ฐ์ | ๋ฐ์ดํฐ ์ฆ๊ฐ์ ๋ฐ๋ฅธ ๊ฒ์๊ธ ๋ชฉ๋ก ์กฐํ API์ ์ฑ๋ฅ ์ ํ | ๋ถํ์ํ I/O๋ฅผ ์ ๊ฑฐํ๋ ์ปค๋ฒ๋ง ์ธ๋ฑ์ค ์ ์ฉ. ์คํ ์๊ฐ ์ฝ 69% ๊ฐ์ (21.97์ด โ 6.85์ด). |
| ์บ์ฑ ๋ฐ DB ๋ถํ ๋ถ์ฐ | ์กฐํ์ ์ฆ๊ฐ ์์ฒญ์ด DB์ ์ง์ ์ ์ธ ๋ถํ๋ฅผ ์ฃผ๋ ๋ฌธ์ | Redis ๋ถ์ฐ ๋ฝ์ผ๋ก ์ค๋ณต ์กฐํ๋ฅผ ๋ฐฉ์งํ๊ณ , 100ํ ๋จ์์ ๋ฐฐ์น ์ ๋ฐ์ดํธ๋ก DB ์ฐ๊ธฐ ๋ถํ๋ฅผ ์ต์ํ. |
ํ๋ก์ ํธ๋ฅผ ์งํํ๋ฉฐ ๋ง์ฃผํ ๊ธฐ์ ์ ๋ฌธ์ ์ ํด๊ฒฐ ๊ณผ์ , ๊ทธ๋ฆฌ๊ณ ์ค๊ณ์ ๋ํ ๊ณ ๋ฏผ์ ๋ธ๋ก๊ทธ์ ์์ธํ ๊ธฐ๋กํ๊ณ ์์ต๋๋ค.
-
์ํคํ ์ฒ ๋ฐ ์ฑ๋ฅ ์ต์ ํ
-
๋ฐ์ดํฐ ์ ํฉ์ฑ ๋ฐ ๋์์ฑ
-
์ด๋ฒคํธ ๊ธฐ๋ฐ ์์คํ ์ค๊ณ