๋ค์ํ ๊ฒ์ ์ ๋ณด์ ๋ฆฌ๋ทฐ๋ฅผ ๊ณต์ ํ๊ณ ๊ฒ์์ ๊ฐ์ด ํ ์น๊ตฌ๋ฅผ ์ฐพ์ ์ ์๋ ์ปค๋ฎค๋ํฐ ์ฌ์ดํธ
| ์ด์์ง | ์ ์์ฐ | ๊ณ ๊ฐํ | ์์ ํ |
|---|---|---|---|
| @yeji-world | @sumyeom | @Newbiekk | @89JHoon |
| BE | BE | BE | BE |
- Kakao์ google ํตํ ๊ฐํธ ๋ก๊ทธ์ธ์ด ๊ฐ๋ฅํฉ๋๋ค.
- ์ด๋ฉ์ผ ์ธ์ฆ ๊ธฐ๋ฅ์ ์ง์ํฉ๋๋ค.
- ํ์ฌ '๋ฆฌ๊ทธ์ค๋ธ๋ ์ ๋' ๋ผ๋ ๊ฒ์์ ๋ํด ๋งค์นญ ์์คํ ์ ํตํด ์น๊ตฌ๋ฅผ ๊ตฌํ ์ ์์ต๋๋ค.
- ๋ด ์ ๋ณด๋ฅผ ์ ๋ ฅํ๊ณ , ์ํ๋ ์๋๋ฐฉ์ ์กฐ๊ฑด์ ์ ๋ ฅํ์ฌ ๋งค์นญ ๋ก์ง์ ํตํด ์ต๋ 5์ธ์ ์ถ์ฒ์ ๋ฐ์ ์ ์์ต๋๋ค.
- ๋ค์ํ ๊ฒ์์ ๋ํ ์ ๋ณด๋ฅผ ์ป์ ์ ์๊ณ , ๊ฐ ๊ฒ์์ ๋ํด ์ฌ์ฉ์๋ค์ด ์์ฑํ ๋ฆฌ๋ทฐ๋ฅผ ํ์ธ ํ ์ ์์ต๋๋ค.
- ๋ด๊ฐ ์ํ๋ ์ฑํฅ์ ๊ฐ์ง ๊ฒ์์ ์ถ์ฒํ๋ ์๋น์ค๋ฅผ ์ ๊ณตํฉ๋๋ค.
- Gemini API ์ฌ์ฉํด ์ฌ์ฉ์๊ฐ ์ ๋ ฅํ ๋ด์ฉ์ ๊ธฐ๋ฐ์ผ๋ก ๊ฒ์์ ์ถ์ฒํด์ค๋๋ค.
- ๊ฒ์์ ๋ํ ๊ฒ์๊ธ์ ์์ฑํ๊ณ , ๊ฒ์๊ธ์ ๋๊ธ ๋๋๊ธ์ ์์ฑํ ์ ์์ต๋๋ค.
- ์กฐํ์ ์์ 5๊ฐ์ ๊ฒ์๊ธ์ ์ค๋์ ๊ฒ์๊ธ๋ก ์ ์ ํด ์ฌ์ฉ์์๊ฒ ์ ๊ณตํฉ๋๋ค.
- ์ฌ์ฉ์๊ฐ์ ํ๋ก์ฐ ๊ธฐ๋ฅ์ ์ ๊ณตํฉ๋๋ค.
- ๊ฒ์๋ฌผ / ๊ฒ์ ๋ฆฌ๋ทฐ์ '์ข์์' ๋๋ '์ซ์ด์'๋ฅผ ๋ฌ ์ ์์ต๋๋ค.
- ์๋ฆผ์ SSE๋ฅผ ํตํด ์ค์๊ฐ์ผ๋ก ์ ๊ณต๋ฉ๋๋ค.
- ๋งค์นญ / ํ๋ก์ฐ / ๋๊ธ / ์ข์์ ๋ฑ์ ์ด๋ฒคํธ์ ๋ํด ์๋ฆผ์ด ์ ๊ณต๋ฉ๋๋ค.
- ์์ฒด ์ฟ ํฐ์ ๋ฐ๊ธํด, ์ฌ์ฉ์๋ค์๊ฒ ์ฌ๋ฌ ํํ๋ค์ ์ ๊ณตํฉ๋๋ค.
| ๋ถ์ผ | ๊ธฐ์ ๋ฐ ๋๊ตฌ | ๋ชฉ์ |
|---|---|---|
| ์ ํ๋ฆฌ์ผ์ด์ ๊ฐ๋ฐ | JDK 17 Spring Boot 3.4.1 IntelliJ IDEA |
1๏ธโฃย JDK 17 - ์ฑ๋ฅ, ๋ณด์, ๊ฐ๋ฐ ํจ์จ์ฑ์ ์ํ ์์ ์ ์ธ ์ด์ ํ๊ฒฝ 2๏ธโฃย Spring Boot 3.4.1 - ํ๋ก๋์ ํ๊ฒฝ์์์ ์์ ์ฑ๊ณผ ํด๋ผ์ฐ๋ ๋ค์ดํฐ๋ธ ๊ธฐ๋ฅ ๊ฐํ 3๏ธโฃย IntelliJ IDEA - ๊ฐ๋ฐ์ ์์ฐ์ฑ์ ๊ทน๋ํํ๋ ๊ฐ๋ ฅํ ํตํฉ ๊ฐ๋ฐ ํ๊ฒฝ(IDE) |
| ์ธ์ฆ / ์ธ๊ฐ | Spring Security JWT OAuth 2.0 |
1๏ธโฃย Spring Security - ์น ์ ํ๋ฆฌ์ผ์ด์ ๋ณด์ ํตํฉ ๊ด๋ฆฌ 2๏ธโฃย JWT - ๋ฌด์ํ(Stateless) ์ธ์ฆ ๊ตฌํ 3๏ธโฃย OAuth 2.0 - ์ 3์ ์๋น์ค์ ์์ ํ ์์ ์ ๊ทผ ๊ด๋ฆฌ |
| ํ์ | Git GitHub Slack Notion GitHub Project / Issue Trello WBS |
1๏ธโฃย Git & GitHub - ์ฝ๋ ํ์ ๊ณผ ๋ฒ์ ๊ด๋ฆฌ 2๏ธโฃย Slack & Notion - ์งํ ์ฌํญ์ ๋ํด ์ํตํ๊ณ , ๋ฌธ์ํ 3๏ธโฃย GitHub Project / Issue & Trello & WBS - ์๊ตฌ ์ฌํญ์ ๋ํ ์ผ์ ๋ฐ ์ฐ์ ์์ ํ์ , ์ ๋ฆฌ - ๊ฐ ์ผ์ ์ ๋ํ ์งํ ์ฌํญ ์ฒดํฌ |
| Database | RDS(MySQL) Redis |
1๏ธโฃย RDS(MySQL) - RDS๋ฅผ ํตํด DB๋ฅผ ์ํ ์ธํ๋ผ ๊ตฌ์ถ - ์์ ์ ์ธ ๊ด๊ณํ DB๋ก ๋ฐ์ดํฐ ๊ด๋ฆฌ 2๏ธโฃย Redis - ๊ฒ์๊ธ ์กฐํ์ ์นด์ดํ ์ ์บ์๋ฅผ ํ์ฉํ์ฌ ํจ๊ณผ์ ์ผ๋ก ๊ด๋ฆฌ - Redisson ๊ธฐ๋ฐ ๋ถ์ฐ ๋ฝ์ ํตํด ๋ฐ์ดํฐ ์ผ๊ด์ฑ ์ ์ง - Redis Stream์ ์ฌ์ฉํด ๋ค์ค WAS ํ๊ฒฝ์์๋ ์๋ฆผ์ด ์ ์ค๋์ง ์๊ณ ์ ๋ฌ๋๋๋ก ๊ด๋ฆฌ |
| ํ์ผ ์ฒจ๋ถ | AWS S3 | 1๏ธโฃย AWS S3 - ๋์ฉ๋ ๋ฐ์ดํฐ๋ฅผ ์์ ์ ์ผ๋ก ์ ์ง ๊ด๋ฆฌ |
| CI/CD | Docker GitHub Action AWS EC2 AWS |
1๏ธโฃย Docker - ์ผ๊ด๋ ๊ฐ๋ฐ ํ๊ฒฝ ์ ๊ณต - ๋ฐฐํฌ ๋ฐ ํ์ฅ์ด ์ฉ์ด 2๏ธโฃย GitHub Action - CI/CD ์๋ํ๋ฅผ ๊ตฌํํ๊ณ , ๋ฐฐํฌ ํ๋ก์ธ์ค๋ฅผ ํจ์จ์ ์ผ๋ก ๊ด๋ฆฌ 3๏ธโฃย AWS - ํด๋ผ์ฐ๋ ํ๊ฒฝ์์ ์์ ์ ์ธ ์๋น์ค ์ด์ |
| ์ธ๋ถ API | Gemini API Google OAuth 2.0 API Kakao OAuth 2.0 API JavaMail |
1๏ธโฃย Gemini API - ์ ๋ขฐ์ฑ ์๋ ๊ฒ์ ์ถ์ฒ์ ์ํ API 2๏ธโฃย OAuth 2.0 API (Google & Kakao) - ์์ ๋ก๊ทธ์ธ์ ์ํ API 3๏ธโฃย JavaMail - ๋ฉ์ผ ๋ฐ์ก์ ์ํ API |
Gemini AI ํ์ฉ ๋ฐฉ์
- ๊ฒ์ ์ถ์ฒ ์์คํ ์ ๋จ์ํ ๊ฒ์ ์ ํ์ ๋์ด ์ฌ์ฉ์ ๊ฒฝํ ์ค๊ณ๋ถํฐ ์ค๋ฆฌ์ ๊ฒ์ฆ๊น์ง ์ข ํฉ์ ์ธ ์ ๊ทผ์ด ํ์ํจ.
- ํ๋ ์ด์ด์ ์ธ์ ๋ฐ์ดํฐ๋ฅผ ์ง์์ ์ผ๋ก ํ์ต์ ๋ฐ์ํ๋ฉด์๋, ์ถ์ฒ์ ๊ทผ๊ฑฐ๋ฅผ ํฌ๋ช ํ๊ฒ ์ ์ํ๋ ๊ฒ์ด ์ฅ๊ธฐ์ ์ ๋ขฐ ํ๋ณด์ ํต์ฌ.
- AI๋ฅผ ์ด์ฉํด ๊ฒ์์ ์ถ์ฒํ๋ ค๋ฉด ๋ค์ ์์๋ค์ ์ข
ํฉ์ ์ผ๋ก ๋ถ์ํด์ผ ํจ:
- ์ฌ์ฉ์ ์ทจํฅ (์ฅ๋ฅด, ํ๋ ์ด ์คํ์ผ)
- ๋ณด์ ๊ธฐ๊ธฐ (PC, ์ฝ์, ๋ชจ๋ฐ์ผ)
- ์ต์ ํธ๋ ๋
- ์์ ๋ฐ์ดํฐ (์น๊ตฌ, ์คํธ๋ฆฌ๋จธ ์ถ์ฒ)
- ์ถ์ฒ ์๊ณ ๋ฆฌ์ฆ (์ฝํ ์ธ ๊ธฐ๋ฐ, ํ์ ํํฐ๋ง)
- ํ ์ธ ์ ๋ณด, ์ฐ๋ น ๋ฑ๊ธ ๋ฑ์ ์์ ๊ณ ๋ ค
- ๋ํ, ์ฌ์ฉ์ ํผ๋๋ฐฑ์ ๋ฐ์ํ์ฌ ์ง์์ ์ผ๋ก ์ถ์ฒ ์์คํ ์ ๊ฐ์ ํ๋ ๊ฒ์ด ์ค์ํจ.
- ๊ตฌ๊ธ์ Gemini AI๋ ๋ฉํฐ๋ชจ๋ฌ ๊ธฐ๋ฅ(ํ ์คํธ, ์ด๋ฏธ์ง, ์์ฑ ๋ฑ)์ ์ง์ํ๋ฉฐ, ๊ฐ๋ ฅํ **์์ฐ์ด ์ฒ๋ฆฌ(NLP)**์ ๋ฐ์ดํฐ ๋ถ์ ๊ธฐ๋ฅ์ ์ ๊ณต.
- Gemini AI๋ฅผ ํ์ฉํ๋ฉด ๋จ์ํ ๊ฒ์ ์ถ์ฒ์ด ์๋๋ผ ์ฌ์ฉ์์ ๋ํํ๋ฉฐ ์ทจํฅ์ ํ์ ํ๊ณ ๋ง์ถคํ ์ถ์ฒ์ ์ ๊ณตํ๋ AI ์์คํ ์ ๊ตฌ์ถ ๊ฐ๋ฅ.
- ํนํ, Google Cloud์ API์ ๊ฒฐํฉํ๋ฉด ๋ ์ ๊ตํ ์ถ์ฒ ๋ชจ๋ธ์ ๊ตฌ์ถํ ์ ์์ด ์ง์์ ์ธ ๋ฐ์ ์ด ๊ธฐ๋๋จ.
- ๊ฒฐ๋ก ์ ์ผ๋ก, Gemini AI๋ฅผ ํ์ฉํ๋ฉด ๋์ฑ ์ ๋ฐํ๊ณ ๊ฐ์ธํ๋ ๊ฒ์ ์ถ์ฒ ์์คํ ๊ตฌ์ถ์ ๊ธฐ๋ํ ์ ์์.
| ๋์ | ์ฅ์ | ๋จ์ |
|---|---|---|
| Microsoft Azure OpenAI + Game Pass ๋ฐ์ดํฐ ์ฐ๊ณ | - Xbox Game Pass ๋ฐ์ดํฐ์ ์ฐ๊ณ ๊ฐ๋ฅ - ํด๋ผ์ฐ๋ ๊ธฐ๋ฐ ํ์ฅ์ฑ ์ฐ์ |
- Microsoft ์ํ๊ณ ๋ด์์ ํ์ฉ๋๊ฐ ๋์ - ๋ฒ์ฉ์ ์ธ ์ถ์ฒ ์์คํ ๊ตฌ์ถ์๋ ํ๊ณ |
| Claude (Anthropic) | - ๊ธด ๋ฌธ๋งฅ์ ์ ์งํ๋ ๋ํ ๋ฅ๋ ฅ ์ฐ์ - ์นํ๊ฒฝ์ ์ด๊ณ ์์ ํ AI ์ค๊ณ |
- ๊ฒ์ ์ถ์ฒ ์๊ณ ๋ฆฌ์ฆ ๊ตฌ์ถ์๋ ๋ค์ ํ๊ณ |
AWS S3 ํ์ฉ ๋ฐฉ์
- ์ปค๋ฎค๋ํฐ์์ ์ฒจ๋ถํ์ผ(์ด๋ฏธ์ง) ๊ด๋ฆฌ๋ ์ฌ๋ฌ ์ธก๋ฉด์์ ์ค์ํจ:
- ์ฌ์ฉ์ ๊ฒฝํ(UX) ํฅ์: ์ด๋ฏธ์ง๊ฐ ํฌํจ๋ ๊ฒ์๊ธ์ ๊ฐ๋ ์ฑ์ ๋์ด๊ณ ๋ชฐ์ ๊ฐ์ ์ ๊ณต.
- ์๋ฒ ์ฑ๋ฅ ์ต์ ํ: ์ ์ฅ ๊ณต๊ฐ๊ณผ ๋ก๋ฉ ์๋๋ฅผ ๊ณ ๋ คํ ์ต์ ํ ํ์.
- ๋ณด์ ๊ฐํ: ๋ถ๋ฒ ์ฝํ ์ธ ํํฐ๋ง ๋ฐ ์ ์ฑ ์ฝ๋ ๋ฐฉ์ง๋ฅผ ์ํ ๋ณด์ ์กฐ์น ํ์.
- ๊ฒ์ ์ต์ ํ: SEO ์ต์ ํ๋ฅผ ํตํด ์ ๊ทผ์ฑ์ ๋์ผ ์ ์์.
- ๋ชจ๋ฐ์ผ ๋์: ๋ค์ํ ๋คํธ์ํฌ ํ๊ฒฝ์์๋ ์ํํ ์ฌ์ฉ ๊ฐ๋ฅ.
- ํจ๊ณผ์ ์ธ ์ด๋ฏธ์ง ๊ด๋ฆฌ๋ฅผ ์ํด ์ด๋ฏธ์ง ์์ถ, AI ํํฐ๋ง, CDN ํ์ฉ ๋ฑ์ ์ ๋ต์ด ํ์ํ๋ฉฐ, ์ด๋ฅผ ์ ์ ์ฉํ์ฌ ์ํํ ์ปค๋ฎค๋ํฐ ์ด์์ ๋ชฉํ๋ก ํจ.
- ๋์ฉ๋ ์ด๋ฏธ์ง ์ ์ฅ ๊ฐ๋ฅ
- ๋น ๋ฅธ ์ด๋ฏธ์ง ๋ก๋ฉ ์๋
- ์ ์ธ๊ณ ์ด๋์์๋ ๋น ๋ฅด๊ฒ ๋ก๋ฉ ๊ฐ๋ฅ
- ๋ชจ๋ฐ์ผ ๋ฐ ์ ์ ์ธํฐ๋ท ํ๊ฒฝ์์๋ ์ํํ ์๋น์ค ์ ๊ณต
- ๋ฐ์ดํฐ ์์ค ์์ ๋ฐ ๋ณด์ ๊ฐํ
- ์๋ ๋ฐ์ดํฐ ๋ณต์ : ์ฌ๋ฌ ๋ฐ์ดํฐ ์ผํฐ์ ๋ณต์ ์ ์ฅํ์ฌ ๋ฐ์ดํฐ ์์ค ๋ฐฉ์ง
- ์ ๊ทผ ์ ์ด ๊ฐ๋ฅ: ํผ๋ธ๋ฆญ/ํ๋ผ์ด๋น ์ค์ ์ ํตํด ํน์ ์ฌ์ฉ์๋ง ์ ๊ทผ ๊ฐ๋ฅ
- ๋น์ฉ ์ ๊ฐ ํจ๊ณผ
- ์ฌ์ฉ๋ ๊ธฐ๋ฐ ๊ณผ๊ธ์ผ๋ก ๋ถํ์ํ ๋น์ฉ ์ ๊ฐ
- ๊ฒฐ๋ก
- S3๋ฅผ ์ฌ์ฉํ๋ฉด ์ด๋ฏธ์ง ์ ์ฅ์ด ํธ๋ฆฌํ๊ณ , ๋ก๋ฉ ์๋๊ฐ ๋น ๋ฅด๋ฉฐ, ๋ฐ์ดํฐ ์์ค ์ํ์ด ๋ฎ๊ณ , ๋ณด์์ด ๋ฐ์ด๋๋ฉฐ, ๋น์ฉ ์ ๊ฐ ๊ฐ๋ฅ
- ํนํ, ๋ง์ ์ฌ์ฉ์๊ฐ ์ด๋ฏธ์ง๋ฅผ ์ ๋ก๋ํ๊ณ ์กฐํํ๋ ์ปค๋ฎค๋ํฐ ๊ฒ์ํ์ ์ ์ ํ ์๋ฃจ์
| ๋์ | ์ฅ์ | ๋จ์ |
|---|---|---|
| Azure Blob Storage | - ๋ง์ดํฌ๋ก์ํํธ ํ๊ฒฝ๊ณผ ์ฐ๊ณ ์ฉ์ด | - AWS ๋๋น ํ์ฅ์ฑ๊ณผ ๊ธ๋ก๋ฒ ์ปค๋ฒ๋ฆฌ์ง ๋ถ์กฑ - ๊ด๋ฆฌ ์ฝ์์ด ๋ค์ ๋ณต์ก |
| Firebase Storage | - ๋ชจ๋ฐ์ผ ์ฑ๊ณผ์ ์ฐ๋ ์ต์ ํ | - ๋๋ ์ด๋ฏธ์ง ์ ์ฅ๋ณด๋ค๋ ๋ชจ๋ฐ์ผ ์ฑ ์ค์ฌ - ๋ฐ์ดํฐ ๊ด๋ฆฌ ๊ธฐ๋ฅ์ด ์ ํ์ |
SSE(Server-Sent Events) ๊ธฐ๋ฐ์ ์ค์๊ฐ ์๋ฆผ ์์คํ
- ์ด๊ธฐ ์๋ฆผ ๊ธฐ๋ฅ์ ์ค์ผ์ค๋ฌ๋ฅผ ํ์ฉํ์ฌ 5๋ถ๋ง๋ค ๋ฏธํ์ธ๋ ์๋ฆผ์ ์ด๋ฉ์ผ๋ก ์ ์กํ๋ ๋ฐฉ์์ผ๋ก ๊ตฌํ๋จ.
- ๊ทธ๋ฌ๋ ์ด๋ฌํ ๋ฐฉ์์ ์ค์๊ฐ์ฑ์ด ๋ถ์กฑํ์ฌ ์ฆ๊ฐ์ ์ธ ์๋ฆผ ์ ๊ณต์ด ๋ถ๊ฐ๋ฅํ์.
- ์ด๋ฅผ ๊ฐ์ ํ๊ธฐ ์ํด SSE(Server-Sent Events) ๊ธฐ๋ฐ์ ์ค์๊ฐ ์๋ฆผ ์์คํ ์ ๋์ ํ๊ฒ ๋จ.
- ์ค์๊ฐ ์๋ฆผ์ ๊ตฌํํ๊ธฐ ์ํด Short Polling, Long Polling, SSE, WebSocket ๋ฑ ๋ค์ํ ๊ธฐ์ ์ ๊ฒํ ํ ๊ฒฐ๊ณผ, SSE๊ฐ ๊ฐ์ฅ ์ ํฉํ๋ค๊ณ ํ๋จ๋จ.
- SSE(Server-Sent Events) ์ฅ์
โ ๋จ๋ฐฉํฅ(one-way) ์ฐ๊ฒฐ์ ํตํด ์๋ฒ์์ ํด๋ผ์ด์ธํธ๋ก ์ค์๊ฐ ์๋ฆผ ์ ์ก
โ ๊ธฐ์กด HTTP ํ๋กํ ์ฝ์ ํ์ฉํ๋ฏ๋ก ์ค์ ์ด ๊ฐํธ
โ WebSocket๊ณผ ๋ฌ๋ฆฌ ์ฌ์ฐ๊ฒฐ(์๋ ๋ณต๊ตฌ) ๊ธฐ๋ฅ ๋ด์ฅ
โ ์๋ฆผ๊ณผ ๊ฐ์ด ๋จ๋ฐฉํฅ ์ ์ก์ด ์ฃผ๋ฅผ ์ด๋ฃจ๋ ์๋น์ค์ ์ ํฉ
| ๊ธฐ์ | ๋์ ๋ฐฉ์ | ์ฅ์ | ๋จ์ |
|---|---|---|---|
| Short Polling | ํด๋ผ์ด์ธํธ๊ฐ ์ผ์ ์ฃผ๊ธฐ๋ง๋ค ์๋ฒ์ ์์ฒญ | ๊ตฌํ์ด ๊ฐ๋จํจ | ๋ถํ์ํ ์์ฒญ ์ฆ๊ฐ๋ก ๋ฆฌ์์ค ๋ญ๋น |
| Long Polling | ์๋ฒ๊ฐ ์๋ก์ด ๋ฐ์ดํฐ๊ฐ ์์ ๋๊น์ง ์๋ต์ ์ง์ฐ | ์ค์๊ฐ์ฑ ๊ฐ์ | ๋ค์์ ์ฐ๊ฒฐ ์ ์ง ์ ์๋ฒ ๋ถ๋ด ์ฆ๊ฐ |
| WebSocket | ํด๋ผ์ด์ธํธ-์๋ฒ ๊ฐ ์๋ฐฉํฅ ์ฐ๊ฒฐ ์ ์ง | ์๋ฐฉํฅ ํต์ ๊ฐ๋ฅ | ์ค์ ์ด ๋ณต์กํ๋ฉฐ, HTTP/2 ํ๊ฒฝ์์ ์ค๋ฒํค๋ ์ฆ๊ฐ ๊ฐ๋ฅ |
- ์๋ฆผ ์๋น์ค๋ ์๋ฒ์์ ํด๋ผ์ด์ธํธ๋ก ๋จ๋ฐฉํฅ ๋ฉ์์ง ์ ์ก์ด ์ฃผ๋ ์ญํ ์ ํจ.
- WebSocket์ ์๋ฐฉํฅ ์ฐ๊ฒฐ ๊ธฐ๋ฅ์ด ๋ถํ์ํ๋ฉฐ, ๊ตฌํ์ด ๊ฐ๋จํ SSE๊ฐ ์ต์ ์ ์ ํ์ด์์.
์๋ฆผ ์์คํ ์ Redis Stream ์ ์ฉ
- ๊ธฐ์กด SSE๋ง ์ฌ์ฉํ ๊ฒฝ์ฐ ๋คํธ์ํฌ ๋ฌธ์ ๋ ์๋ฒ ์ฅ์ ์ ์๋ฆผ์ด ์ ์ค๋ ์ ์์
- Redis Stream์ ์๋ฆผ ๋ฉ์์ง๋ฅผ ์ผ์์ ์ผ๋ก ์ ์ฅํ๊ณ ๊ด๋ฆฌํ์ฌ ๋ฉ์์ง ์ ๋ฌ์ ์ ๋ขฐ์ฑ์ ๋์
- ์ฌ์ฉ์์ ๋คํธ์ํฌ ์ฐ๊ฒฐ์ด ๋๊ธฐ๊ฑฐ๋ ์๋ฒ์ ๋ฌธ์ ๊ฐ ์๊ฒผ์ ๋๋ Stream์ ๋ฉ์์ง๊ฐ ๋ณด๊ด๋์ด ์์ด ์ฌ์ ์ ์ ๋ฏธ์ ์ก๋ ์๋ฆผ์ ์ฒ๋ฆฌํ ์ ์์
- ํฅํ ์์คํ ์ด ํ์ฅ๋์ด ๋ค์ค WAS ํ๊ฒฝ์์ ์๋ฆผ์ ์ฒ๋ฆฌํด์ผ ํ ๊ฒฝ์ฐ Redis Stream์ ํตํด ๋ฉ์์ง ํ ์ญํ ์ ์ํํ์ฌ ๋ถ์ฐ ์์คํ ์์๋ ์์ ์ ์ธ ์๋ฆผ ์ฒ๋ฆฌ๊ฐ ๊ฐ๋ฅ
| ๊ธฐ์ | ๋์ ๋ฐฉ์ | ์ฅ์ | ๋จ์ |
|---|---|---|---|
| Redis Pub/Sub | ๊ตฌ๋ ์๊ฐ ์์ผ๋ฉด ์ค์๊ฐ์ผ๋ก ๋ฉ์์ง ์ ์ก | ๋น ๋ฅธ ์ค์๊ฐ ๋ฉ์์ง | ๊ตฌ๋ ์๊ฐ ์์ผ๋ฉด ๋ฉ์์ง ์ ์ค |
| Kafka | ๋ก๊ทธ ๊ธฐ๋ฐ ์คํธ๋ฆฌ๋ฐ | ๊ฐ๋ ฅํ ๋ฉ์์ง ๋ณด์ฅ, ๋๋ ๋ฐ์ดํฐ ์ฒ๋ฆฌ ๊ฐ๋ฅ | ์ค์ ์ด ๋ณต์กํ๊ณ ์ด์ ๋น์ฉ์ด ๋์ |
| Redis Stream | ๋ฉ์์ง ์ ์ฅ + ์คํธ๋ฆฌ๋ฐ | ๋ฉ์์ง ์ ์ค ๋ฐฉ์ง, ์๋น์ ๊ทธ๋ฃน ์ง์ | Pub/Sub๋ณด๋ค ์ฝ๊ฐ์ ์ค์ ํ์ |
์๋ฆผ ์๋น์ค์์๋ ๋ฉ์์ง ์ ์ค ๋ฐฉ์ง๊ฐ ์ค์ํ๋ฏ๋ก, ๋จ์ Pub/Sub๋ณด๋ค Redis Stream์ด ์ ํฉํ์ต๋๋ค. Kafka๋ ๊ฐ๋ ฅํ ๊ธฐ๋ฅ์ ์ ๊ณตํ์ง๋ง, ์ด์ ๋ณต์ก์ฑ๊ณผ ๋น์ฉ ๋ฌธ์ ๋ฐ ๋๋ ๋ฐ์ดํฐ ์ฒ๋ฆฌ๊ฐ ํ์ฌ ๋ ๋ฒจ์์ ํ์ํ์ง ์์ ๊ฒ์ผ๋ก ์์๋์ด ์ค๋ฒ ์์ง๋์ด๋ง ๊ฐ์์ต๋๋ค. ์ด๋ฌํ ์ด์ ๋ค๋ก Redis Stream์ ์ ํํ์ต๋๋ค.
๊ฒ์์ถ์ฒ- ํ๋กฌํํธ ๊ด๋ จ ๋ณด์ ์ํ
๊ฒ์ ์ถ์ฒ ๊ธฐ๋ฅ์ค ์ฌ์ฉ์์๊ฒ ๋ฐ์ดํฐ๋ฅผ ์๋ต ๋ฐ๋ ์ค SQL ์ธ์ ์ ์ํ์ด ์์๋จ
-
๋ณด์ ์ํ ๋ถ์
String prompt = String.format("...์ถ๊ฐ์ ์ธ ์์ฒญ์ %s ์ผ", userGamePreference.getExtraRequest());
- ๋ฌธ์ ์ :ย **
extraRequest**๊ฐ ํ๋กฌํํธ์ ์ง์ ์ฝ์ ๋์ด ์ ์ฑ ์ฝ๋ ์คํ ๊ฐ๋ฅ - ์ํ: ํ๋กฌํํธ ์กฐ์์ ํตํ ์์คํ ๋ช ๋ น์ด ์ฃผ์ , ๋ฐ์ดํฐ ์ ์ถ ๋ฑ์ ๊ณต๊ฒฉ ๊ฐ๋ฅ์ฑ
- ๋ฌธ์ ์ :ย **
-
๊ฐ์ ๋ฐฉ์
-
OWASP ์ธ์ฝ๋ฉ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ ์ฉ
import org.owasp.encoder.Encode; String safeExtraRequest = Encode.forJava(extraRequest);
- ๊ธฐ๋ฅ: ํน์ ๋ฌธ์ ์๋ ์ด์ค์ผ์ดํ
- ์๋ฐฉ ๊ณต๊ฒฉ: XSS, ๋ช ๋ น์ด ์ฃผ์
-
Bean Validation ํตํฉ
public class UserGamePreferenceRequestDto { @Size(max = 100, message = "์ถ๊ฐ ์์ฒญ์ 100์ ์ด๋ด๋ก ์ ๋ ฅํด์ฃผ์ธ์") @Pattern(regexp = "^[a-zA-Z0-9\\s]+$", message = "ํน์ ๋ฌธ์๋ ์ฌ์ฉํ ์ ์์ต๋๋ค") private String extraRequest; } public class GameRecommendContorller { @PostMapping public ResponseEntity<UserGamePreferenceResponseDto> createUserGamePreference( @Valid @RequestBody UserGamePreferenceRequestDto requestDto, @AuthenticationPrincipal CustomUserDetails customUserDetails ) {
- ์ฅ์ : ์ ์ธ์ ๊ฒ์ฆ ๊ท์น ๊ด๋ฆฌ
-
-
๊ธฐ๋ ํจ๊ณผ
- ์ ๋ ฅ ๋ฐ์ดํฐ์ ๋ฌด๊ฒฐ์ฑ๊ณผ ์ ํจ์ฑ์ด ํฌ๊ฒ ํฅ์๋์ด, ์ ์์ ์ธ ๋ฐ์ดํฐ ์ฃผ์ ์๋๋ฅผ ์ฌ์ ์ ์ฐจ๋จํ ์ ์์
- SQL Injection๊ณผ ๊ฐ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๊ณต๊ฒฉ ์ํ์ด ํ์ ํ ๊ฐ์ํ์ฌ, ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋ณด์์ฑ์ด ํฅ์
- ์ฌ์ฉ์ ๊ฒฝํ ์ธก๋ฉด์์๋ ๊ฐ์ ์ด ์ด๋ฃจ์ด์ ธ, ์ ํจํ์ง ์์ ๋ฐ์ดํฐ ์ ๋ ฅ์ ๋ํ ์ฆ๊ฐ์ ์ธ ํผ๋๋ฐฑ์ ์ ๊ณตํจ์ผ๋ก์จ ์ฌ์ฉ์ ์นํ์ ์ธ ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํ ์ ์์
ํน์ ์ ์ ์ ํ๋ก์ ์กฐํ ์ ์ฑ๋ฅ ์ต์ ํ
public List<FollowFindResponseDto> findFollowers(String email) {
User followee = userRepository.findByEmail(email)
.orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND));
if (followee.getUserStatus() == UserStatus.WITHDRAW) {
throw new ApiException(ErrorCode.IS_WITHDRAWN_USER);
} // ํํดํ ํ์ ์์ธ ์ฒ๋ฆฌ
List<Follow> followListByFollowee = followRepository.findByFollowee(followee);
List<User> followersByFollowee = followListByFollowee.stream()
.map(Follow::getFollower)
.filter(follower -> follower.getUserStatus() != UserStatus.WITHDRAW)
.toList();
return followersByFollowee.stream()
.map(FollowFindResponseDto::toDto)
.toList();
}์ ์ฝ๋์ ๋ฌธ์ ์ ์ FetchType.LAZY๋ก ์ธํด N+1 ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ค๋ ๊ฒ์ ๋๋ค.
Follow์ํฐํฐ์์getFollower()๋ฅผ ํธ์ถํ ๋, ๊ฐ ํ๋ก์์ ๋ํ ๋ณ๋์ ์ฟผ๋ฆฌ๊ฐ ์คํ๋จ- ๊ฒฐ๊ณผ์ ์ผ๋ก ํ๋ก์๊ฐ 1,000๋ช ์ผ ๊ฒฝ์ฐ ์ด 1,002๊ฐ์ ์ฟผ๋ฆฌ๊ฐ ๋ฐ์ (1๊ฐ์ ์ฌ์ฉ์ ์กฐํ + 1๊ฐ์ ํ๋ก์ฐ ๋ฆฌ์คํธ ์กฐํ + 1,000๊ฐ์ ํ๋ก์ ์กฐํ)
์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด JPQL์ ์ฌ์ฉํ์ฌ ํ ๋ฒ์ ์ฟผ๋ฆฌ๋ก ํ์ํ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋๋ก ์์ ํ์์ต๋๋ค.
@Query("SELECT NEW com.example.gamemate.domain.follow.dto.FollowFindResponseDto(f.follower.id, f.follower.nickname) " +
"FROM Follow f " +
"JOIN f.follower " +
"WHERE f.followee.email = :email " +
"AND f.follower.userStatus != 'WITHDRAW'")
List<FollowFindResponseDto> findFollowersDtoByFolloweeEmail(@Param("email") String email);โ ์ต์ ํ๋ ์ฝ๋์ ์ฅ์
- ๋จ 2๊ฐ์ ์ฟผ๋ฆฌ๋ง ์คํ๋จ โ ๊ธฐ์กด 1,002๊ฐ โ 2๊ฐ
- N+1 ๋ฌธ์ ํด๊ฒฐ โ
JOIN์ ํตํด ํ ๋ฒ์ ๋ฐ์ดํฐ ์กฐํ - DTO๋ก ์ง์ ๋งคํ โ ์ํฐํฐ๋ฅผ ๋ถํ์ํ๊ฒ ์์ฑํ์ง ์๊ณ ๋ฐ๋ก DTO๋ก ๋ณํ
| ํ ์คํธ ํ๊ฒฝ | ๊ธฐ์กด ์ฝ๋ | ์ต์ ํ๋ ์ฝ๋ | ์ฑ๋ฅ ๊ฐ์ ์จ |
|---|---|---|---|
| ํ๋ก์ 1,000๋ช ๊ธฐ์ค | 752ms (1,002๊ฐ ์ฟผ๋ฆฌ) | 7ms (2๊ฐ ์ฟผ๋ฆฌ) | 99% ์ฑ๋ฅ ๊ฐ์ ๐ |
JPQL์ ํ์ฉํ ์ต์ ํ๋ฅผ ํตํด:
- N+1 ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ์ฌ ์คํ ์๊ฐ์ 752ms์์ 7ms๋ก 99% ๋จ์ถ
- ๋ถํ์ํ ์ฟผ๋ฆฌ๋ฅผ ์ ๊ฑฐํ์ฌ DB ๋ถํ๋ฅผ ๋ํญ ๊ฐ์ (1,002๊ฐ โ 2๊ฐ)
- DTO ์ง์ ๋งคํ์ผ๋ก ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ ์ต์ ํ
์ด๋ฌํ ์ฑ๋ฅ ๊ฐ์ ์ ํตํด ํ๋ก์๊ฐ ๋ง์ ์ฌ์ฉ์์ ํ๋กํ ์กฐํ์์๋ ๋น ๋ฅธ ์๋ต ์๋๋ฅผ ๋ณด์ฅํ ์ ์๊ฒ ๋์์ต๋๋ค.




