| ์ด๋ฆ | ์ญํ | GitHub ์ฃผ์ |
|---|---|---|
| ๊ตฌ๋์ | ๋ฆฌ๋ | https://github.com/GuDaeWoong |
| ์ต๊ฒฝ์ง | ๋ถ๋ฆฌ๋ | https://github.com/Che0807 |
| ์ฐ์๋น | ๋ฉค๋ฒ | https://github.com/saevit |
| ์คํฌ์ค | ๋ฉค๋ฒ | https://github.com/planbsoho |
| ์ดํธ์ค | ๋ฉค๋ฒ | https://github.com/Gaeso |
| ์ดํจ์ | ๋ฉค๋ฒ | https://github.com/hohohosn |
| ๐ ์๋น์ค ๊ฐ์ | ๐๏ธ ํต์ฌ ๊ธฐ๋ฅ | ๐๏ธ ์์คํ ์ํคํ ์ฒ |
| ๐ ์ค๊ณ ๋ฌธ์ | ๐ ๏ธ ๊ธฐ์ ์คํ | ๐ ์๋น์ค ํ๋ก์ฐ |
| ๐ก ์์ฌ๊ฒฐ์ ๋ฐ ๊ธฐ๋ฅ ๊ตฌํ | โก๏ธ ์ฑ๋ฅ ๊ฐ์ | ๐จ ํธ๋ฌ๋ธ ์ํ |
| ๐ ์ผ์ | ๐งญย ํฅํ ๊ฐ์ ๊ณํ |
์์ฆ ์ ๋๋ฉ์ด์ ์ '๋ฌธํ'์ ๋๋ค.
ํ์ง๋ง ์ ๋๋ฉ์ด์ ์ ์ฌ๋ํ๋ ๋ํ๋ค์ ์ฌ์ ํ ์ ๋ณด ์์ง, ์ํต, ๊ตฟ์ฆ ๊ฑฐ๋, ์ปค๋ฎค๋ํฐ
๋ฑ ์ผ์ ์ ๋ถํธํจ์ ๊ฒช๊ณ ์์ต๋๋ค.
๐ฌ ๋งค์ผ๋งค์ผ "์๋ก ๋์จ ์ฌ๋ฐ๋ ์ ๋ ๊ตฟ์ฆ ์ํ์ ์์๊น?" ๊ถ๊ธํ์ง๋ง,
์ฌ๋ฌ ์ฌ์ดํธ๋ฅผ ๋์๋ค๋๋ฉฐ ์ฐพ๊ธฐ์ ๋๋ฌด ๋ฒ๊ฑฐ๋กญ๊ณ ...
โป๏ธ ํ ๋ฒ ๋ณด๊ณ ๋ง๋ ์ํฌ๋ฆด ์คํ ๋, ์ ๋ง๋ ํผ๊ท์ด, ์ค๋ณต ๊ตฟ์ฆ๋ค...
์ค๊ณ ๊ฑฐ๋๋ ์ฌ๊ธฐ ๊ฑฑ์ ๋ ๋๊ณ ๋๋ฌด ๊ท์ฐฎ๊ณ ...
๐๏ธ ํ์ ํ ๊ตฟ์ฆ ๋์น๊ณ ์ถ์ง ์์๋ฐ
์ด๋์ ์ธ์ ๋์ค๋์ง ์ ๋ณด๊ฐ ๋ค ํฉ์ด์ ธ ์๊ณ ...
๐ฅ ์ง๊ธ ์ฌ๋๋ค์ด ๊ฐ์ฅ ๋ง์ด ์ฐพ๋ ๊ฑด ๋ญ๊น?
์ค์๊ฐ์ผ๋ก ํ์ธํ ๋ฐฉ๋ฒ์ด ์์ผ๋, ํธ๋ ๋๋ฅผ ๋์น๊ธฐ ์ผ์ค...
๐ค "์ด ๊ตฟ์ฆ ์ด๊น? ๋ง๊น?" ํผ์ ๊ณ ๋ฏผ๋ง ํ๋ค๊ฐ
๊ฒฐ๊ตญ ์ถฉ๋๊ตฌ๋งค๋ก ํํํ๊ฑฐ๋, ๋ง์ค์ด๋ค ๋์น๊ฑฐ๋...
์ด๋ฐ ๋ถํธํจ์ ํด๊ฒฐํ๊ธฐ ์ํด,
ํ๋ฃจ ํ ๋ฒ, ์ ๋๋ฉ์ด์ ๊ณผ ๋ ๊ฐ๊น์์ง๋ ํ๋ซํผ์ ๋ง๋ค์์ต๋๋ค.
๊ฑฐ๋์ ๊ฐ ์ฑํ ์์คํ
- ๊ตฟ์ฆ ๊ฑฐ๋ ์ ์ํ ์ํ ๋ฐ ๊ฐ๊ฒฉ ํ์
- ๊ฑฐ๋ ์กฐ๊ฑด ๋ฐ ๋ฐฐ์ก ๋ฐฉ๋ฒ ๋ ผ์
- ๊ฒ์ฆ๋ websocket๊ณผ stompํต์ ์ ํตํด ์์ ํ ๊ฑฐ๋ ํ๊ฒฝ ์ ๊ณต
- ์ํ ํ์ฅ์ ๊ณ ๋ คํ ๋ฉ์ธ์ง ๋ธ๋ก์ปค ์ค๊ณ๋ก ์์ ์ฑ ์ ๊ณต
๋ถ์ฐ๋ฝ์ ํตํ ์ค๋ณต ๊ฑฐ๋ ๋ฐฉ์ง
- ๊ฐ์ ์ํ์ ๋ํ ๋์ ๊ตฌ๋งค ์์ฒญ ๋ฐฉ์ง
- ์ ์ฐฉ์ ๊ฑฐ๋ ์์คํ ์ผ๋ก ๊ณต์ ํ ๊ฑฐ๋ ๋ณด์ฅ
- ๊ฒฐ์ ์งํ ์ค ๋ค๋ฅธ ์ฌ์ฉ์์ ์ ๊ทผ ์ฐจ๋จ
- ๊ฑฐ๋ ์ทจ์ ์ ์ฆ์ ๋ค๋ฅธ ์ฌ์ฉ์์๊ฒ ๊ธฐํ ์ ๊ณต
- ์๋ฒ ๊ณผ๋ถํ ์ํฉ์์๋ ์์ ์ ์ธ ๊ฑฐ๋ ์ฒ๋ฆฌ
- ๋ฐ์ดํฐ ์ ํฉ์ฑ ๋ณด์ฅ์ผ๋ก ๊ฑฐ๋ ์ค๋ฅ ์ต์ํ
- ํ์ ํ ๊ตฟ์ฆ ํ๋งค ์ ์์คํ ์์ ์ฑ ํ๋ณด
๊ด์ฌ์ฌ ํ๊ทธ ์์คํ
- ๊ฐ์ธ ๊ด์ฌ์ฌ ํ๊ทธ ์ค์ ๋ฐ ๊ด๋ฆฌ
- ํ๊ทธ ๊ธฐ๋ฐ ๋ง์ถค ์ํ ์ถ์ฒ
- ๊ตฟ์ฆ ๊ฒ์ ์ ๊ด์ฌ ํ๊ทธ ์ฐ์ ํ์
๊ตฌ๋งค ๊ณ ๋ฏผ ์๋ด ํ์ด์ง
- "์ด๊น๋ง๊น" ๊ณ ๋ฏผ ๊ฒ์ํ ์ด์
- ๋ค๋ฅธ ๋ํ๋ค์ ์์งํ ๊ตฌ๋งค ์กฐ์ธ
- ์ํ๋ณ ์ฅ๋จ์ ๋ฐ ํ๊ธฐ ๊ณต์
- ์ข์์ / ์ซ์ด์ : โ๊ตฌ๋งค ์ถ์ฒ ์ฌ๋ถโ ์ค์๊ฐ ์ง๊ณ
- ํฌํ ์ด์ : ์ฌ๋ฏธ ๋๋ ๊ตฌ๋งค๋ฅผ ๊ฐ๋ฑํ๋ ์ ์ ๋ฅผ ์ํ ์ข์์/์ซ์ด์
- ์ข์์ ์ซ์ด์ ์ด์ธ์๋ ์ํ ๊ตฌ๋งคํ๋ ๊ฒ์ ๋ํ ์ง์ฌ ์ด๋ฆฐ ์กฐ์ธ
์ต์ ์์ ๊ณต์ ํ์ด์ง
- "์์์" ๊ณต์ง ๊ฒ์ํ ์ด์
- ์ต์ ์๋ธ์ปฌ์ฒ ์ปจํ ์ธ , ๊ตฟ์ฆ, ์ด๋ฒคํธ ์์์ ์ ๊ณต
- ๊ด๊ณ ๊ฒ์๋ฅผ ํตํด ์์ต ์ฐฝ์ถ ๊ฐ๋ฅ
- Redis ์บ์๋ฅผ ํ์ฉํด ๋น ๋ฅธ ์๋ต ์๋์ ๋์ ์ฒ๋ฆฌ๋ ํ๋ณด
- TTL๊ณผ ๋ฌดํจํ ๋ก์ง์ ํจ๊ป ์ ์ฉํด ๋ฐ์ดํฐ ์ต์ ์ฑ ํ๋ณด
์ธ๊ธฐ ๊ฒ์์ด ๋ญํน
- ์ง๋ 24์๊ฐ ๋์ ๊ฐ์ฅ ๋ง์ด ๊ฒ์๋ ํค์๋๋ฅผ ์ค์๊ฐ์ผ๋ก ์ง๊ณ
- ์ต์ ํธ๋ ๋๋ฅผ ํ๋์ ํ์ ํ ์ ์๋ ํ์ ๊ฒฝํ ์ ๊ณต
- ๊ด์ฌ์ฌ ๊ธฐ๋ฐ์ผ๋ก ์๋ก์ด ์ํ์ ๋ฐ๊ฒฌํ๊ณ ๊ตฌ๋งค๋ก ์ฐ๊ฒฐ
- ๊ฒ์๋ ๋ฐ์ดํฐ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์์ ๋์ ์ํ์ ์์ธก
์ด์ ๋ฐ ๋ชจ๋ํฐ๋ง
- ์ฃผ์ ๋น์ฆ๋์ค ๋ก์ง์ ์คํ ํ๋ฆ๊ณผ ์์ธ๋ฅผ ์๋์ผ๋ก ๋ก๊น
- ์๋ฌ ์ถ์ ๋ฐ ๋๋ฒ๊น ํจ์จ์ฑ ํฅ์
- ์ ํ๋ฆฌ์ผ์ด์ ์ฑ๋ฅ ๋ชจ๋ํฐ๋ง(APM)์ ํตํ ํธ๋์ญ์ ์ถ์
- ์ฅ์ ๋ฐ์ ์ ์์ธ ๋ถ์ ๋ฐ ์ฑ๋ฅ ๋ณ๋ชฉ ์ง์ ํ์ ๊ฐ๋ฅ
๐ฆ Mania-Place
โโโ ๐ common # ๊ณตํต ๋ชจ๋, ์ ์ฒด ํ๋ก์ ํธ์์ ์ฌ์ฌ์ฉ๋๋ ์ฝ๋
โ โโโ ๐ annotation # ์ปค์คํ
์ด๋
ธํ
์ด์
โ โโโ ๐ aop # AOP ๊ด๋ จ ๋ก์ง
โ โโโ ๐ config # ๊ณตํต ์ค์
โ โโโ ๐ dto # ๊ณตํต DTO
โ โโโ ๐ entity # ๊ณตํต ์ํฐํฐ
โ โโโ ๐ exception # ์์ธ ์ฒ๋ฆฌ
โ โ โโโ ๐ enums # ์์ธ ๊ด๋ จ ์ด๊ฑฐํ
โ โ โโโ ๐ exceptionclass # ์ปค์คํ
์์ธ ํด๋์ค
โ โโโ ๐ filter # ํํฐ ์ฒ๋ฆฌ
โ โโโ ๐ health # ์ํ ์ฒดํฌ
โ โโโ ๐ security # ๋ณด์ ๊ด๋ จ
โ โโโ ๐ jwt # JWT ์ธ์ฆ ์ฒ๋ฆฌ
โโโ ๐ domain # ๋๋ฉ์ธ๋ณ ๋ชจ๋
โโโ ๐ auth # ์ธ์ฆ/์ธ๊ฐ
โ โโโ ๐ controller # API ์ปจํธ๋กค๋ฌ
โ โ โโโ ๐ dto # ์์ฒญ/์๋ต DTO
โ โโโ ๐ domain # ๋๋ฉ์ธ ๋ ์ด์ด
โ โ โโโ ๐ model # ๋๋ฉ์ธ ๋ชจ๋ธ
โ โ โโโ ๐ repository # DB ์ ๊ทผ
โ โโโ ๐ service # ์๋น์ค ๋ก์ง
โโโ ๐ chatmessage # ์ฑํ
๋ฉ์์ง
โ โโโ ๐ controller
โ โโโ ๐ dto
โ โ โโโ ๐ request
โ โโโ ๐ entity
โ โโโ ๐ repository
โ โโโ ๐ service
โโโ ๐ chatroom # ์ฑํ
๋ฐฉ
โ โโโ ๐ controller
โ โโโ ๐ dto
โ โ โโโ ๐ request
โ โ โโโ ๐ response
โ โโโ ๐ entity
โ โโโ ๐ repository
โ โโโ ๐ service
โโโ ๐ Image # ์ด๋ฏธ์ง
โ โโโ ๐ dto
โ โโโ ๐ entity
โ โโโ ๐ repository
โ โโโ ๐ service
โโโ ๐ item # ์ํ
โ โโโ ๐ controller
โ โโโ ๐ dto
โ โ โโโ ๐ request
โ โ โโโ ๐ response
โ โโโ ๐ entity
โ โโโ ๐ repository
โ โโโ ๐ service
โโโ ๐ itemcomment # ์ํ ๋๊ธ
โ โโโ ๐ controller
โ โโโ ๐ dto
โ โ โโโ ๐ request
โ โ โโโ ๐ response
โ โโโ ๐ entity
โ โโโ ๐ repository
โ โโโ ๐ service
โโโ ๐ itemtag # ์ํ ํ๊ทธ
โ โโโ ๐ entity
โ โโโ ๐ repository
โโโ ๐ keyword # ํค์๋
โ โโโ ๐ controller
โ โโโ ๐ domain
โ โ โโโ ๐ model
โ โ โโโ ๐ repository
โ โโโ ๐ service
โ โโโ ๐ dto
โโโ ๐ newsfeed # ๋ด์คํผ๋
โ โโโ ๐ controller
โ โโโ ๐ dto
โ โ โโโ ๐ request
โ โ โโโ ๐ response
โ โโโ ๐ entity
โ โโโ ๐ repository
โ โโโ ๐ service
โโโ ๐ newsfeedcomment # ๋ด์คํผ๋ ๋๊ธ
โ โโโ ๐ controller
โ โโโ ๐ dto
โ โ โโโ ๐ request
โ โ โโโ ๐ response
โ โโโ ๐ entity
โ โโโ ๐ repository
โ โโโ ๐ service
โโโ ๐ order # ์ฃผ๋ฌธ
โ โโโ ๐ controller
โ โโโ ๐ dto
โ โ โโโ ๐ request
โ โ โโโ ๐ response
โ โโโ ๐ entity
โ โโโ ๐ repository
โ โโโ ๐ service
โโโ ๐ post # ๊ฒ์๊ธ
โ โโโ ๐ controller
โ โโโ ๐ dto
โ โ โโโ ๐ request
โ โ โโโ ๐ response
โ โโโ ๐ entity
โ โโโ ๐ repository
โ โโโ ๐ service
โโโ ๐ postcomment # ๊ฒ์๊ธ ๋๊ธ
โ โโโ ๐ controller
โ โโโ ๐ dto
โ โ โโโ ๐ request
โ โ โโโ ๐ response
โ โโโ ๐ entity
โ โโโ ๐ repository
โ โโโ ๐ service
โโโ ๐ tag # ํ๊ทธ
โ โโโ ๐ controller
โ โโโ ๐ dto
โ โ โโโ ๐ request
โ โ โโโ ๐ response
โ โโโ ๐ entity
โ โโโ ๐ repository
โ โโโ ๐ service
โ โโโ ๐ util
โโโ ๐ user # ์ฌ์ฉ์
โ โโโ ๐ controller
โ โโโ ๐ dto
โ โโโ ๐ entity
โ โโโ ๐ repository
โ โโโ ๐ service
โโโ ๐ usertag # ์ฌ์ฉ์ ํ๊ทธ
โ โโโ ๐ entity
โ โโโ ๐ repository
โโโ ๐ vote # ํฌํ
โโโ ๐ controller
โโโ ๐ dto
โ โโโ ๐ request
โ โโโ ๐ response
โโโ ๐ entity
โโโ ๐ repository
โโโ ๐ service
| ๊ตฌ๋ถ | ์ฌ์ฉ ๊ธฐ์ |
|---|---|
| Back-end | Java 17, Spring Framework, Spring Boot, Spring Data JPA, Spring Security, JWT, Query DSL, Websocket, STOMP, Jackson |
| Productivity Tools | Lombok, Gradle |
| Database | MySQL, Redis |
| Infra & CI/CD | Docker, RabbitMQ, Amazon EC2, Amazon SES, GitHub Actions |
| Test | Postman, JMeter |
| Monitoring | PinPoint |
| Tools | IntelliJ IDEA |
| Collaboration | GitHub, Notion, Slack, ERD cloud, draw.io |
๐ก JWT&Spring Security
Mania Place ์๋น์ค์์๋ ๋ก๊ทธ์ธ๊ณผ ๊ถํ ๊ด๋ฆฌ๊ฐ ํ์ํ์ต๋๋ค.
์ธ์ ๊ธฐ๋ฐ ์ธ์ฆ์ ๊ณ ๋ คํ์ผ๋, ํธ๋ํฝ์ด ๋ชฐ๋ฆด ๋ ์๋ฒ ๋ถ๋ด์ด ํฌ๊ณ ์ฌ๋ฌ ์๋ฒ๋ฅผ ํ์ฅํ ๋ ์ธ์ ๊ณต์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์์์ต๋๋ค.
์ด ๋ฌธ์ ๋๋ฌธ์ JWT์ Spring Security๋ฅผ ๋์ ํ๊ฒ ๋์์ต๋๋ค.
- ์ฌ์ฉ์๊ฐ ๋ก๊ทธ์ธ ํ ์์ ํ๊ฒ ์ธ์ฆ ์ํ๋ฅผ ์ ์งํ ๊ฒ
- ์ผ๋ฐ ์ฌ์ฉ์ / ๊ด๋ฆฌ์ ๊ถํ์ ๊ตฌ๋ถํด ์ ๊ทผ์ ์ ์ดํ ๊ฒ
- ์๋ฒ๊ฐ ์ํ๋ฅผ ์ง์ ๋ค๊ณ ์์ง ์์๋ ํ์ฅ์ด ๊ฐ๋ฅํ ๊ฒ
- ๋ณด์์ฑ์ ํด์น์ง ์์ผ๋ฉด์๋ ์ฌ์ฉ์๊ฐ ๋ถํธํ์ง ์๊ฒ ๊ตฌํํ ๊ฒ
- ์ธ์ ๊ธฐ๋ฐ ์ธ์ฆ์ ์๋ฒ ๋ถํ์ ํ์ฅ์ฑ ๋ฌธ์ ๋ก ์ ์ธ
- OAuth2 / OIDC๋ ๊ธฐ๋ฅ์ ํ๋ถํ์ง๋ง ํ์ฌ ํ๋ก์ ํธ ๋ฒ์์๋ ์ค๋ฒ์คํ์ด๋ผ ํ๋จ
- JWT๋ ๋ฌด์ํ(stateless)๋ก ์๋ฒ ๋ถ๋ด์ ์ค์ด๊ณ , ๊ถํ ์ ๋ณด๋ ํจ๊ป ๋ด์ ์ ์์ด ์ ํฉํ๋ค๊ณ ํ๋จ
- JWT + Spring Security ์กฐํฉ์ผ๋ก ๊ฒฐ์
- ๋ก๊ทธ์ธ ์ฑ๊ณต ์ ์ก์ธ์ค ํ ํฐ๊ณผ ๋ฆฌํ๋ ์ ํ ํฐ์ ๋ฐ๊ธ
- ์ก์ธ์ค ํ ํฐ: ์ ํจ๊ธฐ๊ฐ ์งง๊ฒ ์ค์ (๋ณด์ ๊ฐํ)
- ๋ฆฌํ๋ ์ ํ ํฐ: ์ ํ ํฐ์ ๋ฐ๊ธ๋ฐ์ ๋ ์ฌ์ฉ
- Spring Security์ ํํฐ ์ฒด์ธ์ ์ค์ ํด
- JWT ํ ํฐ ๊ฒ์ฆ ๊ณผ์ ์์ ์ฌ์ฉ์ ๊ถํ(
ROLE_USER,ROLE_ADMIN)์ ํ์ธํด ๊ธฐ๋ฅ๋ณ ์ ๊ทผ ์ ์ด - ๋ก๊ทธ์์ ์ ํ ํฐ ๋ธ๋๋ฆฌ์คํธ ์ฒ๋ฆฌ๋ฅผ ํตํด ํ ํฐ ์ฌ ์ฌ์ฉ์ ๋ฐฉ์ง
- ํ ํฐ ๊ฐฑ์ ๊ณผ์ ์์ ๋ฆฌํ๋ ์ ํ ํฐ ์ฌ์ฌ์ฉ ๋ฐฉ์ง ๋ก์ง์ ๊ฐํํ ํ์ ์์
- ์ถํ ์ธ๋ถ ์๋น์ค ์ฐ๋ ์ OAuth2 / OIDC๋ก ํ์ฅ ๊ฐ๋ฅ์ฑ ๊ณ ๋ ค
๐ก Query DSL์ ํตํ ์ํ ๊ฒ์
์ํ ์ ์ฒด ์กฐํ ์ ์ฐ๊ด๋ ์ด๋ฏธ์ง์ ํ๊ทธ๋ฅผ N+1 ๋ฌธ์ ์์ด fetch join์ผ๋ก ๊ฐ์ ธ์ค๋ฉด์, ๋์์ Pageable๋ก ํ์ด์ง ์ฒ๋ฆฌํ์ต๋๋ค.
๊ทธ๋ฌ๋ ์ํ๊ณผ ์ด๋ฏธ์ง๊ฐ @OneToMany ๊ด๊ณ๋ก:
- ์กฐ์ธ ๊ฒฐ๊ณผ๊ฐ ๊ณฑํด์ ธ ์ค๋ณต row ๋ฐ์ โ JSON ์๋ต์ ๋ถํ์ํ ์ค๋ณต ๊ฐ ํฌํจ
- DB ๋ ๋ฒจ์์ LIMIT, OFFSET ์ ์ฉ ๋ถ๊ฐ โ Hibernate๊ฐ Java ๋ฉ๋ชจ๋ฆฌ์์ ์ค๋ณต ์ ๊ฑฐ
- ๋์ฉ๋ ๋ฐ์ดํฐ ์ ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ ์ฆ๊ฐ, ์ฑ๋ฅ ์ ํ ์ฐ๋ ค
๋ฑ์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ต๋๋ค.
- ์ฐ๊ด ์ํฐํฐ ์ค๋ณต ์์ด ์กฐํ
- DB ๋ ๋ฒจ์์ ํ์ด์ง ์ ์ฉ
- N+1 ๋ฌธ์ ๋ฐฉ์ง
- ๋์ฉ๋ ๋ฐ์ดํฐ์์๋ ์ฑ๋ฅ ๋ฌธ์ ๋ฐฉ์ง
๋จ๊ณ๋ณ๋ก ๋ถ๋ฆฌํ์ฌ ์กฐํ
- ์กฐ๊ฑด์ ๋ง๋ Item์ ID๋ง ๋จผ์ ํ์ด์ง ์ฒ๋ฆฌํ์ฌ ์กฐํ
- ํด๋น ID ๋ฆฌ์คํธ ๊ธฐ์ค์ผ๋ก ์ฐ๊ด๋ Tag, Image๋ฅผ
fetch joinํ์ฌ ์กฐํ - ๋ณ๋ count ์ฟผ๋ฆฌ๋ฅผ ์์ฑํ์ฌ ์ ์ฒด ํ์ด์ง ์ ๊ณ์ฐ
- ์ต์ข
์ ์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ์กฐ๋ฆฝํ์ฌ
Page<ItemDto>ํํ๋ก ๋ฐํ
๋จ๊ณ๋ณ๋ก ๋ถ๋ฆฌํ์ฌ ์กฐํํ๋ ์ด์
- ์ค๋ณต row ๋ฐฉ์ง: fetch join์ผ๋ก ์ฌ๋ฌ ์ปฌ๋ ์ ์ ํ ๋ฒ์ ์กฐํํ๋ฉด (Item ร Tag ร Image)์ฒ๋ผ ๊ณฑํด์ ธ ์ค๋ณต ๋ฐ์
- DB ๋ ๋ฒจ ํ์ด์ง ์ ์ฉ: ID๋ง ๋จผ์ ์กฐํํ๋ฉด LIMIT, OFFSET์ด ์ ํํ ๋์
- ์ฑ๋ฅ ์ต์ ํ: ํ์ด์ง์ ํ์ํ Item์ ๋ํด์๋ง ์กฐํํ๋ฏ๋ก, ๋์ฉ๋ ๋ฐ์ดํฐ์์๋ ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋๊ณผ ์ฒ๋ฆฌ ์๋๊ฐ ์ด๊ธฐ ๋ฒ์ ๋๋น ํจ์จ์
- ID ๊ธฐ๋ฐ ์กฐํ ์ฅ์ : IN์ ์กฐํ๋ ๊ธฐ๋ณธ ์ํฐํฐ ์ค์ฌ ์ฟผ๋ฆฌ๋ผ JPA๊ฐ ๊ฐ์ Item ID๋ฅผ ๊ธฐ์ค์ผ๋ก ํ๋์ ๊ฐ์ฒด๋ก ๋ฌถ์ด ์ฐ๊ด ์ปฌ๋ ์ ์ ์ถ๊ฐ โ row ์ ์ฆ๊ฐ ์์ด ์์ ํ๊ฒ ์กฐํ ๊ฐ๋ฅ
QueryDSL์ ๋์ ํ ์ด์
- ํ์ด์ง, ๋์ where์ , ์ ๋ ฌ ๋ฑ ๋ณต์กํ ์ฌ๋ฌ ์ฟผ๋ฆฌ๋ค์ ํ๋์ ๋ฉ์๋์ ์์ง์ํฌ ์ ์์
- ๋ฆฌํฌ์งํ ๋ฆฌ ๊ณ์ธต์์ ์กฐํ ์ฑ ์์ ๋ช ํํ๊ฒ ๊ด๋ฆฌํ ์ ์์ด, ์ ์ง๋ณด์์ฑ๊ณผ ํ์ฅ์ฑ ์ธก๋ฉด์์๋ ์ ๋ฆฌํ๋ค๊ณ ํ๋จ
ItemRepositoryCustomImpl
@RequiredArgsConstructor
public class ItemRepositoryCustomImpl implements ItemRepositoryCustom {
private final JPAQueryFactory queryFactory;
@Override
public Page<Item> search(String keyword, List<String> tags, Long userId, Pageable pageable) {
// ์ํ ์กฐํ๋ฅผ ์ํ where์
BooleanExpression whereCondition = null;
// ํค์๋๋ฅผ ์ ๋ฌ ๋ฐ์๋ค๋ฉด ํด๋น ํค์๋๊ฐ ์ ๋ชฉor์ค๋ช
์ ํฌํจ๋ ์ํ์ ์กฐํ
if (keyword != null) {
BooleanExpression keywordCondition = item.itemName.likeIgnoreCase("%" + keyword + "%")
.or(item.itemDescription.likeIgnoreCase("%" + keyword + "%"));
whereCondition = keywordCondition;
}
// ํ๊ทธ๋ฅผ ์ ๋ฌ ๋ฐ์๋ค๋ฉด ํด๋น ํ๊ทธ๊ฐ ์กด์ฌํ๋ ์ํ์ ์กฐํ
if (tags != null && !tags.isEmpty()) {
BooleanExpression tagCondition = tag.tagName.in(tags);
whereCondition = (whereCondition == null) ? tagCondition : whereCondition.or(tagCondition);
}
// ์ ์ id๋ฅผ ์ ๋ฌ ๋ฐ์๋ค๋ฉด ํด๋น ํ๋งค์๊ฐ ํด๋น id์ ์ผ์นํ๋ ์ํ์ ์กฐํ
if (userId != null) {
BooleanExpression userCondition = item.user.id.eq(userId);
whereCondition = (whereCondition == null) ? userCondition : whereCondition.and(userCondition);
}
return buildPagedItem(whereCondition, pageable);
}
private Page<Item> buildPagedItem(BooleanExpression whereCondition, Pageable pageable) {
// ์ ๋ ฌ ์กฐ๊ฑด
OrderSpecifier<?> orderSpecifier = getOrderSpecifiers(pageable);
// ํ์ด์ง์ id ์กฐํ
List<Long> itemIds = queryFactory
.select(item.id)
.from(item)
.join(item.itemTags, itemTag)
.join(itemTag.tag, tag)
.where(
item.isDeleted.isFalse(),
whereCondition)
.orderBy(orderSpecifier)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
if (itemIds.isEmpty()) {
return new PageImpl<>(Collections.emptyList(), pageable, 0);
}
// ํ์ด์ง์ ์ํ๋ค ์ ๋ณด ์กฐํ
List<Item> items = queryFactory
.selectFrom(item)
.distinct()
.join(item.user).fetchJoin()
.join(item.itemTags, itemTag).fetchJoin()
.join(itemTag.tag, tag).fetchJoin()
.join(item.images).fetchJoin()
.where(item.id.in(itemIds))
.orderBy(orderSpecifier)
.fetch();
// ํ์ด์ง ์ฒ๋ฆฌ๋ฅผ ์ํ ์ ์ฒด ์ด ์นด์ดํธ
long total = Optional.ofNullable(
queryFactory
.select(item.countDistinct()) // fetchJoin ์์ด count ํ๊ธฐ์ํด distinct
.from(item)
.join(item.itemTags, itemTag)
.join(itemTag.tag, tag)
.where(
item.isDeleted.isFalse(),
whereCondition)
.fetchOne()
).orElse(0L);
return new PageImpl<>(items, pageable, total);
}
private OrderSpecifier<?> getOrderSpecifiers(Pageable pageable) {
// Query DSL ํ๋ ์ ๊ทผ ๊ฒฝ๋ก(path) ์ง์ ๋๊ตฌ
PathBuilder<Item> path = new PathBuilder<>(Item.class, "item");
// ํํฐ๋งํด์ createdAt ์ ๋ ฌ๋ง ๋ฐ๊ณ , ์์ผ๋ฉด ๊ธฐ๋ณธ๊ฐ(์ต์ ์) ์ธํ
Sort.Order firstOrder = pageable.getSort().stream()
.filter(order -> "createdAt".equals(order.getProperty()))
.findFirst()
.orElse(new Sort.Order(Sort.Direction.DESC, "createdAt"));
// pageable์ ์ ๋ ฌ ๊ฐ์ Query DSL์ ์ ์ฉ๊ฐ๋ฅํ ํํค ๋ณํ ("createdAt,desc" -> item.createdAt.desc())
return new OrderSpecifier<>(
firstOrder.isAscending() ? Order.ASC : Order.DESC, // .asc()/.desc() ํ๋ณ
path.getComparable(firstOrder.getProperty(), Comparable.class)); // "createdAt" -> item.createdAt
}
}
ItemService
@Slf4j
@Service
@RequiredArgsConstructor
public class ItemService {
private final ItemRepository itemRepository;
@Loggable
@Transactional
public PageResponseDto<ItemSummaryResponse> searchItems(String keyword, List<String> tags, Long userId,
Pageable pageable) {
Page<Item> pagedItems = itemRepository.search(keyword, tags, userId, pageable);
Page<ItemSummaryResponse> response = pagedItems.map(ItemSummaryResponse::from);
return new PageResponseDto<>(response);
}
}
ํ์ฌ ๊ฐ์
- ์ ํํ ํ์ด์ง + ์ฐ๊ด ์ ๋ณด ํ๋ฒ์ ์กฐํ
- fetch join์ผ๋ก N+1 ๋ฌธ์ ํด๊ฒฐ
- ๋์ ์กฐ๊ฑด ๋ฐ ์ ๋ ฌ ๋ฑ ์ฟผ๋ฆฌ ์ต์ ํ ์ ๋ฆฌ
์ ์ฝ ์ฌํญ
- ์ฟผ๋ฆฌ 3๋ฒ ํ์
- ์ ๋ ฌ ์ ์ง๋ ์ค๋ณต ์ ๊ฑฐ ๋ฑ ์ฟผ๋ฆฌ ์์ฑ ์ ์ฃผ์ ํ์
- ์ฑ ์ ๋ถ๋ฆฌ ์ค๊ณ๊ฐ ์ด๋ ค์
๐ก ์ํ ๊ฒ์๊ณผ ์ด๋ฏธ์ง ์กฐํ ๋ก์ง ๋ถ๋ฆฌ
QueryDSL์ ์ฌ์ฉํ์ฌ ์ํ ์กฐํ ์ ์ฐ๊ด ์ ๋ณด๋ฅผ ํ ๋ฒ์ ์กฐํํ๋ฉด์ ํ์ด์ง์ด ์๋๋๋ก ์๋ํ๋๋ก ๊ตฌํํ์ต๋๋ค.
๊ทธ๋ฌ๋, ์ํ์ ์ข ์๋ ํ๊ทธ์ ๋ฌ๋ฆฌ ์ด๋ฏธ์ง๋ ์ํ๋ฟ ์๋๋ผ ์ฌ๋ฌ ๋๋ฉ์ธ์์ ์ฌ์ฌ์ฉ๋ ์ ์์ด, ์ด๋ฏธ์ง๋ฅผ ์ํ๊ณผ ํ ๊ฐ์ฒด๋ก ๋ณผ ์ ์์์ง ๊ณ ๋ฏผ์ด ์์์ต๋๋ค.
- ์ฌ๋ฌ ๊ฐ์ฒด ๊ฐ์ ๋ก์ง ๋ถ๋ฆฌํ์ฌ ์ฑ ์ ๋ช ํํ
- ์ฐ๊ด ์ํฐํฐ ์ค๋ณต ์์ด ์กฐํ
- DB ๋ ๋ฒจ์์ ํ์ด์ง ์ ์ฉ
- N+1 ๋ฌธ์ ๋ฐฉ์ง
- ๋์ฉ๋ ๋ฐ์ดํฐ์์๋ ์ฑ๋ฅ ๋ฌธ์ ๋ฐฉ์ง
์ด๋ฏธ์ง ์กฐํ ๋ก์ง ๋ถ๋ฆฌ
- ์ํ ์ํฐํฐ ํ์ด์ง์ผ๋ก ์กฐํ
- ํด๋น ํ์ด์ง์ ์ํ ID ๋ฆฌ์คํธ ์ถ์ถ
- ์ฐ๊ด ์ ๋ณด(์ด๋ฏธ์ง)๋ฅผ ID ๋ฆฌ์คํธ ๊ธฐ๋ฐ์ผ๋ก ๋ณ๋ ์กฐํํ์ฌ Map์ผ๋ก ๊ทธ๋ฃนํ
- ๋ฐํ๋ ์ฐ๊ด ์ ๋ณด์ ํจ๊ป ์๋ต๊ฐ ์์ฑ
์ด๋ฏธ์ง ๋ก์ง์ ๋ถ๋ฆฌํ ์ด์
- ํ ๋ฒ์ ์ด๋ฏธ์ง๋ฅผ ์กฐํํ๋ฉด ์ฟผ๋ฆฌ ์๊ฐ ์ ๊ณ ์กฐํ ์๋๊ฐ ๋น ๋ฅด๋ค๋ ์ฅ์ ์ ์ธ์ง
- ๊ทธ๋ฌ๋ ์ด๋ฏธ์ง๋ ์ฌ๋ฌ ๋๋ฉ์ธ์์ ์ฌ์ฌ์ฉ ๊ฐ๋ฅ โ ์ํ๊ณผ ํจ๊ป ์กฐํํ๋ฉด ์ฑ ์ ๋ถ๋ฆฌ ์ด๋ ค์
- ์ด๋ฏธ์ง์ ์ ์ฅยท์์ ยท์ญ์ ๋ก์ง์ ์ด๋ฏธ์ง ์๋น์ค์์ ๊ด๋ฆฌ โ ๊ด๋ฆฌ ์ผ๊ด์ฑ ํ๋ณด ํ์
- ๊ตฌ์กฐ ๋ณต์ก์ฑ์ ๋ฐฉ์งํ๊ณ , ์ฌ๋ฌ ๋๋ฉ์ธ์์ ์์ฒญ ์ ๋์ผํ ๊ตฌ์กฐ๋ฅผ ์ ์งํ๋ฉด ์ฝ๋ ์ดํด๋ ํฅ์๋ ๊ฒ์ด๋ผ ํ๋จ
ItemRepositryCustomImpl
@RequiredArgsConstructor
public class ItemRepositoryCustomImpl implements ItemRepositoryCustom {
. . . (๊ธฐ๋ณธ์ ์ธ ์ฌํญ๋ค์ ์์ ๊ตฌํ ๋ด์ฉ๊ณผ ๋์ผ)
private Page<Item> buildPagedItem(BooleanExpression whereCondition, Pageable pageable) {
// ์ ๋ ฌ ์กฐ๊ฑด
. . .
// ํ์ด์ง์ id ์กฐํ
. . .
// ํ์ด์ง์ ์ํ๋ค ์ ๋ณด ์กฐํ <- '.join(item.images).fetchJoin()' ์ ๊ฑฐ
List<Item> items = queryFactory
.selectFrom(item)
.distinct()
.join(item.user).fetchJoin()
.join(item.itemTags, itemTag).fetchJoin()
.join(itemTag.tag, tag).fetchJoin()
.join(item.images).fetchJoin()
.where(item.id.in(itemIds))
.orderBy(orderSpecifier)
.fetch();
// ํ์ด์ง ์ฒ๋ฆฌ๋ฅผ ์ํ ์ ์ฒด ์ด ์นด์ดํธ
. . .
return new PageImpl<>(items, pageable, total);
}
}
ItemService
@Slf4j
@Service
@RequiredArgsConstructor
public class ItemService {
private final ItemRepository itemRepository;
private final ImageService imageService;
@Loggable
@Transactional(readOnly = true)
public PageResponseDto<ItemGetAllResponse> searchItems(String keyword, List<String> tags, Long userId,
Pageable pageable) {
searchKeywordService.addKeyword(keyword);
Page<Item> pagedItems = itemRepository.search(keyword, tags, userId, pageable);
return buildGetAllItems(pagedItems);
}
// ํด๋น ํ์ด์ง ์ํ์ ์ด๋ฏธ์ง๋ฅผ ๋งตํํ์ฌ ๋ฐํ
@Transactional(readOnly = true)
protected PageResponseDto<ItemGetAllResponse> buildGetAllItems(Page<Item> pagedItems) {
// ํด๋น ๊ฒ์๊ธ ID ๋ชฉ๋ก์ ๋ํ ์ด๋ฏธ์ง ์ ๋ณด๋ฅผ ๋ฐํ
Map<Long, Image> mainImagesMap = imageService.getMainImagesForItems(pagedItems);
// ์กฐํฉ
Page<ItemGetAllResponse> dtoPage = pagedItems.map(item -> {
// --๋ฉ์ธ์ด๋ฏธ์ง ์กฐํฉ
Image mainImage = mainImagesMap.getOrDefault(item.getId(), null);
return ItemGetAllResponse.from(item, mainImage.getImageUrl());
});
return new PageResponseDto<>(dtoPage);
}
}
ImageService
@Service
@RequiredArgsConstructor
public class ImageService {
private final ImageRepository imageRepository;
// ํ์ฌ ์ํ ํ์ด์ง์ ์๋ ์ํ์ ์ด๋ฏธ์ง๋ค์ ๋งต์ผ๋ก ๋ฌถ์ด ๋ฐํ
@Transactional(readOnly = true)
public Map<Long, Image> getMainImagesForItems(Page<Item> pagedItems) {
// ํ์ฌ ํ์ด์ง์ ์กด์ฌํ๋ itemId ๋ฆฌ์คํธ
List<Long> itemIds = pagedItems.getContent().stream()
.map(Item::getId)
.distinct()
.collect(Collectors.toList());
if (itemIds.isEmpty()) {
return Collections.emptyMap();
}
// ํด๋น ์ํ๋ค์ ๋ํ ์ด๋ฏธ์ง ์กฐํ
List<Image> mainImages = imageRepository.findMainImagesByItemIds(itemIds);
// ๊ฒฐ๊ณผ ๋ฆฌ์คํธ๋ฅผ itemId๋ฅผ ํค๋ก ํ๋ Map์ผ๋ก ๋ณํ
return mainImages.stream()
.collect(Collectors.toMap(
img -> img.getItem().getId(),
img -> img
));
}
}
ํ์ฌ ๊ฐ์
- ์ด๋ฏธ์ง ๊ด๋ จ ๋ก์ง์ ๋ ๋ฆฝ์ ์ผ๋ก ๊ด๋ฆฌํ ์ ์์ด ์ ์ง๋ณด์์ฑ์ด ๋์
- ์ฌ๋ฌ ๋๋ฉ์ธ์์ ์ฌ์ฌ์ฉ ๊ฐ๋ฅ
- ์ํ ๋ก์ง๊ณผ ํผํฉ๋์ง ์์ ๋๋ฉ์ธ ๊ตฌ์กฐ ์์ฒด๋ ๋ช ํ
์ ์ฝ ์ฌํญ
- ๊ฐ์ ํ ๊ตฌ์กฐ๋ ์ฐ๊ด ๊ฐ์ฒด๊ฐ ๋ง์์ง์๋ก ์ถ๊ฐ ์กฐํ ์ฟผ๋ฆฌ ์ ์ฆ๊ฐ
- ๊ฒฐ๊ณผ๋ฅผ ์ง์ ์กฐ๋ฆฝํด์ผ ํด์ ์ฝ๋ ๋ณต์ก๋๊ฐ ์์น
- ์กฐํ ์ฟผ๋ฆฌ์ ๋ฐ์ดํฐ ๋งคํ์ด ๋ถ์ฐ๋์ด ์ค์ ์กฐํ์ ๋ฐ์ดํฐ ํ๋ฆ ํ์ ์ด ์ด๋ ค์ธ ์ ์์
๐ก ๊ฒ์์ด ๋ญํน ๊ธฐ๋ฅ Redis (Zset) ๋์
๊ธฐ์กด ์ธ๊ธฐ ๊ฒ์์ด ๋ญํน ๊ธฐ๋ฅ์ RDB ๊ธฐ๋ฐ์ผ๋ก ๊ตฌํ๋์ด ์์์ผ๋,
๋๋์ ์ค์๊ฐ ์ฐ๊ธฐ์ ์ ๋ ฌ/์ง๊ณ ์ฐ์ฐ์ด ๋์์ ๋ฐ์ํ๋ฉด์ ์ฑ๋ฅ ์ ํ์ DB ์ปค๋ฅ์ ํ ๊ณ ๊ฐ ๋ฌธ์ ๊ฐ ๋ฐ์ํ์์ต๋๋ค.
์ด์ ๋ฐ๋ผ ์ค์๊ฐ ์ง๊ณ์ ๋น ๋ฅธ ์กฐํ๋ฅผ ๋์์ ๋ง์กฑ์ํฌ ์ ์๋ ๋์์ด ํ์ํ์ต๋๋ค.
- ๋๋์ ์ค์๊ฐ ์ฐ๊ธฐ ์ฒ๋ฆฌ๋ฅผ ์์ ์ ์ผ๋ก ์ง์ํด์ผ ํจ
- ์์ N๊ฐ ์ธ๊ธฐ ๊ฒ์์ด๋ฅผ ์ด๊ณ ์ ์กฐํ ๊ฐ๋ฅํด์ผ ํจ
- ์ด์ ์ค์ธ ์๋น์ค์์ ํธํ์ฑ ๋ฐ ์ด์ ํจ์จ์ฑ์ ๊ณ ๋ คํด์ผ ํจ
- ์๋ก์ด ์ธํ๋ผ ๋์ ์ ์ด์ ๋ถ๋ด ์ต์ํ ํ์
ํด๊ฒฐ ๋ฐฉ์์ผ๋ก๋ RDB ํ๋, ์ธ๋ฉ๋ชจ๋ฆฌ ์บ์, NoSQL ๊ณ์ด DB ๋์ ์ด ๊ฒํ ๋์์ต๋๋ค.
| ๋์ | ์ฅ์ | ํ๊ณ |
|---|---|---|
| ๊ธฐ์กด RDB ํ๋ (์ฟผ๋ฆฌ ์ต์ ํ, ์ธ๋ฑ์ค, ์ปค๋ฅ์ ํ ํ๋, ๋ฐฐ์น ์ฒ๋ฆฌ) | ์ถ๊ฐ ์ธํ๋ผ ์์ด ๊ฐ์ ๊ฐ๋ฅ, ์์ ์ ์ธ ์ด์ ๊ฒฝํ | ๋๋ ์ค์๊ฐ ์ฐ๊ธฐ ์ ๋ฌผ๋ฆฌ์ ํ๊ณ, ์ ๋ ฌ/์ง๊ณ ์ฐ์ฐ ๋ถํ ์ง์ |
| ์ธ๋ฉ๋ชจ๋ฆฌ ์บ์ (Caffeine ๋ฑ) | ๋จ์ผ ์๋ฒ ํ๊ฒฝ์์ ๋งค์ฐ ๋น ๋ฅธ ์กฐํ ์๋ | ๋ค์ค ์๋ฒ ํ๊ฒฝ ๋๊ธฐํ ์ด๋ ค์, ์๋ฒ ์ฌ์์ ์ ๋ฐ์ดํฐ ์ ์ค, ๋ญํน ์ ๋ ฌ ๋ก์ง ์ง์ ๊ตฌํ ํ์ |
| NoSQL ๊ณ์ด (MongoDB, Cassandra, Redis ๋ฑ) | ๊ณ ์ฑ๋ฅ ์ฐ๊ธฐ ์ฒ๋ฆฌ, ์ผ๋ถ ์ ํ์ TTLยท์ ๋ ฌ ์ง์ | ๋๋ถ๋ถ ๋ญํน ๋ก์ง ๋ณ๋ ๊ตฌํ ํ์, ์๋ก์ด DB ์ด์ ๋ถ๋ด |
๊ทธ์ค Redis๋ฅผ ์ ํํ ์ด์ :
- ์ด๋ฏธ ์๋น์ค ๋ด ๋ค๋ฅธ ๊ธฐ๋ฅ์์ Redis๋ฅผ ์ด์ ์ค โ ์ถ๊ฐ ํ์ตยท์ด์ ๋ถ๋ด ์ต์ํ
- Redis์ ZSet ์๋ฃ๊ตฌ์กฐ๋ฅผ ํตํด ๋ญํน ์ ๋ ฌ์ ๋ณ๋ ๊ตฌํ ์์ด ์ง์
- ์ธ๋ฉ๋ชจ๋ฆฌ ์บ์์ ์ฅ์ ์ ํฌํจํ๋ฉด์๋ NoSQL DB์ ์ฑ๋ฅ์ ์ด์ ์ ํ์ฉ ๊ฐ๋ฅ
- ZSet(์ ๋ ฌ ์งํฉ) ํ์ฉ
- ์ ์ ๊ธฐ๋ฐ ์๋ ์ ๋ ฌ ๋ฐ ์์ N๊ฐ ๊ฒ์์ด๋ฅผ ๋น ๋ฅด๊ฒ ์กฐํ ๊ฐ๋ฅ
- ๋ณ๋ ๋ญํน ๊ตฌํ ํ์ ์์
- ์ฐ๊ธฐยท์ฝ๊ธฐ ์ฑ๋ฅ ์ต์ ํ
- ์ด๋น ์์ญ๋ง ๊ฑด ์์ค์ ์ฐ๊ธฐ ์ฒ๋ฆฌ ๊ฐ๋ฅ
- DB ์ปค๋ฅ์ ํ ๊ณ ๊ฐ ๋ฌธ์ ๊ทผ๋ณธ์ ํด๊ฒฐ
- ์ด์ ํจ์จ์ฑ
- ๊ธฐ์กด Redis ์ด์ ๊ฒฝํ ์ฌํ์ฉ
- ์๋ก์ด ํ์ต ๋ฐ ์ด์ ๋ถ๋ด ์ต์ํ
- Redis ์ฅ์ ๋์: ๋จ์ผ ์ธ์คํด์ค ์ฅ์ ์ ๋ฐ์ดํฐ ์ ์ค ๊ฐ๋ฅ โ ํด๋ฌ์คํฐ๋ง ๋ฐ ์์ํ ์ต์ (RDB, AOF) ๊ฒํ ํ์
- ๋ฉ๋ชจ๋ฆฌ ๊ด๋ฆฌ: ์ธ๋ฉ๋ชจ๋ฆฌ ํน์ฑ์ ๋ฐ์ดํฐ ํฌ๊ธฐ ๊ด๋ฆฌ ํ์ โ TTL ๋ฐ ๋ฐ์ดํฐ ์ ๋ฆฌ ์ ์ฑ ์ ์ฉ ํ์
- ํ์ฅ์ฑ ๊ณ ๋ ค: ํฅํ ํธ๋ํฝ ๊ธ์ฆ ์ Redis Cluster๋ก ์ํ ํ์ฅ ๊ฐ๋ฅ์ฑ ํ๋ณด
๐ก ์ํ ์ฃผ๋ฌธ ๋์์ฑ ์ ์ด
๋ฌธ์ ์ํฉ
- ๋ค์ค ์ฌ์ฉ์ ํ๊ฒฝ์์ ๋์ผํ ๋ฆฌ์์ค์ ๋ํ ๋์ ์ ๊ทผ์ผ๋ก ์ธํ ๋ฐ์ดํฐ ์ผ๊ด์ฑ ๋ฌธ์ ๋ฐ์
- ํธ๋ํฝ ์ฆ๊ฐ์ ๋ฐ๋ฅธ ๋์์ฑ ์ด์ ๋น๋ฐ๋ก ๋น์ฆ๋์ค ๋ก์ง์ ์ ํ์ฑ ๋ณด์ฅ ํ์
๋น์ฆ๋์ค ์ํฉํธ
- ๋ฐ์ดํฐ ๋ถ์ผ์น๋ก ์ธํ ๊ณ ๊ฐ ๋ถ๋ง ๋ฐ ์ ๋ขฐ๋ ํ๋ฝ
- ์ฌ๊ณ ์ค๋ฒ์ ๋ง, ์ค๋ณต ์์ฝ ๋ฑ์ ์ด์์ ๋ฌธ์
- ์์คํ ์์ ์ฑ ๋ฐ ๋ฐ์ดํฐ ๋ฌด๊ฒฐ์ฑ ํ๋ณด ํ์
๊ธฐ๋ฅ์ ์๊ตฌ์ฌํญ
- ๋์ผ ๋ฆฌ์์ค์ ๋ํ ๋์ ์ ๊ทผ ์ ๋ฐ์ดํฐ ์ผ๊ด์ฑ ๋ณด์ฅ
- ํธ๋์ญ์ ๊ฒฉ๋ฆฌ ์์ค์ ๋ฐ๋ฅธ ์ ์ ํ ๋ฝ ๋ฉ์ปค๋์ฆ ๊ตฌํ
- ๋ฐ๋๋ฝ ๋ฐฉ์ง ๋ฐ ์ฒ๋ฆฌ ๋ฉ์ปค๋์ฆ ๊ตฌ์ถ
- ๋์ ๋์์ฑ์ ์ง์ํ๋ฉด์๋ ์ฑ๋ฅ ์ต์ ํ
๋์์ฑ ์ ์ด ๋ฐฉ์ ์ ํ
๋น๊ด๋ฝ, ๋๊ด๋ฝ ์ฅ์
| ํน์ฑ | ๋น๊ด๋ฝ | ๋๊ด๋ฝ |
|---|---|---|
| ๋ฐ์ดํฐ ์ผ๊ด์ฑ | ์๋ฒฝํ ์ผ๊ด์ฑ ๋ณด์ฅ (๋ฐ์ดํฐ ์ถฉ๋ ๋ฐ์ ๋ถ๊ฐ) | ๋ฒ์ ๊ด๋ฆฌ๋ฅผ ํตํด ์ถฉ๋ ์ ์ด |
| ์ฑ๋ฅ | - ์์ฐจ์ ์ผ๋ก ์ฒ๋ฆฌํ์ฌ ์์ ์ - ์ฌ์๋ ๋ก์ง ๋ถํ์ - ์ผ์ ํ ์๋ต ์๊ฐ |
- ๋์ ๋์์ฑ ์ฒ๋ฆฌ - ๋ฝ ๋๊ธฐ์๊ฐ ์์ - ๋ณ๋ ฌ ์ฒ๋ฆฌ ์ต์ ํ |
| ๊ตฌํ ๋ณต์ก๋ | - DB ๋ฝ ๋ฉ์ปค๋์ฆ ํ์ฉ - ์ง๊ด์ ์ธ ์ฝ๋ |
- ๋น์ฆ๋์ค ๋ก์ง๊ณผ ๋ถ๋ฆฌ - ์ ์ฐํ ์ถฉ๋ ์ฒ๋ฆฌ ์ ๋ต - ์ธ๋ฐํ ์ ์ด ๊ฐ๋ฅ |
| ์ฌ์ฉ์ ๊ฒฝํ | - ํ์คํ ์ฒ๋ฆฌ ๋ณด์ฅ - ์คํจ ์ ์ฆ์ ์๋ฆผ ๋ฐํ - ์์ธก ๊ฐ๋ฅํ ๊ฒฐ๊ณผ |
- ๋น ๋ฅธ ์ด๊ธฐ ์๋ต - ๋๊ธฐ ์๊ฐ ์์ - ๋์ ์์ ๊ฐ๋ฅ |
| ํ์ฅ์ฑ | - ํธ๋์ญ์
๊ฒฝ๊ณ ๋ช
ํ - ๋ฐ์ดํฐ ๋ฌด๊ฒฐ์ฑ ๋ณด์ฅ |
- ์ํ ํ์ฅ ์ฉ์ด - ํด๋ผ์ฐ๋ ํ๊ฒฝ ์ต์ ํ |
๋น๊ด๋ฝ, ๋๊ด๋ฝ ๋จ์
| ํน์ฑ | ๋น๊ด๋ฝ | ๋๊ด๋ฝ |
|---|---|---|
| ์ฑ๋ฅ | - ๋ฎ์ ๋์์ฑ - ์์ฐจ ์ฒ๋ฆฌ๋ก ์ธํ ๋๊ธฐ |
- ๋์ ์ถฉ๋๋ฅ ์์ ์ฑ๋ฅ์ ํ - ์ง์์ ์ธ ์ฌ์๋ ์ค๋ฒ ํค๋ - CPU ์ฌ์ฉ๋ ์ฆ๊ฐ - ๋ถ์์ ํ ์๋ต์๊ฐ |
| ๋ฆฌ์์ค ์ ์ | - DB ์ปค๋ฅ์
์ฅ์๊ฐ ์ ์ - ์ปค๋ฅ์ ํ ๊ณ ๊ฐ ์ํ - ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ ์ฆ๊ฐ |
- ์ฌ์๋๋ก ์ธํ ๋ฆฌ์์ค ๋ญ๋น - ๋ฒ์ ๊ด๋ฆฌ ์ค๋ฒํค๋ - ๋ณต์กํ ์ํ๊ด๋ฆฌ |
| ์์คํ ์ํ | - ๋ฐ๋๋ฝ ์ํ - ๋ฝ ํ์์์ ์ฒ๋ฆฌ ๋ณต์ก - ์ฅ์ ์ ํ ์ํ |
- ๋ณต์กํ ์๋ฌ ์ฒ๋ฆฌ - ๋ถ๋ถ ์คํจ ์ํฉ ๊ด๋ฆฌ - ๋ฐ์ดํฐ ์ ํฉ์ฑ ๊ฒ์ฆ ๋ณต์ก |
| ๊ตฌํ ๋ณต์ก๋ | - ํธ๋์ญ์
๊ฒฝ๊ณ๊ด๋ฆฌ ๋ณต์ก - ๋ฝ ๋ฒ์ ์ค์ ์ด๋ ค์ - ์ค์ฒฉ ํธ๋์ญ์ ์ฒ๋ฆฌ ๋ณต์ก |
- ๋ณต์กํ ์ฌ์๋ ๋ก์ง - ๋ฒ์ ์ถฉ๋ ํด๊ฒฐ ์ ๋ต ํ์ - ๋๋ฒ๊น ๋ณต์ก์ฑ |
๋น๊ด๋ฝ์ ์ ํํ ์ด์
- ๋ฐ์ดํฐ ์ผ๊ด์ฑ ๋ณด์ฅ
- ์ค๊ณ ๊ฑฐ๋์์ ๊ฐ์ฅ ์ค์ํ ๊ฒ์ โ์ ํํ 1๊ฐ ํ๋งคโ ์ฌ๊ณ ๋ถ์ผ์น๋ก ์ธํ ๊ณ ๊ฐ๋ถ๋ง ์ฐจ๋จ
- ์ค๊ณ ์ํ์ ํน์ฑ : ์ฌ๊ณ ํฌ์์ฑ
- ์ค๊ณ ์ํ์ ๋๋ถ๋ถ 1๊ฐ ํ์ ํ๋งค ์ด๊ธฐ๋๋ฌธ์ ๋๊ด๋ฝ์ ์ฅ์ ์ธ ๋์ ๋์์ฑ์ด ๋ฌด์๋ฏธํจ
- ๋๊ด๋ฝ์ ๋จ์ ์ธ ๋์ ์ถฉ๋๋ฅ ์ ์ํ์ฌ ์ฑ๋ฅ ์ ํ๊ฐ ๋ฐ์
- ๊ตฌํ ๋จ์์ฑ
- ๋น๊ด๋ฝ : ๋ณต์กํ ์ฌ์๋ ๋ก์ง ์์ด ๊ฐ๋จํ๊ฒ ๊ตฌํ๊ฐ๋ฅ, ์ ์ง๋ณด์์ฑ ํฅ์
- ๋๊ด๋ฝ : ์ฑ๋ฅ์ ์ข์ง๋ง ๋ณต์กํ ์ฌ์๋ ๋ก์ง
- ๋น์ฆ๋์ค ๋ก์ง ํตํฉ
- ์ฌ๊ณ ํ์ธ, ์ฌ์ฉ์ ๊ฒ์ฆ, ๊ฒฐ์ ์ฒ๋ฆฌ๋ฅผ ํ๋์ ํธ๋์ญ์ ์ผ๋ก ์์ ํ๊ฒ ์ฒ๋ฆฌ
- ์ฌ์ฉ์ ๊ฒฝํ ๊ฐ์
- ๊ตฌ๋งค ๊ฐ๋ฅ ์ฌ๋ถ๋ฅผ ๋ฏธ๋ฆฌ ํ์คํ๊ฒ ํ์ธ๊ฐ๋ฅ
DB ๋น๊ด๋ฝ vs Redis ๋น๊ด๋ฝ(๋ถ์ฐ๋ฝ)
| ๊ตฌ๋ถ | DB ๋น๊ด๋ฝ | Redis ๋น๊ด๋ฝ (๋ถ์ฐ๋ฝ) |
|---|---|---|
| ์ฅ์ | - ACID ๋ณด์ฅ - ๋ฐ์ดํฐ ์ผ๊ด์ฑ ์๋ฒฝ - ํธ๋์ญ์ ๋กค๋ฐฑ ์ง์ |
- DB ๋ถํ ๋ถ์ฐ - ๋งค์ฐ ๋น ๋ฅธ ๋ฝ ์ฒ๋ฆฌ ์๋ - ํ์ฅ์ฑ ์ฐ์ - ํ์์์ ์ ์ด ๊ฐ๋ฅ |
| ๋จ์ | - DB ์ปค๋ฅ์
์ ์ - ์ปค๋ฅ์ ํ ๊ณ ๊ฐ ์ํ - ๋์ DB ๋ถํ - ํ์ฅ์ฑ ์ ํ |
- ์ถ๊ฐ ์ธํ๋ผ ํ์ (Redis ์๋ฒ) - ๋คํธ์ํฌ ์ง์ฐ/์ฅ์ ์ํฅ - Redis ์ฅ์ ์ ์ํ |
| ํ์ฅ์ฑ | ๋ฎ์ (DB ์ค์ผ์ผ์ ํ์) | ๋์ (์ํ ํ์ฅ ๊ฐ๋ฅ) |
| ์ ํฉํ ๊ฒฝ์ฐ | ๋จ์ผ DB ํ๊ฒฝ, ๋ฐ์ดํฐ ์ผ๊ด์ฑ์ด ์ต์ฐ์ ์ผ ๋ | ๋ค์ค ์๋ฒ ํ๊ฒฝ, ๋๊ท๋ชจ ํธ๋ํฝ, ๊ณ ์ ๋ฝ์ด ํ์ํ ๊ฒฝ์ฐ |
Redis ๋ถ์ฐ๋ฝ์ ์ ํํ ์ด์
- ํ์ฅ์ฑ ๊ณ ๋ ค
- ์๋น์ค ์ฑ์ฅ์ ๋ฐ๋ฅธ ์ํ ํ์ฅ์ ๋๋นํด ๋ถ์ฐํ๊ฒฝ์์๋ ๋์ํ๋ ๋ฝ ํ์
- DB ๋ถํ ๋ถ์ฐ
- ์ธ๊ธฐ ์ํ์ ๊ฒฝ์ฐ ๋์ ์ ์์๊ฐ ๊ธ์ฆํ๋๋ฐ DB ์ปค๋ฅ์ ์ ์ค๋ ์ ์ ํ๋ฉด ์ ์ฒด ์์คํ ์ฑ๋ฅ ์ ํ ์ ๋ฐ
- Redis๋ก ๋ฝ ์ฒ๋ฆฌ๋ฅผ ๋ถ๋ฆฌํด DB๋ ์ค์ ๋ฐ์ดํฐ ์ฒ๋ฆฌ์๋ง ์ง์ค
- ์ ์ฐํ ํ์ ์์ ์ ์ด
- Redis์์ ๋ฝ ํ์์์์ ์ ์ฐํ๊ฒ ์กฐ์ ํด ๋ฐ๋๋ฝ ๋ฐฉ์ง ๊ฐ๋ฅ
- ์ฑ๋ฅ ์ต์ ํ
- ๋ฉ๋ชจ๋ฆฌ ๊ธฐ๋ฐ Redis๋ ๋ฝ ํ๋/ํด์ ๊ฐ ๋งค์ฐ ๋น ๋ฆ
์ฌ๊ณ ๊ด๋ฆฌ ์๋น์ค
@Service
@RequiredArgsConstructor
public class StockService {
@Lazy
private final RedissonClient redissonClient;
private final ItemRepository itemRepository;
// ๋ถ์ฐ๋ฝ์ ํตํ ์ฌ๊ณ ๊ฐ์
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void decreaseStock(Long itemId, Long quantity) {
String lockKey = "Lock:" + itemId;
RLock lock = redissonClient.getLock(lockKey);
try {
// ๋ฝ ํ๋ ๋๊ธฐ์๊ฐ 3์ด, ์๋ ํด์ ์๊ฐ 10์ด
boolean acquired = lock.tryLock(3000, 10000, TimeUnit.MILLISECONDS);
if (!acquired) {
throw new CustomException(ExceptionCode.STOCK_LOCK_FAILED);
}
try {
// DB ๋ ๋ฒจ ๋น๊ด์ ๋ฝ๊ณผ ์กฐํฉ
Item item = itemRepository.findByIdWithLock(itemId)
.orElseThrow(() -> new CustomException(ExceptionCode.NOT_FOUND_ITEM));
item.decreaseStock(quantity);
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new CustomException(ExceptionCode.OPERATION_INTERRUPTED);
}
}
}
Repository ๋ ๋ฒจ ๋น๊ด์ ๋ฝ
@Repository
public interface ItemRepository extends JpaRepository<Item, Long>, ItemRepositoryCustom {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT i FROM Item i WHERE i.id = :itemId")
Optional<Item> findByIdWithLock(@Param("itemId") Long itemId);
}
์ฃผ๋ฌธ ์๋น์ค
@Service
@RequiredArgsConstructor
public class OrderService {
private final StockService stockService;
@Transactional
public CreateOrderResponseDto createOrder(CreateOrderRequestDto requestDto, Long userId) {
// ๋น์ฆ๋์ค ๋ก์ง ๊ฒ์ฆ...
// ๋ถ์ฐ ๋ฝ์ ํตํ ์์ ํ ์ฌ๊ณ ์ฐจ๊ฐ
stockService.decreaseStock(item.getId(), requestDto.getQuantity());
// ์ฃผ๋ฌธ ์์ฑ ๋ก์ง...
}
}
๋ถ์ฐ๋ฝ ์์๋
๋ฐ๋๋ฝ ๋ฐฉ์ง ์ ๋ต
- ๋ฝ ์์ ์ ๋ ฌ: ์ฌ๋ฌ ๋ฆฌ์์ค ๋ฝ ์ ID ์์ผ๋ก ์ ๋ ฌํ์ฌ ํ๋
- ํ์์์ ์ค์ : ๋ชจ๋ ๋ฝ์ ์ ์ ํ ํ์์์ ์ค์
- ๋ฝ ๋ฒ์ ์ต์ํ: ํธ๋์ญ์ ๋ฒ์๋ฅผ ์ต์ํ์ผ๋ก ์ ์ง
๋ชจ๋ํฐ๋ง
- ๋ฝ ๋๊ธฐ์๊ฐ ๋ชจ๋ํฐ๋ง: ํ๊ท ๋๊ธฐ์๊ฐ ๋ฐ ์ต๋ ๋๊ธฐ์๊ฐ ์ถ์
- ๋ฐ๋๋ฝ ๋ฐ์๋ฅ ์ถ์ : ๋ฐ๋๋ฝ ๋ฐ์ ๋น๋ ๋ฐ ํจํด ๋ถ์
- ์ฑ๋ฅ ๋ฉํธ๋ฆญ ์์ง: ์ฒ๋ฆฌ๋, ์๋ต์๊ฐ, ์๋ฌ์จ ์ง์์ ๋ชจ๋ํฐ๋ง
ํ์ฅ์ฑ ๊ณ ๋ ค์ฌํญ
- ์ค๋ฉ ์ ๋ต: ๋ฐ์ดํฐ ์ฆ๊ฐ ์ ์ํ์ ํ์ฅ์ ์ํ ์ค๋ฉ ๊ณํ
๐กย ์๋ง์กด SES๋ฅผ ์ด์ฉํ ๋ฉ์ผ ์๋ ๊ธฐ๋ฅ ๊ตฌํ
์์์์ ๋ฐ๋งค ์์ ์ธ ๊ตฟ์ฆ๋ ์ด๋ฒคํธ์ ๊ฐ์ ๊ณต์์ ์ธ ์ ๋ณด๋ฅผ ์ฌ์ฉ์์๊ฒ ์ ๋ฌํ๋ ๊ณต๊ฐ์ ๋๋ค.
ํ์๋ค์ด ์์์์ ๋ ๋ง์ ๊ด์ฌ์ ๊ฐ์ง๊ณ ์์ฃผ ์ด์ฉํ๋๋ก ํ๊ธฐ ์ํด, ์์์์ด ๋ฑ๋ก๋๋ฉด ์ด๋ฉ์ผ๋ก ์๋ฆผ์ ๋ณด๋ด๋ ๋ฐฉ์์ ๊ฒํ ํ์์ต๋๋ค.
๊ฒํ ๊ณผ์ ์์ ๊ณ ๋ คํ ์ฌํญ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
- ํ๋ก ํธ์๋๋ ์ฑ์ด ๊ตฌ๋๋์ด ์์ง ์์
- ์ด๋ฉ์ผ, ๋ฌธ์, ์นด์นด์คํก ์ค์์ ๋ฌธ์์ ์นด์นด์คํก์ ํ์ฌ ์์ ์์ ๋น์ฆ๋์ค ๊ณ์ ์ธ์ฆ์ด ์ด๋ ค์
- ์ด๋ฉ์ผ์ ์ด๋ฏธ ๊ธฐ์ ๊ด๊ณ ๋งค์ฒด๋ก ๋๋ฆฌ ์ฌ์ฉ๋จ
- ์ฐ๋ฆฌ ์๋น์ค์์๋ ํ์ ๊ฐ์ ์ ์ด๋ฉ์ผ ์ ๋ ฅ์ด ํ์์
โ ๋ฐ๋ผ์, ์๋ฆผ ๋งค์ฒด๋ก ์ด๋ฉ์ผ ์ ์ก ๊ธฐ๋ฅ์ ์ฐ์ ์ ์ผ๋ก ๋์ ํ๊ธฐ๋ก ๊ฒฐ์ ํ์์ต๋๋ค.
- ์์์ ๋ฑ๋ก ์ ํ์์๊ฒ ์ด๋ฉ์ผ ์ ์ก
- ๋ฉ์ผ ๋ฐ์ก ์ค API ์๋ต ์ง์ฐ ์ต์ํ
- ๋ฉ์ผ ๋ฐ์ก ์คํจ ์ ๋ก๊ทธ ๊ธฐ๋ก ๋ฐ ์ฌ์๋ ๊ฐ๋ฅ
- ํ์ฅ์ฑ ๊ณ ๋ ค (๋์ค์ ์ธ๋ถ MQ ์ ์ฉ ๊ฐ๋ฅ)
SMTP ์ ํ ์ด์
- ์ด๋ฉ์ผ ์๋ฆผ ๊ธฐ๋ฅ์ ๊ตฌํํ๊ธฐ ์ํด **SMTP(Simple Mail Transfer Protocol)**๋ฅผ ์ฌ์ฉํ๊ธฐ๋ก ๊ฒฐ์
- SMTP๋ ์ธํฐ๋ท์์ ์ด๋ฉ์ผ์ ์ ์กํ ๋ ์ฌ์ฉํ๋ ํ์ค ํต์ ๊ท์ฝ์ผ๋ก, ๋ฐ์ ๋ฉ์ผ์ ๋ฐ์ ์์ ์๋ฒ๋ก ์ ๋ฌํ๋ ์ญํ
- SMTP๋ฅผ ์ฌ์ฉํจ์ผ๋ก์จ:
- ๋ฒ์ฉ์ฑ: ๊ตฌ๊ธ, ์๋ง์กด SES, ์ฌ๋ด ๋ฉ์ผ ์๋ฒ ๋ฑ ๊ฑฐ์ ๋ชจ๋ ๋ฉ์ผ ์๋ฒ์์ ์ง์
- ์์ ์ฑ: ํ์คํ๋ ํ๋กํ ์ฝ์ด๋ฏ๋ก ํธํ์ฑ ๋ฌธ์ ์ต์ํ
- ์ง์ ์ ์ด ๊ฐ๋ฅ: ์ ์ก ๊ณผ์ ์ ์ฝ๋์์ ๋ฐ๋ก ์ ์ดํ ์ ์์
SMTP ํ ์คํธ ํ๊ฒฝ๊ณผ ์ค์ ์๋น์ค
- ์ด๊ธฐ ๊ฐ๋ฐ ๋จ๊ณ์์๋ ๊ตฌ๊ธ SMTP๋ก ํ ์คํธํ์์ผ๋ ์ ์ก ํ์ ์ ํ์ด ์กด์ฌ
- ๋ฐ๋ผ์, ์ค์ ์๋น์ค์์๋ ์ ๋ฌธ ๋ฉ์ผ ์ก์ ์๋น์ค์ธ ์๋ง์กด SES๋ฅผ ์ฌ์ฉํ์ฌ ๊ตฌํ
ํ ํ์ฉ ๋ฐ ์ค๊ณ ์ด์
- ๋ฉ์ผ ๋ฐ์ก ์คํจ ์ ์์ ์ ์ธ ์ฒ๋ฆฌ๋ฅผ ์ํด Redis ํ๋ฅผ ํ์ฉ
- ์์์ ๋ฑ๋ก ์ ๋ชจ๋ ํ์์๊ฒ ๋ฉ์ผ์ ๋ณด๋ด๋๋ก ๋น๋๊ธฐ ์ฒ๋ฆฌํ๋๋ก ์ค๊ณ
- ํ๋ฅผ ์ฌ์ฉํจ์ผ๋ก์จ:
- ๋๋ ๋ฐ์ก ์ API ์๋ต ์ง์ฐ ์ต์ํ
- ์คํจํ ๋ฉ์ผ์ ๋ํ ์ฌ์๋ ๋ฐ ๋ก๊น ์ฉ์ด
- ๋์ค์ RabbitMQ, Kafka, SQS ๋ฑ ๋ค๋ฅธ ๋ฉ์์ง ํ๋ก ์ ํ ์ ๊ตฌ์กฐ์ ์ ์ฐ์ฑ ํ๋ณด
๊ณผ์ 1. ๊ธฐ๋ณธ ๋ฉ์ผ ์ ์ก ๊ตฌํ
build.gardle ์์กด์ฑ ์ถ๊ฐ
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-mail'
}
application.yml ๋ฉ์ผ ์ค์ ์ถ๊ฐ
spring:
mail:
host: smtp.gmail.com
port: 587
username: ${SES_USERNAME}
password: ${SES_PASSWORD}
properties:
mail:
smtp:
auth: true
starttls:
enable: true
email: ${EMAIL}
MailSendService
@RequiredArgsConstructor
public class MailSendService {
private final JavaMailSender mailSender;
// ์ก์ ์ ๋ฉ์ผ ํ๊ฒฝ๋ณ์ ๋ฐ๊ธฐ
@Value("${email}")
private String from;
public void sendNewsfeedNotification(String toEmail, String newsfeedTitle) {
// ๋ฉ์ผ ๋ฉ์์ง ๊ฐ์ฒด ์์ฑ
SimpleMailMessage message = new SimpleMailMessage();
// ์ก์ ์ ๋ฉ์ผ
message.setFrom(from)
// ์์ ์ ๋ฉ์ผ
message.setTo(toEmail);
// ์ ๋ชฉ
message.setSubject("[์์์] '" + newsfeedTitle + "'๊ฐ ์๋ก ์ฌ๋ผ์์ด์!");
// ๋ณธ๋ฌธ
StringBuilder body = new StringBuilder();
body.append("์๋
ํ์ธ์, Mania Place์
๋๋ค.\n\n");
body.append("์ ๋ชฉ: ").append(newsfeedTitle).append("\n");
body.append("์ง๊ธ ๋ฐ๋ก Mania Place์ ์ ์ํ๊ณ ์์์์ ํ์ธํด๋ณด์ธ์!");
message.setText(body.toString());
// ์ ์ก
mailSender.send(message);
}
}
JavaMailSender๋ก ๋ฉ์ผ ๋ฐ์ก ์๋น์ค ๊ตฌํ- ๋๊ธฐ(Synchronous) ๋ฐฉ์์ผ๋ก ์ฒ๋ฆฌ๋์ด ๊ฐ ๋ฉ์ผ ๋ฐ์ก ์๋ฃ๊น์ง ์๋ต ๋๊ธฐ โ API ์ง์ฐ ๋ฐ ์๋ฒ ๋ฆฌ์์ค ์ ์ ๋ฐ์
- ์ค์ ์ธก์ ๊ฒฐ๊ณผ: ๋ฉ์ผ 1๊ฑด ๋ฐ์ก ์ 4.34์ด ์์, ์์ ์ ์ ์ฆ๊ฐ์ ๋น๋กํ์ฌ ์๋ต ์๊ฐ์ด ์ ํ์ ์ผ๋ก ์ฆ๊ฐํจ ํ์ธ
๊ณผ์ 2. ๋น๋๊ธฐ ์ฒ๋ฆฌ ์๋
- MailService์
@Async๋ฅผ ์ ์ฉํด ๋ฉ์ผ ๋ฐ์ก์ ๋น๋๊ธฐ ์ฒ๋ฆฌ ์๋ - API ์๋ต์ ์ฆ์ ๋ฐํ๋๋ฉฐ, ๋ฉ์ผ ์ ์ก์ ๋ฐฑ๊ทธ๋ผ์ด๋์์ ์งํ
- ๋น๋๊ธฐ ์ ์ฉ ํ ์๋ต ์๊ฐ: ํ๊ท 200ms ์์ค
- ๋น๋๊ธฐ๋ก ์๋ต ์๋๋ ๋นจ๋ผ์ก์ง๋ง, ๋ฉ์ผ ์ ์ก ์คํจ ์ ๋ฐ๋ก ์๊ธฐ ์ด๋ ต๊ณ ์ฌ์ฒ๋ฆฌ๋ ์ฝ์ง ์๋ค๋ ๋จ์ ๋ฐ์
๊ณผ์ 3. ์์ ์ ๋ฐ์ก์ ์ํ ํ ์ ์ฉ
MailRequestDto
@Data
public class MailRequest implements Serializable {
private String toEmail;
private String title;
private int retryCount = 0; // ์ฌ์๋ ํ์ ๊ธฐ๋ณธ 0
private MailRequest(String toEmail, String newsfeedTitle) {
this.toEmail = toEmail;
this.title = newsfeedTitle;
}
public static MailRequest of(String toEmail, String newsfeedTitle) {
return new MailRequest(toEmail, newsfeedTitle);
}
}
โ RedisMailQueueService
@Service
@RequiredArgsConstructor
public class RedisMailQueueService {
private final RedissonClient redissonClient;
private static final String MAIL_QUEUE_KEY = "newsfeed:mail:queue";
public void enqueueMail(MailRequest mailRequest) {
// ๋ฉ์ผ ์์ฒญ์ Redis ํ์ ๋ฃ๋ ์ ์ฅยท์ ๋ฌ ๋ก์ง
RBlockingQueue<MailRequest> queue = redissonClient.getBlockingQueue(MAIL_QUEUE_KEY);
queue.add(mailRequest);
}
}
RedisMailWorker
@Slf4j
@Component
@RequiredArgsConstructor
public class RedisMailWorker implements InitializingBean, DisposableBean {
private final RedissonClient redissonClient;
private final MailSendService mailService;
private static final String MAIL_QUEUE_KEY = "newsfeed:mail:queue";
private volatile boolean running = true;
private Thread workerThread;
@Override
public void afterPropertiesSet() {
workerThread = new Thread(() -> {
RBlockingQueue<MailRequest> queue = redissonClient.getBlockingQueue(MAIL_QUEUE_KEY);
while (running) {
MailRequest mailRequest = null;
try {
// ํ์์ MailRequest ๊ฐ์ฒด๊ฐ ๋ค์ด์ฌ ๋๊น์ง ๋๊ธฐ
mailRequest = queue.take();
// ๋ฐ์ ์์ฒญ์ผ๋ก ๋ฉ์ผ ์ ์ก
mailService.sendNewsfeedNotification(
mailRequest.getToEmail(),
mailRequest.getTitle()
);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // ์ธํฐ๋ฝํธ ์ฒ๋ฆฌ
} catch (Exception e) {
// ๋ฉ์ผ ์ ์ก ์๋ฌ ๋ฐ์ ์ ๋ก๊ทธ ๊ธฐ๋ก
log.error("[{}] {} - {} - (USER ID: {}) ({} ms) - ERROR: {} | Params: {}",
"MAIL_SEND", MAIL_QUEUE_KEY, "sendNewsfeedNotification",
"system", 0L, e.getMessage(),
mailRequest != null ? mailRequest.toString() : "null"
);
if (mailRequest != null && mailRequest.getRetryCount() < 3) {
mailRequest.setRetryCount(mailRequest.getRetryCount() + 1);
try {
// ์คํจํ ์์ฒญ ๋ค์ ํ์ ๋ฃ๊ธฐ
Thread.sleep(1000); // 1์ด ๋๊ธฐ
queue.offer(mailRequest);
} catch (Exception ex) {
// ํ์ ๋ค์ ๋ฃ๋ ์ค ์๋ฌ ๋ฐ์ ์ ๋ก๊ทธ ๊ธฐ๋ก
log.error("[{}] {} - {} - (USER ID: {}) ({} ms) - ERROR: {} | Params: {}",
"MAIL_SEND_REQUEUE", MAIL_QUEUE_KEY, "queue.offer",
"system", 0L, ex.getMessage(),
mailRequest != null ? mailRequest.toString() : "null"
);
}
}
}
}
});
workerThread.setDaemon(true);
workerThread.start();
}
@Override
public void destroy() {
running = false;
workerThread.interrupt();
}
}
- ์์ ๋จ๊ณ์์ ์ ์ฉํ
@Async์ ๊ฑฐ - Redis ํ๋ฅผ ์ฌ์ฉํ์ฌ ๋ฉ์ผ ๋ฐ์ก ์์ฒญ์ ๋น๋๊ธฐ์ ์ผ๋ก ์ฒ๋ฆฌํ๋๋ก ๋ณ๊ฒฝ
- ์์ปค๊ฐ ํ๋ฅผ ๋ชจ๋ํฐ๋งํ๋ฉฐ ๋ฉ์ผ ๋ฐ์ก ์ํ
- ๋ฉ์ผ ์ ์ก์ ์คํจํ๋ฉด ๋ค์ ํ์ ๋ฃ์ด ์ ์ก ์ฌ์๋ ์ํ ์ด๋, ๋ฌดํ ์ฌ์๋๋ก ์ธํ ํ ์ ์ฒด ๋ฐ ๋ฆฌ์์ค ๋ญ๋น๋ฅผ ๋ฐฉ์งํ๊ธฐ ์ํด ์ต๋ 3ํ๋ก ์ ํ
- ์ฌ์๋ ์ ์ฑ
๊ฐ์
- ์ง์ ๋ฐฑ์คํ: ์ฌ์๋ ๊ฐ๊ฒฉ์ ์ ์ฐจ ๋๋ ค ๊ณผ๋ถํ ๋ฐฉ์ง
- ๋ฐฐ์น ์ฌ์ ์ก: ์คํจ ๋ฉ์ผ์ ๋ชจ์ ์ผ์ ์์ ์ ์ผ๊ด ์ฌ๋ฐ์ก
- ๋ฉ์์ง ํ ํ์ฅ์ฑ
- RabbitMQ: ์์ ์ ์ธ ๋ฉ์์ง ์ ์ก, ack/queue ๊ด๋ฆฌ ์ฉ์ด
- Kafka: ๋์ ์ฒ๋ฆฌ๋(TPS), ์ด๋ฒคํธ ์คํธ๋ฆฌ๋ฐ ์ ํฉ
- SQS (AWS): ์์ ๊ด๋ฆฌํ ์๋น์ค๋ก ์๋ฒ ์ด์ ๋ถ๋ด ์์
๐กย ์ค์๊ฐ์ฑํ ๋์
http ํ๋กํ ์ฝ์ ๋จ๋ฐฉํฅํต์ ์ผ๋ก ์ค๊ณ๋์๋๋ฐ ์ค์๊ฐ ์ฑํ ์ ๊ตฌํํ๋ค๊ณ ๊ฐ์ ํํ ๋, ์ฑํ ์๋ณด๋ด๊ณ ์๋ต์ ๋ฐ๋์์ฒญ์ ๋ ๋ณด๋ด์ผ ํ๊ธฐ์ ๊ตฌ์กฐ์ ์ผ๋ก ์์ํจ์ ๋๊ปด ์ข ๋ ๋์ ๋ฐฉ์๋ค์ด ์๋์ง ์ฐพ์๋ณด์์ต๋๋ค.
์ค์๊ฐ ์๋ฐฉํฅ ํต์ ์ผ๋ก ์ฌ์ฉ์๊ฐ ๋ฉ์ธ์ง๋ฅผ ๋ณด๋์ ๋ ์ฆ์ ์๋๋ฐฉ์๊ฒ ๋ฉ์ธ์ง๊ฐ ๋์ฐฉํด์ผํฉ๋๋ค.
- ํด๋ง : ์คํ๋ฝ์ฒ๋ผ ๊ณ์ํด์ ์๋ก์ด ๋ฉ์ธ์ง๋ฅผ ํ์ธํ๋ ๋ฐฉ์
๋จ์ : ๋ฉ์ธ์ง๊ฐ ์๋์ง ๊ณ์ ํ์ธ์ ํ๋๊ณผ์ ์์ ๊ณ์ํด์ ๋ฆฌ์์ค๋ฅผ ์ฌ์ฉํ๋๋ฐ, ์ด๋ฐ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด 1์ด๋ง๋ค ํ์ธํ๋๊ฒ์ 5์ด๋ง๋ค ํ์ธํ๋์์ผ๋ก ์์ ํ๋ค๋ฉด ์ค์๊ฐ์ฑํ ์ ํ์ธํ๊ธฐ ์ํ ๋ชฉ์ ์ด ๋ถ์ ๋๋ ๋ชจ์์ด ๋ฐ์ํด ์ ํฉํ์ง ์์ ๋ฐฉ์์ด๋ผ๊ณ ํ๋จํ์ต๋๋ค.
- ๋กฑํด๋ง : ์์ฒญ์ ๋ณด๋ด๊ณ ์๋ก์ด ๋ฉ์ธ์ง๊ฐ ์๊ธฐ๋ฉด ์๋ตํ๋๋ฐฉ์
๋จ์ : ํด๋ง์ ๋จ์ ์ธ ๋ฌด์๋ฏธํ ์์ฒญ์ ๊ณ์ ๋ณด๋ด๋๊ฒ์ ํด๊ฒฐํ์ง๋ง ๋ฉ์ธ์ง๊ฐ ์๊ธธ๋๊น์ง ์ฐ๊ฒฐ์ ์ ์งํ๊ธฐ๋๋ฌธ์ ์ฐ๋ ๋๋ฅผ ์ ์ ํ๋ ๋ฌธ์ ๊ฐ ์์ด ๋๊ท๋ชจ ํธ๋ํฝ์ํฉ์์ ๋ถ์ ํฉํ ๋ฐฉ์์ด๋ผ๊ณ ํ๋จํ์ต๋๋ค.
- SSE : ํด๋ผ์ด์ธํธ๊ฐ ์๋ฒ์ ์ฐ๊ฒฐ์ ์์ฒญํ๋ฉด ์๋ฒ๋ ์ด ์ฐ๊ฒฐ์ ์ ์งํ๋ฉฐ ์๋ก์ด ๋ฐ์ดํฐ๊ฐ ์๊ธธ๋๋ง๋ค ํด๋ผ์ด์ธํธ์๊ฒ ์๋ต์์ฃผ๋ ์ค์๊ฐ ๋จ๋ฐฉํฅ ํต์ ๋ฐฉ์
๋จ์ : ์์ฒญ์ http, ์๋ต์ sse๋ก ๊ตฌํ์ ๊ฐ๋ฅํ์ง๋ง ๊ธฐ๋ณธ์ ์ผ๋ก ๋จ๋ฐฉํฅ ํต์ ์ ์ํด ๋์จ๋ฐฉ์์ด๊ธฐ๊ณ ์ฑํ ๊ณผ ๊ฐ์ ์๋ฐฉํฅ ๊ตฌ์กฐ์ ์ ํฉํ์ง ์๋ค๊ณ ํ๋จํ์ต๋๋ค.
- websocket + stomp
websockete : ํ ๋ฒ ์ฐ๊ฒฐ๋๋ฉด ์๋ฐฉํฅ์ผ๋ก ๊ณ์ ํต์ ํ๋ ์๋ฐฉํฅ ํต์ ์ง์ ํ๋กํ ์ฝ statefulํ ํ๋กํ ์ฝ์ด๊ธฐ์ ์๋ก์ ์ํ๋ฅผ ์์์์ด ์ฑํ ์ฝ์, ์ฑํ ๋ฐฉ ๋๊ฐ๋ฑ์ ์ ๋ณด๋ ์ค์๊ฐ์ผ๋ก ์์๊ฐ ์์.
stompํ๋กํ ์ฝ : ์น์์บฃ ํ๋กํ ์ฝ์ ์์์ ๋์ํ๋ ํ๋กํ ์ฝ๋ก send, subscribe, publish ๋ฑ์ ๊ท์ ๋ ํ์ ์ ๊ณตํ๋ ์๋ธ ํ๋กํ ์ฝ ๋จ์ : ๋จ์ผ ์๋ฒ์์๋ ์๋ง์ websocket์ฐ๊ฒฐ์ ์ ์งํ๊ณ stomp ํต์ ์ฒ๋ฆฌํ๋๊ฒ์ด ๋ถ๋ด๋ ์ ์์ต๋๋ค.
- ๊ฒฐ์
์์ ๋ฐฉ์๋ค์ค ์๋น์ค์ ์์ ์ฑ, ์์ ์๋ชจ๋ฅผ ๊ณ ๋ คํ์๋ ์๋์๊ฐ์ ์ด์ ๋ก 4๋ฒ์ ์ ํํ์ต๋๋ค.
์น์์ผ ์ฐ๊ฒฐ ์์ฒด๋ ์ค๋ ๋๋ฅผ ๊ณ์ ์ ์ ํ์ง ์๊ณ ๋๊ธฐํ๋ค๊ฐ stompํต์ ์ด ์ค๊ฐ๋๋ง ์ค๋ ๋๊ฐ ํ ๋น๋์ด ์ ์งํ๋๊ฒ์๋ ๋ฌธ์ ๊ฐ์๋ค๊ณ ํ๋จํ์ต๋๋ค.
๋ํ ๋์ฉ๋ ํธ๋ํฝ ์ํฉ์์๋ TaskExecutor ์ค์ ์ ํตํด ๋ฉ์์ง ์ฒ๋ฆฌ๋ฅผ ์ํ ์ค๋ ๋ ํ์ ํฌ๊ธฐ๋ฅผ ์ ์ฐํ๊ฒ ์กฐ์ ํ ์ ์์ด ์ ์ฐํ ๋์ฒ์ ์ ํฉํ๋ค๊ณ ํ๋จํ์ต๋๋ค.
๋จ์ผ์๋ฒ๋ก ์ธํด ์๊ธฐ๋ ๋ฌธ์ ์ ์ ๋ฉ์ธ์ง ํ๋ฑ์ ์ฌ์ฉํ์ฌ ์ฌ๋ฌ ์๋ฒ๋ก ์ํ๋ฅผ ๊ณต์ ํ๋ ๋ฐฉ์์ผ๋ก ๊ตฌํํด ํด๊ฒฐํด ๋ณผ ์ ์์ ๊ฒ ๊ฐ์ websocket + stomp ๋ฐฉ์์ผ๋ก ๊ตฌํํ๊ธฐ๋ก ๊ฒฐ์ ํ์ต๋๋ค.
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws/chat") //stomp ์ฐ๊ฒฐ์ ์ํ ์๋ํฌ์ธํธ ์ค์
.setAllowedOriginPatterns("*")// ๊ฐ๋ฐ ๋จ๊ณ์์ ๋ชจ๋ origin ํ์ฉ
.withSockJS();// socket+javascript
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/sub");
// ๋ฉ์ธ์ง๋ธ๋ก์ปค์์ ํด๋ผ์ด์ธํธ๊ฐ ๊ตฌ๋
ํ prefix
registry.setApplicationDestinationPrefixes("/pub");
// ํด๋ผ์ด์ธํธ๊ฐ ์๋ฒ๋ก ๋ฉ์ธ์ง ๋ณด๋ผ ๋ prefix
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(interceptors);
}
๋ฉ์ธ์ง์ ์์์ฑ, ๋ฉ์ธ์ง์ ์ ์ค, ๋๊ท๋ชจ ํธ๋ํฝ์ํฉ์์์ ์์ ์ ์ธ ์ฐ๋ ๋ ํ ๋น์ด ํ์ํฉ๋๋ค.
๐กย rabbitMQ ๋์
๊ธฐ์กด ์ฑํ ์์คํ ์ ๋ฉ์ธ์ง๊ฐ ์์ฑ๋ ๋๋ง๋ค ๋งค๋ฒ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ง์ INSERT ์ฟผ๋ฆฌ๋ฅผ ๋ณด๋ด๋ ๋๊ธฐ์ ๋ฐฉ์์ผ๋ก ๊ตฌํ๋์ด ์์ต๋๋ค.
์ด ๋ฐฉ์์ ํธ๋ ํฝ์ด ๋ชฐ๋ฆฌ๋ ์ํฉ์ด๋ผ๋ฉด DB์ ๋ถํ๊ฐ ์ง์ค๋์ด ์ฑ๋ฅ์ด ๋จ์ด์ง ์ ์๊ณ , DB ์ ๊ทผ ๊ณผ์ ์์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ค๋ฉด ์๋น์ค ์๋ต ์์ฒด๊ฐ ์ง์ฐ๋๋ ๋ฌธ์ ๋ฅผ ๋ฐ์์ํค๊ธฐ๋ ํฉ๋๋ค.
//controller
@MessageMapping("/chatroom/{roomId}")
public void sendMessageV1(
@DestinationVariable Long roomId,
@Payload ChatMessageRequest request,
CustomPrincipal principal
) {
chatMessageService.saveMessage(roomId, request, principal);
messagingTemplate.convertAndSend("/sub/chatroom/" + roomId, request);
}
//service
@Transactional
public ChatMessage saveMessage(Long roomId,ChatMessageRequest request, CustomPrincipal principal) {
User sender = userService.findByIdOrElseThrow(principal.getId());
ChatRoom room = chatRoomService.findByIdOrElseThrow(roomId);
ChatMessage message = ChatMessage.of(room, sender, request.getContent(), null);
return chatMessageRepository.save(message);
}
๋น๋๊ธฐ ์ฒ๋ฆฌ : ๋ฉ์ธ์ง ์ ์ฅ ๋ก์ง์ ๋ถ๋ฆฌํ์ฌ ์ฌ์ฉ์๊ฐ ๋ฉ์ธ์ง๋ฅผ ๋ณด๋์ ๋ ์ฆ๊ฐ์ ์ผ๋ก ์๋ต์ ๋ฐ์ ์ ์์ด์ผํจ.
์ฑ๋ฅ ๋ฐ ์์ ์ฑ : DB์ ์ง์ ์ ์ธ ๋ถํ๋ฅผ ์ค์ด๊ณ , ํธ๋ํฝ์ด ๋ชฐ๋ ค๋ ์์ ์ ์ผ๋ก ์ฒ๋ฆฌํ ์ ์์ด์ผํจ.
์ ์ฐํ ์์คํ ํ์ฅ : ํฅํ ํธ๋ํฝ ์ฆ๊ฐ์ ๋๋นํ์ฌ ์์คํ ์ ํ์ฅํ ์ ์์ด์ผํจ.
๊ธฐ์กด ๋๊ธฐ์ ๋ฐฉ์์ ์ฑํ ์ ์ฅ ๋ก์ง๊ณผ ์ฌ์ฉ์ ์๋ต ๋ก์ง์ด ๊ฐํ๊ฒ ๊ฒฐํฉ๋์ด ์์ด ์๋น์ค์ ์์ ์ฑ์ ๋ณด์ฅํ๊ธฐ ์ด๋ ต๋ค๊ณ ํ๋จํด ๋ฉ์ธ์ง ์ ์ฅ ๋ก์ง์ ๋น๋๊ธฐ์ ์ผ๋ก ์ฒ๋ฆฌํ ์ ์๋ ๋ฉ์ธ์ง ๋ธ๋ก์ปค๊ฐ ํ์ํ๋ค๊ณ ํ๋จํ์ต๋๋ค.
๊ธฐ์ ์คํ ๊ณ ๋ฏผ
- redis pub/sub : ์ธ๋ฉ๋ชจ๋ฆฌ๋ฅผ ์ฌ์ฉํด ๋น ๋ฅธ ์๋น์ค๋ฅผ ์ ๊ณตํ์ง๋ง ์์์ฑ์ ์ง์ํ์ง ์์ ๋ฉ์ธ์ง ์ ์ค ๊ฐ๋ฅ์ฑ์ด์์ต๋๋ค.
- ์นดํ์นด ๋์ ์ฒ๋ฆฌ๋๊ณผ ํ์ฅ์ฑ์ ๊ฐํ ๋ํ์ ์ธ ๋ฉ์ธ์ง ๊ธฐ์ ์คํ์ด์ง๋ง ์ด๋ฒ ์ฑํ ๊ธฐ๋ฅ์ ๊ฒฝ์ฐ ์๋ธ๋น์ฆ๋์ค๋ก์ง์ด๊ธฐ์ ์ค๋ฒ์คํฉ์ด๋ผ๊ณ ํ๋จํ์ต๋๋ค.
- rabbitMQ : ๋ฉ์ธ์ง ์ ์ก๋ณด์ฅ๊ณผ ์ ์ค๋ฐฉ์ง๋ฅผ ์ง์ํ์ง๋ง ์นดํ์นด๋๋น ๋ฎ์ ์ฒ๋ฆฌ๋, ๋ฉ์ธ์ง๊ฐ ํ์ ์์ฌ ๋ณ๋ชฉ์ด ๋ฐ์ํ์ง ์๋๋ก ๊ด๋ฆฌ ํ์ํฉ๋๋ค
- acitveMQ : ์ค๋๋ ์๋ฐ๊ธฐ๋ฐ์ jms ๋ธ๋ก์ปค๋ก ์๋ฐ์ํ๊ณ์ ํนํ๋์ด์๊ณ ์ฐ๋์ด ์ฝ์ต๋๋ค.
- ๊ฒฐ๋ก ๋ค๊ฐ์ ๋ฉ์ธ์ง ๊ธฐ์ ์ค rabbitMQ์ activeMQ๋ฅผ ๊ณ ๋ฏผํ์ต๋๋ค. ์๋ฐ๊ธฐ๋ฐ์ ์ธ์ด์ธ activeMQ๊ฐ ํธํ์ฑ ๋ฌธ์ ๋ฑ ์ ํฉํ๋ค๊ณ ๋ ์๊ฐํ์ง๋ง, ์คํ๋ ค ์๋ฐ์ ๋ํ ๋์ ์ข ์์ฑ์ด ์ถํ ํ์ฅ์ ๊ณ ๋ คํ์ ๋ ๊ฒฐํฉ๋๋ฅผ ๋์ผ ์ ์๋ค๊ณ ์๊ฐํ์ต๋๋ค.
์ด๋ฒ ๊ณ ๋ฏผ์ ๊ฐ์ฅ ๊ทผ๋ณธ์ ์ธ ๋ถ๋ถ์ธ ์ ์ฐ์ฑ๊ณผ ํ์ฅ์ฑ, ๋์จํ ๊ฒฐํฉ์ด๋ผ๋ ๊ด์ ์์ ๋ณด์์๋ java message service ๊ธฐ๋ฐ์ธ activeMQ๋ณด๋ค๋ AMPQ๊ธฐ๋ฐ์ RabbitMQ๋ฅผ ์ ํํ๋๊ฒ ๊ณ ๋ฏผํ๋ ๋ถ๋ถ์ ํด์ํ ์ ์๋ค๊ณ ํ๋จํ์ต๋๋ค.
์ปจํธ๋กค๋ฌ ๋ ์ด์ด์์ ์๋น์ค๋ ์ด์ด์ ๋น์ฆ๋๋ก์ง์ ํธ์ถํ๋๊ฒ์ด ์๋ RabbitTemplate๊ณผ MessageTemplate์ผ๋ก ๋ฐํํ๊ธฐ๋ง ํ๊ณ , ์๋น์ค๋ ์ด์ด์์๋ @RabbitListener ์ด๋ ธํ ์ด์ ์ผ๋ก ๋น๋๊ธฐ์ ์ผ๋ก ์๋น ํ ์ผ๊ด์ ์ผ๋ก ์ ์ฅํฉ๋๋ค.
์ด๋ฌํ ๋ฉ์ธ์ง ๋ธ๋ก์ปค ๊ธฐ๋ฐ์ ๊ตฌ์กฐ๋ ์ค์ผ์ผ ์์์ ์ ๋ฆฌํ์ฌ ๋ฉ์ธ์ง ์ฒ๋ฆฌ๋์ ๋๋ ค์ผ ํ ๊ฒฝ์ฐ ๋จ์ํ @RabbitListener๋ฅผ ์ฌ์ฉํ๋ ์๋น์ค ์๋ฒ๋ฅผ ์ถ๊ฐํ์ฌ ๋ถํ๋ฅผ ๋ถ์ฐํจ์ผ๋ก์จ ์ฌ๋ฌ ์๋ฒ๊ฐ ๋ฉ์ธ์ง๋ฅผ ๋ถ์ฐํ๊ฒฝ์์ ์ฒ๋ฆฌ ํ ์ ์๋๋ก ํด์ค๋๋ค.
//controller
@MessageMapping("/chatroom/{roomId}")
public void sendMessageV2(
@DestinationVariable Long roomId,
@Valid @Payload ChatMessageRequest request,
CustomPrincipal principal
) {
if (principal == null) {
throw new CustomException(ExceptionCode.NOT_FOUND_USER);
}
MessageQueue messageQueue = MessageQueue.of(request.getContent(), principal.getId(), roomId, LocalDateTime.now());
rabbitTemplate.convertAndSend(CHAT_QUEUE_NAME, messageQueue);
messagingTemplate.convertAndSend("/sub/chatroom/" + roomId, messageQueue);
}
//service
@RabbitListener(queues = "chat.queue")
public void consumeAndBufferMessage(MessageQueue request) {
messageBuffer.add(request);
log.info("๋ฒํผ์ ๋ฉ์ธ์ง ์ถ๊ฐ: {}, ๋ด๊ธด๋ฉ์ธ์ง ๊ฐ์: {}", request.getContent(), messageBuffer.size());
}
@Scheduled(fixedDelay = 20000)
public void saveMessageFromBuffer() {
if(messageBuffer.isEmpty()) {
return;
}
List<MessageQueue> messageSave = new ArrayList<>(messageBuffer);
messageBuffer.clear();
List<ChatMessage> chatMessages = messageSave.stream()
.map(this::createChatMessageFromQueue)
.toList();
chatMessageRepository.saveAll(chatMessages);
log.info("๋ฒํผ์ ์๋ ๋ฉ์ธ์ง {} ๊ฐ 20์ด๋ง๋ค ์ผ๊ด์ ์ฅ", chatMessages.size());
}
ํธ๋ํฝ ์ฆ๊ฐ ์ RabbitMQ ํ์ ๋ฉ์์ง๊ฐ ์์ฌ ๋ณ๋ชฉ ํ์์ด ๋ฐ์ํ์ง ์๋๋ก ํ์ ์ํ ๋ชจ๋ํฐ๋ง์์คํ ๊ตฌ์ถ์ด ํ์ํ ์ ์์ต๋๋ค.
๋ฉ์์ง ์ ์ก ์คํจ ์ ์ฌ์๋ ๋ก์ง์ด๋ ๋ฐ๋ ๋ ํฐ ํ(Dead Letter Queue)๋ฅผ ํ์ฉํ๋ ๋ฐฉ์์ ๊ณ ๋ฏผํด์ผ ํฉ๋๋ค.
๐กย Pinpoint APM ๋์
ํ๋ก์ ํธ๊ฐ ๊ธฐ๋ฅ ๊ตฌํ ๋จ๊ณ๋ฅผ ์๋ฃํ๊ณ ์ฑ๋ฅ ์ต์ ํ ๋จ๊ณ์ ์ง์ ํ๋ฉด์, ๊ธฐ์กด์ ์ฑ๋ฅ ํ ์คํธ ๋ฐฉ์์ ํ๊ณ๊ฐ ๋๋ฌ๋ฌ์ต๋๋ค. JMeter๋ฅผ ํ์ฉํ ๋ถํ ํ ์คํธ๋ฅผ ํตํด ์ฑ๋ฅ ๋ณ๋ชฉ ํ์์ ํ์ธํ ์ ์์ง๋ง, ์ค์ ์ด๋ค ๊ตฌ๊ฐ์์ ์ง์ฐ์ด ๋ฐ์ํ๋์ง ๊ตฌ์ฒด์ ์ธ ์์ธ์ ํ์ ํ๊ธฐ ์ด๋ ค์ด ์ํฉ์ด์์ต๋๋ค.
๋ก๊น ์์คํ ์ ํตํด ์ผ๋ถ ์ถ์ ์ด ๊ฐ๋ฅํ๋, ๋ค์๊ณผ ๊ฐ์ ๋ฌธ์ ์ ๋ค๋ก ์ธํด ์๋ก ๋์ ํ๊ธฐ๋ก ํ์ต๋๋ค.
- ๋ก๊ทธ ๋ถ์์ ๋ง์ ์๊ฐ์ด ์์๋จ
- ์ ์ฒด ์์ฒญ ํ๋ก์ฐ์์ ๋ณ๋ชฉ ๊ตฌ๊ฐ์ ํน์ ํ๊ธฐ ์ด๋ ค์
- ๋ฆฌ์์ค ์ฌ์ฉ๋ฅ ๊ณผ ์ฑ๋ฅ ์งํ๋ฅผ ์ค์๊ฐ์ผ๋ก ํ์ธํ ์ ์์
- ์ฑ๋ฅ ๊ฐ์ ์ ํ ๋น๊ต ๋ถ์์ ์ํ ์ ๋์ ์งํ ๋ถ์กฑ
์ค๊ณ ๊ฑฐ๋ ํ๋ซํผ์ ์ํ ๊ฒ์, ์์ธ ์กฐํ, ์ฑํ , ๊ฒฐ์ ๋ฑ ์ฌ๋ฌ ๊ธฐ๋ฅ์ด ํจ๊ป ์๋ํ๊ธฐ ๋๋ฌธ์, ์ฑ๋ฅ ๋ณ๋ชฉ์ ์ ํํ ํ์ ํ ์ ์๋ ๋๊ตฌ๊ฐ ํ์ํ๋ค๊ณ ๋๊ผ์ต๋๋ค.
๊ธฐ๋ฅ์ ์๊ตฌ์ฌํญ
- ์์ฒญ ์ถ์ : ์ฌ์ฉ์ ์์ฒญ๋ถํฐ ์๋ต๊น์ง์ ์ ์ฒด ํ๋ก์ฐ ์ถ์
- ๋ณ๋ชฉ ์ง์ ์๋ณ: ๊ฐ ๋ฉ์๋์ ์๋น์ค ๋ ์ด์ด๋ณ ์๋ต ์๊ฐ ์ธก์
- ์ค์๊ฐ ๋ชจ๋ํฐ๋ง: CPU, ๋ฉ๋ชจ๋ฆฌ ๋ฑ ์์คํ ๋ฆฌ์์ค ์ฌ์ฉ๋ฅ ์ค์๊ฐ ํ์ธ
- ์ค๋ฅ ์ถ์ : ์์ธ ๋ฐ์ ์์ ๊ณผ ์คํ ํธ๋ ์ด์ค ์์ธ ์ ๋ณด ์ ๊ณต
- ์ฑ๋ฅ ์งํ ์๊ฐํ: ์๋ต ์๊ฐ, TPS, ์ฒ๋ฆฌ๋ ๋ฑ ์ฃผ์ ์งํ์ ๊ทธ๋ํ ํ์
๋น๊ธฐ๋ฅ์ ์๊ตฌ์ฌํญ
- ๋ฎ์ ์ค๋ฒํค๋: ์ด์ ํ๊ฒฝ์์๋ ์ต์ํ์ ์ฑ๋ฅ ์ํฅ
- ๊ฐํธํ ์ค์น ๋ฐ ์ค์ : ๋ณต์กํ ์ค์ ์์ด ๋น ๋ฅธ ๋์ ๊ฐ๋ฅ
- ์คํ์์ค: ๋ผ์ด์ ์ค ๋น์ฉ ๋ถ๋ด ์์ด ํ์ฉ ๊ฐ๋ฅ
- ํ๊ตญ์ด ์ง์: ๊ตญ๋ด ๊ฐ๋ฐ ํ๊ฒฝ์ ์ต์ ํ๋ ๋ฌธ์์ ์ปค๋ฎค๋ํฐ
์ฌ๋ฌ APM ๋๊ตฌ๋ฅผ ๋น๊ต ๊ฒํ ํ ๊ฒฐ๊ณผ Pinpoint๋ฅผ ์ ํํ์ต๋๋ค.
์ ํ ์ด์ :
- ๋ค์ด๋ฒ์์ ๊ฐ๋ฐํ ์คํ์์ค๋ก ํ๊ตญ์ด ๋ฌธ์์ ์ปค๋ฎค๋ํฐ ์ง์์ด ํ๋ถ
- Java ์ ํ๋ฆฌ์ผ์ด์ ์ ํนํ๋์ด Spring Boot ํ๋ก์ ํธ์์ ํธํ์ฑ์ด ์ฐ์
- ์์ด์ ํธ ๋ฐฉ์์ผ๋ก ์ฝ๋ ์์ ์์ด ๋์ ๊ฐ๋ฅ
- ์์ธํ ํธ๋ ์ด์ฑ: ๋ฉ์๋ ๋ ๋ฒจ๊น์ง ์ธ๋ฐํ ์ฑ๋ฅ ์ถ์ ์ ๊ณต
- ์ง๊ด์ ์ธ UI: ์๋น์ค ๋งต๊ณผ ์ฑ๋ฅ ์ฐจํธ๋ฅผ ํตํ ์๊ฐ์ ๋ถ์ ๊ฐ๋ฅ
๋ค๋ฅธ ๋๊ตฌ ๋๋น ์ฅ์ :
- Zipkin: ๋ ์์ธํ ๋ฉํธ๋ฆญ ์ ๊ณต
- New Relic: ๋ฌด๋ฃ๋ก ์ฌ์ฉ ๊ฐ๋ฅ, ์์ธํ ํ๊ตญ์ด ๋ฌธ์
๋จ๊ณ์ ๋์
- 1๋จ๊ณ: ๋ก์ปฌ ํ๊ฒฝ์์ Docker๋ก Pinpoint๋ฅผ ๊ตฌ์ฑํ์ฌ ์ฑ๋ฅ ๋ถ์ ์ฉ๋๋ก๋ง ํ์ฉ
- 2๋จ๊ณ: ๋ก์ปฌ์์์ ์์ ์ฑ ํ์ธ ํ EC2 ์๋ฒ์ ๊ธฐ์กด ์ปจํ ์ด๋์ ํตํฉ ๋ฐฐํฌ
- 3๋จ๊ณ: ์๋ ๊ณ ๋ ค์ฌํญ์ผ๋ก ์ธํด Pinpoint ์ ์ฉ ์ปจํ ์ด๋๋ก ๋ถ๋ฆฌํ์ฌ ๋ ๋ฆฝ ์ด์
๊ณ ๋ ค์ฌํญ
์ฒ์์๋ ์ ํ๋ฆฌ์ผ์ด์ ๊ณผ Pinpoint๋ฅผ ๋์ผ ์ปจํ ์ด๋์ ๋ฐฐ์นํ๋ ค ํ์ผ๋, ๋ฉ๋ชจ๋ฆฌ ๋ถ์กฑ ํ์์ด ๋ฐ์ํ๋ ๊ฒ์ ํ์ธํ์ต๋๋ค. ๋ํ, ๋ชจ๋ํฐ๋ง ์์คํ ๊ณผ ์๋ฒ๊ฐ ํ ์ปจํ ์ด๋์ ํจ๊ป ์์ ๊ฒฝ์ฐ, ํ ์ปจํ ์ด๋๊ฐ ๋ฌธ์ ๊ฐ ์๊ธฐ๋ฉด ๋ชจ๋ํฐ๋ง๊น์ง ์ํฅ์ ๋ฐ์ ์ ์๋ค๋ ์ ์ ๊ณ ๋ คํ์ฌ, ์ปจํ ์ด๋๋ฅผ ๋ถ๋ฆฌํ์ฌ ๊ตฌ์ฑํ๊ธฐ๋ก ๊ฒฐ์ ํ์ต๋๋ค.
์ํคํ ์ฒ
Pinpoint ์ํคํ
์ฒ
์ธ์คํด์ค 1์์ ๊ตฌ๋๋๋ Spring ์ฑ์ Pinpoint Agent๋ฅผ ๋ถ์ฐฉํ์ฌ ํธ๋์ญ์
๊ณผ ์ฑ๋ฅ ๋ฐ์ดํฐ๋ฅผ ์์งํ๊ณ , ์ด๋ฅผ ์ธ์คํด์ค 2์ Pinpoint Collector๋ก ์ ์กํฉ๋๋ค. Collector๋ ์์ง๋ ๋ฐ์ดํฐ๋ฅผ HBase์ ์ ์ฅํ๋ฉฐ, HBase์ ์ ์ฅ๋ ๋ฐ์ดํฐ๋ Pinpoint Web์ ํตํด ์๊ฐํ๋์ด ์น ๋์๋ณด๋๋ก ์ ๊ณต๋ฉ๋๋ค.
Pinpoint ๊ตฌ์ถ ๋ฐ ์ฐ๊ฒฐ ์์
- ์คํ๋ง ์ฑ ์ปจํ
์ด๋(์ปจํ
์ด๋ 1)
- Pinpoint Agent๋ฅผ ๋ถ์ฐฉ (VM ํ๊ฒฝ๋ณ์ ์ค์ )
// ์์
java -javaagent:/home/ubuntu/pinpoint-agent-2.5.3/pinpoint-bootstrap.jar \
-Dpinpoint.agentId=mania001 \
-Dpinpoint.applicationName=mania_place.dev \
-Dpinpoint.config=/home/ubuntu/pinpoint-agent-2.5.3/profiles/release/pinpoint.config \
-jar Mania-Place-0.0.1-SNAPSHOT.jar
- Pinpoint ์๋ฒ ์ปจํ
์ด๋(์ปจํ
์ด๋ 2)
- Docker๋ก Pinpoint๋ฅผ ๊ตฌ๋ํ๊ณ Collector, HBase, Web ์ค๋น
- ์ปจํ
์ด๋ ๊ฐ ๋คํธ์ํฌ ์ค์
- ํ๋ผ์ด๋น IP ๊ธฐ๋ฐ ํต์ ๊ตฌ์ฑ
- ํฌํธ 8081๋ก ์ธ๋ฐ์ด๋/์์๋ฐ์ด๋ ํธ๋ํฝ ํ์ฉ
๊ฐ์ ๋ฐ ํ์ฅ ๊ณํ
- ์๋ฆผ ์์คํ ๊ตฌ์ถ: ์ฑ๋ฅ ์๊ณ์น ์ด๊ณผ ์ ์๋ ์๋ฆผ ์ค์
- ๋ชจ๋ํฐ๋ง ๋๊ตฌ ํ์ฅ: Prometheus, Grafana์์ ํตํฉ ๋ชจ๋ํฐ๋ง ๊ตฌ์ถ
์ํ ์์ ๋ฐ ๋์ ๋ฐฉ์
์ฃผ์ ์ํ ์์:
- Agent ์ค๋ฒํค๋๋ก ์ธํ ์ฑ๋ฅ ์ํฅ
- HBase ์ ์ฅ์ ์ฉ๋ ๊ด๋ฆฌ ์ด์
๋์ ๋ฐฉ์:
- ๊ฐ๋ฐ ํ๊ฒฝ์์ ์ถฉ๋ถํ ๊ฒ์ฆ ํ ๋จ๊ณ์ ํ๋
- ๋ฐ์ดํฐ ๋ณด์กด ๊ธฐ๊ฐ ์ ์ฑ ์๋ฆฝ์ผ๋ก ์ฉ๋ ๊ด๋ฆฌ
โก๏ธ ์ธ๊ธฐ ๊ฒ์์ด ๋ญํน
Mania Place์ ์ธ๊ธฐ ๊ฒ์์ด ๋ญํน ๊ธฐ๋ฅ์
์ง๋ 24์๊ฐ ๋์ ์ฌ์ฉ์๋ค์ด ๊ฐ์ฅ ๋ง์ด ๊ฒ์ํ ํค์๋๋ฅผ ์ค์๊ฐ์ผ๋ก ์ง๊ณํ์ฌ ๋ณด์ฌ์ฃผ๋ ๊ธฐ๋ฅ์ ๋๋ค.
์ฌ์ฉ์๋ ์ต์ ํธ๋ ๋๋ฅผ ๋น ๋ฅด๊ฒ ํ์ ํ ์ ์๊ณ , ํ๋งค์๋ ์์๊ฐ ๋์ ์ํ์ ์์ธกํ์ฌ ํ๋งค ์ ๋ต์ ์ธ์ธ ์ ์์ต๋๋ค.
๊ธฐ์กด ์์คํ ์์๋ ์ธ๊ธฐ ๊ฒ์์ด ์ง๊ณ๊ฐ ๋๋ฆฌ๊ณ ์ ํ๋๊ฐ ๋จ์ด์ก์ต๋๋ค.
- ๊ฒ์์ด ๋ฐ์ดํฐ๊ฐ DB์๋ง ์ ์ฅ๋์ด, ์ค์๊ฐ ์ง๊ณ๊ฐ ์ด๋ ต์ต๋๋ค.
- 24์๊ฐ ๊ธฐ์ค ์ง๊ณ ๋ก์ง์ด ๋ณต์กํ๊ณ , ๊ณ์ฐ ๋น์ฉ์ด ๋์ต๋๋ค.
- ํธ๋ํฝ์ด ๊ธ์ฆํ๋ฉด DB ์ปค๋ฅ์ ์ด ๊ณ ๊ฐ๋์ด ์์คํ ์๋ต ์๋๊ฐ ์ ํ๋ฉ๋๋ค.
- Redis ZSet์ ํ์ฉํ์ฌ ์ค์๊ฐ ๊ฒ์์ด ์ ์ ์ง๊ณ
- 1์๊ฐ ๋จ์ ์ค๋ ์ท์ ํตํด ๊ณผ๊ฑฐ 24์๊ฐ ๋ฐ์ดํฐ๋ฅผ ํจ์จ์ ์ผ๋ก ๊ณ์ฐ
- ํค๋ณ ์ ์ ํฉ์ฐ ๋ฐ ์ ๋ ฌ๋ก ์์ N๊ฐ ๊ฒ์์ด๋ฅผ ๋น ๋ฅด๊ฒ ์กฐํ
- TPS (์ด๋น ์ฒ๋ฆฌ๋): 113.5 / sec
- Latency (ํ๊ท ์๋ต ์๊ฐ): 4,030 ms
- Error Rate (์๋ฌ์จ): 0 %
- ๋์ ์๋ต ์ง์ฐ ์๊ฐ (High Response Latency): ํ๊ท ์๋ต ์๊ฐ์ด 4์ด(4,030ms)๋ฅผ ์ด๊ณผํฉ๋๋ค. ์ด๋ ์ค์๊ฐ์ฑ์ด ์ค์ํ ๋ญํน ์๋น์ค์์ ์ฌ์ฉ์๊ฐ ์ฌํ ์ง์ฐ์ ์ฒด๊ฐํ ์ ์๋ ์์ค์ ๋๋ค.
- ํ์ฅ์ฑ ํ๊ณ (Scalability Limitation): ์ด๋น ์ฝ 113๊ฑด์ ์์ฒญ๋ง ์ฒ๋ฆฌ ๊ฐ๋ฅํด, ํฅํ ์ฌ์ฉ์ ์ฆ๊ฐ๋ ์ด๋ฒคํธ ๋ฐ์ ์ ๊ธ์ฆํ๋ ํธ๋ํฝ์ ๊ฐ๋นํ์ง ๋ชปํ๊ณ ์๋น์ค ์ฅ์ ๋ก ์ด์ด์ง ์ํ์ด ๋์ต๋๋ค.
- TPS (์ด๋น ์ฒ๋ฆฌ๋): 132.6 / sec
- Latency (ํ๊ท ์๋ต ์๊ฐ): 2,393 ms
- Error Rate (์๋ฌ์จ): 0 %
DB ๋ถํ๋ฅผ Redis๋ก ์ด์ ํ์ฌ 1์ฐจ์ ์ธ ์์คํ ์์ ์ฑ ํ๋ณด์๋ ์ฑ๊ณตํ์ง๋ง, ์๋ต ์๋ ์ธก๋ฉด์์๋ ์ถ๊ฐ ๊ฐ์ ์ด ํ์ํ ์ํ์ ๋๋ค.
- 1์ฐจ ๋ชฉํ ๋ฌ์ฑ (ํ์ฅ์ฑ ํ๋ณด): ์ฒ๋ฆฌ๋(TPS)์ 17% ์ฆ๊ฐํ๊ณ ์๋ต ์๊ฐ์ 41% ๋จ์ถ๋์ด, ๋๊ท๋ชจ ํธ๋ํฝ์ ์๋ฌ ์์ด ์์ ์ ์ผ๋ก ์ฒ๋ฆฌํ ์ ์๋ ํ์ฅ์ฑ์ ํ๋ณดํ์ต๋๋ค.
- ํฅํ ๊ณผ์ (์๋ต ์๋ ์ต์ ํ): 1์ฐจ ๋ชฉํ๋ ๋ฌ์ฑํ์ผ๋, 2.4์ด ์์ค์ ์๋ต ์๊ฐ์ ์ค์๊ฐ ์๋น์ค์์ ๋ ๊ฐ์ ๋ ํ์๊ฐ ์์ต๋๋ค. ๋ค์ ๋จ๊ณ๋ก, Redis์ ์ค์๊ฐ ๋ฐ์ดํฐ ์ง๊ณ ๋ฐฉ์์ ์ถ๊ฐ ์ต์ ํํ์ฌ ์๋ต ์๊ฐ์ 1์ด ๋ฏธ๋ง์ผ๋ก ๋จ์ถํ๋ ๊ฒ์ ๋ชฉํ๋ก ํฉ๋๋ค.
- Redis ZSet ํ์ฉ ์ค์๊ฐ ์ง๊ณ
- ์๊ฐ๋ณ Redis ํค(
keyword_rankings:yyyy-MM-dd-HH)๋ก ๊ฒ์์ด ์ ์ ๋์ - ๊ฐ ํค๋ 25์๊ฐ TTL ์ ์ฉ โ ์๋ ๋ง๋ฃ๋ก ์ค๋๋ ๋ฐ์ดํฐ ์ ๊ฑฐ
- ์๊ฐ๋ณ Redis ํค(
- ๊ฒ์์ด ์ถ๊ฐ
- ์ ๋ ฅ ํค์๋๋ ์ค์๊ฐ์ผ๋ก ํด๋น ์๊ฐ๋ Redis ZSet์ ์ ์ 1์ฉ ์ฆ๊ฐ
- 24์๊ฐ ๋ญํน ์กฐํ
- ํ์ฌ ์๊ฐ๋ถํฐ 23์๊ฐ ์ ๊น์ง ์ด 24๊ฐ์ ์๊ฐ๋ณ Redis ํค ์กฐํ
- ZUNIONSTORE๋ฅผ ํ์ฉํ์ฌ 24์๊ฐ ํค์ ์ ์๋ฅผ Redis์์ ๋ฐ๋ก ํฉ์ฐ
reverseRangeWithScores๋ก ์ ์ ๋ด๋ฆผ์ฐจ์ ์กฐํ โ ์์ N๊ฐ ๋ฐํ- 0 ์ดํ ์ ์๋ ๋ญํน์์ ์ ์ธ
- ์ค์ผ์ค๋ฌ ๊ธฐ๋ฐ DB ์ค๋
์ท ์ ์ฅ (์์ ์ฑ ํ๋ณด)
- ๋งค ์ ์(๋งค์๊ฐ 0๋ถ 0์ด) Redis์์ ํค์๋ ์ ์ ์กฐํ โ DB ์ค๋ ์ท ํ ์ด๋ธ ์ ์ฅ
- Redis ์๋ฒ๊ฐ ๋ค์ด๋๋๋ผ๋ ์ค๋ ์ท ๋ฐ์ดํฐ๋ฅผ ๊ธฐ์ค์ผ๋ก ์ธ๊ธฐ ๊ฒ์์ด ์กฐํ ๊ธฐ๋ฅ์ด ์ ์ง
- Redis ์๋ฒ ๋ณต๊ตฌ ํ์๋ ์ค๋ ์ท ๋ฐ์ดํฐ๋ฅผ ์ด์ฉํด ์ ์ ์๋น์ค ๋ณต๊ตฌ ๊ฐ๋ฅ
- ๋ฐ์ดํฐ ์ ๋ฆฌ
- 30์ผ ์ด์ ๋ DB ์ค๋ ์ท ๋ฐ์ดํฐ ์ฃผ๊ธฐ์ ์ญ์ ๋ก ์ฉ๋ ๊ด๋ฆฌ
1. DB ์ค๋ ์ท์ ํ์ฉํ ์ฅ์ ๋์ ๋ฅ๋ ฅ ๊ฐํ
ํ์ฌ ๋งค์๊ฐ ์์ฑํ๊ณ ์๋ DB ์ค๋ ์ท์ ์ฌํด ๋ณต๊ตฌ ๋ฐ ์๋น์ค ์ฐ์์ฑ ๋ณด์ฅ์ ์ ๊ทน์ ์ผ๋ก ํ์ฉํ์ฌ ์์คํ ์ ์์ ์ฑ์ ํ ๋จ๊ณ ๋์ด์ฌ๋ฆฝ๋๋ค.
- Fallback ๋ก์ง ๋์ : Redis ์ฅ์ ๊ฐ ๋ฐ์ํ๋๋ผ๋ ์๋น์ค๊ฐ ์ค๋จ๋์ง ์๋๋ก, ๊ฐ์ฅ ์ต์ DB ์ค๋ ์ท์ ์กฐํํ์ฌ ์ธ๊ธฐ ๊ฒ์์ด ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ Fallback(๋์ฒด ์๋) ๋ก์ง์ ๊ตฌํํฉ๋๋ค. ์ด๋ฅผ ํตํด ๋ฐ์ดํฐ ์ ์ค์ ์ต๋ 1์๊ฐ ์ด๋ด๋ก ์ ํํ๊ณ ์๋น์ค ๋ค์ดํ์์ ์ต์ํํฉ๋๋ค.
- ์ ์ํ ๋ณต๊ตฌ ์ง์: ์ฅ์ ๋ก๋ถํฐ Redis ์๋ฒ๊ฐ ๋ณต๊ตฌ๋์์ ๋, **DB์ ์ต์ ์ค๋ ์ท ๋ฐ์ดํฐ๋ฅผ Redis์ ์๋์ผ๋ก ๋ค์ ์ ์ฌ(Cache Warming)**ํ๋ ํ๋ก์ธ์ค๋ฅผ ๊ตฌ์ถํ์ฌ ๋น ๋ฅด๊ณ ์์ ์ ์ผ๋ก ์๋น์ค๋ฅผ ์ ์ํํฉ๋๋ค.
2. ๋ญํน ์กฐํ API ์ฑ๋ฅ ์ต์ ํ
ํ์ฌ 2.4์ด(2,393ms) ์์ค์ธ ์ธ๊ธฐ ๊ฒ์์ด ์กฐํ API์ ์๋ต ์๊ฐ์ 1์ด ๋ฏธ๋ง์ผ๋ก ๋จ์ถํ์ฌ, ์ฌ์ฉ์์๊ฒ ์ค์๊ฐ์ ๊ฐ๊น์ด ๊ฒฝํ์ ์ ๊ณตํ๋ ๊ฒ์ ๋ชฉํ๋ก ํฉ๋๋ค.
- ์ค์๊ฐ ์ง๊ณ ๋ก์ง ๊ฐ์ : ๋ชจ๋ ์ฌ์ฉ์ ์์ฒญ ์๋ง๋ค 24๊ฐ์ ํค๋ฅผ
ZUNIONSTORE๋ก ์ง๊ณํ๋ ํ์ฌ ๋ฐฉ์ ๋์ , ๋ณ๋์ ์ค์ผ์ค๋ฌ๊ฐ ๋ญํน ๊ฒฐ๊ณผ๋ฅผ ์ฃผ๊ธฐ์ ์ผ๋ก ๋ฏธ๋ฆฌ ๊ณ์ฐํ์ฌ ์บ์ํด๋๋ ๋ฐฉ์์ผ๋ก ๋ณ๊ฒฝํ์ฌ ์กฐํ ์๋๋ฅผ ํ๊ธฐ์ ์ผ๋ก ๊ฐ์ ํฉ๋๋ค.
3. ๊ฒ์ ๊ด๋ จ ๊ธฐ๋ฅ ํ์ฅ
์์ ํ๋ ๋ญํน ์์คํ ์ ๊ธฐ๋ฐ์ผ๋ก ์ฌ์ฉ์์๊ฒ ๋ ํ๋ถํ ๊ฐ์น๋ฅผ ์ ๊ณตํ๊ธฐ ์ํ ์ ๊ท ๊ธฐ๋ฅ ํ์ฅ์ ๊ฒํ ํฉ๋๋ค.
- ์ฐ๊ด ๊ฒ์์ด ์ถ์ฒ: ์ฌ์ฉ์๊ฐ ํน์ ํค์๋๋ฅผ ๊ฒ์ํ์ ๋, ์ฐ๊ด์ฑ์ด ๋์ ๋ค๋ฅธ ํค์๋๋ฅผ ํจ๊ป ์ถ์ฒํ์ฌ ํ์์ ํธ์์ฑ์ ๋์ ๋๋ค.
- ์๊ฐ๋๋ณ ํธ๋ ๋ ๋ถ์: ์๊ฐ์ ํ๋ฆ์ ๋ฐ๋ฅธ ํน์ ํค์๋์ ์ธ๊ธฐ๋ ๋ณํ๋ฅผ ์๊ฐ์ ์ผ๋ก ๋ณด์ฌ์ฃผ์ด, ์ฌ์ฉ์์ ํ๋งค์ ๋ชจ๋์๊ฒ ์ ์ฉํ ์ธ์ฌ์ดํธ๋ฅผ ์ ๊ณตํฉ๋๋ค.
โก๏ธย ์บ์ฑ์ ์ด์ฉํ์ฌ ์์์ ์กฐํ๋ฅผ ๋์ฑ ๋น ๋ฅด๊ฒ!
์์์์ ๋ฐ๋งค ์์ ์ธ ๊ตฟ์ฆ๋ ์ด๋ฒคํธ ๊ฐ์ ๊ณต์์ ์ธ ์์์ ์ฌ์ฉ์์๊ฒ ์๋ฆฌ๋ ๊ณต๊ฐ์ผ๋ก,
์ด์์๊ฐ ๊ฒ์๊ธ์ ์ฌ๋ฆฌ๋ฉด ์ฌ์ฉ์๊ฐ ์กฐํํฉ๋๋ค. ์ ๊ธ ์ ๋ฐ์ดํธ ์ ์ฌ์ฉ์์๊ฒ ์ด๋ฉ์ผ์ด ๋ฐ์ก๋ฉ๋๋ค.
๊ณต์ ์์์ด ์ฌ๋ผ์ค๋ ์ฐฝ๊ตฌ์ด๋ฏ๋ก ์ฌ์ฉ์๋ ์ ๊ท ์์์์ ๋งค์ฐ ๋ฏผ๊ฐํ๊ฒ ๋ฐ์ํฉ๋๋ค.
๋ฐ๋ผ์ ๋ฉ์ผ ์๋ฆผ์ ๋ฐ์ ๋ค์์ ์ฌ์ฉ์๊ฐ ์ฆ์ ์ ์ํ ๊ฐ๋ฅ์ฑ์ด ๋์, ์ด์ ๋ฐ๋ฅธ ์์คํ ์ฑ๋ฅ ์ ํ๊ฐ ๋ฐ์ํฉ๋๋ค.
- ๊ด๋ฆฌ์๋ง ์์์ ๋ฑ๋ก, ์์ , ์ญ์ ์ ๊ถํ์ด ์์ผ๋ฉฐ, ์์์ ๋ฑ๋ก์ ๋น๋ฒํ์ง ์๊ณ ์์ ๋ฐ ์ญ์ ๋ ๋งค์ฐ ๋๋ญ ๋๋ค.
- ๊ณต์ง ์ฑ๊ฒฉ์ ๋ง์ ์ฌ์ฉ์๊ฐ ๊ฐ์ ๋ฐ์ดํฐ๋ฅผ ์งง์ ์๊ฐ์ ๋ฐ๋ณต์ ์ผ๋ก ์กฐํํฉ๋๋ค.
- ์์์์ ์ ์ฒด ์กฐํ๋ง ๊ฐ๋ฅํ๋ฉฐ, ์ฌ์ฉ์๋ค์ด ๋ค์ํ ์์์ ๊ณจ๊ณ ๋ฃจ ์ ํ ์ ์๋๋ก ๊ฐ์ธํ๋ ๊ฒ์ ๊ธฐ๋ฅ์ ์ ํํ๊ณ ์์ต๋๋ค.
- ๋ณ๊ฒฝ ์ฃผ๊ธฐ๋ ๋ฎ์ง๋ง ์กฐํ ๋น๋๊ฐ ๋์ ๋ฐ์ดํฐ์ด๋ฏ๋ก, ๋งค๋ฒ DB์์ ์กฐํํ๋ ๋์ ์บ์์ ์ ์ฅํ๋ฉด ์๋ต ์๋๋ฅผ ๋์ด๊ณ ์๋ฒ ๋ถํ๋ฅผ ํฌ๊ฒ ์ค์ผ ์ ์๋ค๊ณ ํ๋จํ์ต๋๋ค.
โ๏ธ ํ ์คํธ ์กฐ๊ฑด
[ํ๊ฒฝ]
- ๋ก์ปฌ ํ๊ฒฝ์์ ํ ์คํธ: ์นฉ Apple M1 | ๋ฉ๋ชจ๋ฆฌ 16GB
- JMeter ์ฌ์ฉ
- ๊ฒ์๊ธ 5000๊ฑด ์กด์ฌ
[์ค๋ ๋ ์์ฑ]
- ์ฌ์ฉ์ ์: 500๋ช
- Ramp-up ์๊ฐ: 1์ด
- ๋ฃจํ ์นด์ดํธ: 10ํ
[๋์]
- ์บ์ฑ ์ ์์์ ์ ์ฒด ์กฐํ
- ์ธ๋ฉ๋ชจ๋ฆฌ ์บ์ฑ ์ ์ฉ ํ ์์์ ์ ์ฒด์กฐํ
- Caffeine ์บ์ฑ ์ ์ฉ ํ ์์์ ์ ์ฒด์กฐํ
- Redis ์บ์ฑ ์ ์ฉ ํ ์์์ ์ ์ฒด์กฐํ
โ๏ธ ํ ์คํธ ๋ชฉ์
- ๋ก์ปฌ ํ๊ฒฝ์์ 500๋ช ๋์ ์ฌ์ฉ์ ๋ถํ๋ฅผ ์๋ฎฌ๋ ์ด์ ํด ๋ค์ํ ์บ์ฑ ๊ธฐ๋ฒ ์ ์ฉ ์ ํ์ ์์์ ์กฐํ ์ฑ๋ฅ๊ณผ ์์ ์ฑ์ ๋น๊ต
- ์๋น์ค ํ๊ฒฝ์์ ์ ์ฉ ๊ฐ๋ฅ์ฑ์ด ๋์ ์บ์ ์๋ฃจ์ ์ ๋์ถ
โ๏ธ ํ ์คํธ ๊ฒฐ๊ณผ
[Response Times Over Time(์๊ฐ ๊ฒฝ๊ณผ๋ณ ์๋ต ์๊ฐ)]
|
|
|
| [๊ฐ์ ์ ] : 3,000ms ์ด์ ๊ตฌ๊ฐ์์ ์๋
(y์ถ: 04000ms/ x์ถ: 033s) | [์ธ๋ฉ๋ชจ๋ฆฌ ์บ์ฑ ์ ์ฉ] : 500660ms ๋ฒ์์์ ์์ 660ms/ x์ถ: 0
(y์ถ: 07s) |
| 600ms ๋ฒ์
|
|
| [์นดํ์ธ ์บ์ฑ ์ ์ฉ] : 500
(y์ถ: 0600ms/ x์ถ: 06s) | [๋ ๋์ค ์บ์ฑ ์ ์ฉ] : 550700ms ๋ฒ์ 700ms/ x์ถ: 0~6s) |
(y์ถ: 0
- ์บ์ฑ ์ : ํ๊ท ์๋ต ์๊ฐ์ด 3,000ms ์ด์์ด๋ฉฐ, ๋ชจ๋ ์์ฒญ์ ์ฒ๋ฆฌํ๋ ๋ฐ ์ฝ 33์ด๊ฐ ์์๋๋ค.
- ์บ์ฑ ํ: ์๋ต ์๊ฐ์ด ๋์ฒด๋ก 400~700ms ์ฌ์ด๋ก ๊ฐ์ ๋์๊ณ , ๋ชจ๋ ์์ฒญ ์ฒ๋ฆฌ ์๊ฐ์ ์ฝ 10์ด ๋ด์ธ์ด๋ค.
[Transactions per Second(์ด๋น ์ฒ๋ฆฌ ๊ฑด์)]
[๊ฐ์ ์ ] : 100~190๊ฑด ์ฌ์ด์ ๊ฐ์์ ์๋
(y์ถ: 0~200/ x์ถ:0~33s)
[์ธ๋ฉ๋ชจ๋ฆฌ ์บ์ฑ ์ ์ฉ] : 950๊ฑด๊น์ง ์์นํ๋ค ํ๊ฐ
(y์ถ: 0~950/ x์ถ:0~7s)
[์นดํ์ธ ์บ์ฑ ์ ์ฉ] : 1000๊ฑด๊น์ง ์์นํ๋ค ํ๊ฐ
(y์ถ: 0~1000/ x์ถ:0~7s)
[๋ ๋์ค ์บ์ฑ ์ ์ฉ] : 850๊ฑด๊น์ง ์์นํ๋ค ํ๊ฐ
(y์ถ: 0~900/ x์ถ:0~6s)
- ์บ์ฑ ์ : ๊ทธ๋ํ๊ฐ ํ๋ค๋ฆฌ๋ ๋ชจ์์ ๋๋ฉฐ, ๋์ฒด์ ์ผ๋ก 100~190๊ฑด์ ์์ฒญ์ ์ด๋ง๋ค ์๋ตํ๊ณ ์๋ค.
- ์บ์ฑ ํ: ๊ทธ๋ํ๊ฐ ์๋ก ๋ณผ๋กํ ๊ณก์ (3์ฐจ ํจ์ ํํ)์ ๋ณด์ด๋ฉฐ, ์ด๋น ์ต๋ ์ฝ 1,000๊ฑด์ ์์ฒญ์ ์ฒ๋ฆฌํ๋ค.
**[์ดํฉ ๋ณด๊ณ ์]**
| ์บ์ ์ข ๋ฅ | ํ๊ท | 99% | ์ต์ | ์ต๋ | ์ค๋ฅ๋ฅ | ์ฒ๋ฆฌ๋ |
|---|---|---|---|---|---|---|
| ์บ์ ์์ | 3010 | 4765 | 75 | 6009 | 0.00% | 152.3/sec |
| ์ธ๋ฉ๋ชจ๋ฆฌ ์บ์ | 539 | 1092 | 12 | 2086 | 0.00% | 765.3/sec |
| Caffeine ์บ์ | 506 | 1031 | 11 | 1405 | 0.00% | 806.6/sec |
| Redis ์บ์ | 551 | 1097 | 13 | 1500 | 0.00% | 733.9/sec |
- ํ๊ท ์๋ต ์๊ฐ
- Caffeine(506ms) < ์ธ๋ฉ๋ชจ๋ฆฌ(539ms) < Redis(551ms) โช ์บ์ ์์(3010ms)
- 99ํผ์ผํ์ผ ์๋ต ์๊ฐ (์ต์
์ฑ๋ฅ)
- Caffeine(1031ms) < ์ธ๋ฉ๋ชจ๋ฆฌ(1092ms) โ Redis(1097ms) โช ์บ์ ์์(4765ms)
- ์๋ต ์๊ฐ ์ผ๊ด์ฑ
- Caffeine โ Redis โ ์ธ๋ฉ๋ชจ๋ฆฌ > ์บ์ ์์
- ์ฒ๋ฆฌ๋ (TPS)
- Caffeine(806.6) > ์ธ๋ฉ๋ชจ๋ฆฌ(765.3) > Redis(733.9) โซ ์บ์ ์์(152.3)
โ๏ธย ย ์ต์ข ์ ํ
| ๋ฐฉ์ | ์ฅ์ | ๋จ์ | ์ ํฉํ ํ๊ฒฝ |
|---|---|---|---|
| ์บ์ ์์ | ๊ตฌํ ๋จ์ | ์ฑ๋ฅยท์์ ์ฑ ๋ชจ๋ ์ด์ธ | ๊ถ์ฅํ์ง ์์ |
| ์ธ๋ฉ๋ชจ๋ฆฌ ์บ์ฑ | ๊ตฌํ ๊ฐ๋จ, ๋จ์ผ ์ธ์คํด์ค์ ์ ํฉ | RedisยทCaffeine ๋๋น ์ฑ๋ฅ ๋ฎ์ | ๋จ์ผ ์๋ฒ, ๊ฐ๋จํ ๊ตฌ์กฐ |
| Caffeine ์บ์ฑ | ์๋ยท์ฒ๋ฆฌ๋ ์ฐ์, ๋ณ๋ ํญ ์ต์ | ๋จ์ผ ์ธ์คํด์ค ํ์ | ์ต๊ณ ์ฑ๋ฅ์ด ํ์ํ ๋จ์ผ ์๋ฒ |
| Redis ์บ์ฑ | TPS ์ ์ง๋ ฅ ์ฐ์, ๋ฉํฐ ์๋ฒ ํ๊ฒฝ ์ง์ | ๋คํธ์ํฌ ์ค๋ฒํค๋ ๋ฐ์ | ๋๊ท๋ชจ ๋ถ์ฐ ํ๊ฒฝ |
- ํ ์คํธ ๊ฒฐ๊ณผ Caffeine์ด ์๋ต ์๊ฐ๊ณผ ์ฒ๋ฆฌ๋ ๋ฉด์์ ๊ฐ์ฅ ์ฐ์ํ์ผ๋(ํ๊ท ยทp99ยทTPS ๋ชจ๋ ์ ๋), Caffeine๊ณผ ๋ค๋ฅธ ์บ์๋ค ๊ฐ ์ฑ๋ฅ ๊ฒฉ์ฐจ๋ ํฌ์ง ์์์ต๋๋ค.
- ์ธ๋ฉ๋ชจ๋ฆฌ ์บ์๋ JVM ๋ด๋ถ ํ์ ์ผ๋ก ๋ฉํฐ ์๋ฒ ํ๊ฒฝ์์ ํ์ฅ์ฑ๊ณผ ์ด์์ ํ๊ณ๊ฐ ์์ต๋๋ค.
- ๋ณธ ํ๋ก์ ํธ๋ ๋ค๋ฅธ ์์ญ์์ ๋ถ์ฐ ํ๊ฒฝ ํ์ฅ์ฑ์ ๊ณ ๋ คํด ์ ํฉํ ๊ธฐ์ ์ ๋์ ํ์ผ๋ฉฐ, ์บ์ฑ๋ ๊ฐ์ ๊ด์ ์์ ๊ฒฐ์ ํ์ต๋๋ค. ๊ธฐ์ ํ๊ฒฝ์์๋ ์ด์ ๊ฐ์ ์ด์ ๋ก Redis ๋ฑ ๋ถ์ฐ ์บ์๋ฅผ ๋๋ฆฌ ์ฌ์ฉํฉ๋๋ค.
๋ฐ๋ผ์ ์บ์ ๋ฏธ์ฌ์ฉ ๋๋น ํ์ ํ ์ฑ๋ฅ ํฅ์์ด ํ์ธ๋์๊ณ , ์ ์ฒด์ ์ธ ์ฑ๋ฅ ๊ฒฉ์ฐจ๊ฐ ํฌ์ง ์๋ค๋ ์ ๊ณผ ๋ถ์ฐ ๊ตฌ์กฐ์์์ ์ ํฉ์ฑ์ ์ข ํฉ์ ์ผ๋ก ๊ณ ๋ คํ์ฌ ์ต์ข ์ ์ผ๋ก Redis๋ฅผ ์ ํํ์ต๋๋ค.
// build.gradle
dependencies {
// redis ์บ์
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
//json ์ง๋ ฌํ๋ฅผ ์ํ jackson
implementation 'com.fasterxml.jackson.core:jackson-databind'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
}
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
// ์๋ฐ ํ์ ๋ชจ๋ ๋ฑ๋กํ์ฌ LocalDateTime -> ISO-8601 ํํ๋ก ์ง๋ ฌํ
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
// ํ์
์ ๋ณด ํฌํจ (์ง๋ ฌํ/์ญ์ง๋ ฌํ ์ ํด๋์ค ์ ๋ณด ์ ์ง)
mapper.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.PROPERTY
);
GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(mapper);
// ์บ์ ์ค์
RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) // ํค๋ ๋ฌธ์์ด ์ง๋ ฌํ
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer)) // ๊ฐ์ JSON ์ง๋ ฌํ
.disableCachingNullValues() // null์ ์บ์์ ์ ์ฅํ์ง ์์
.entryTtl(Duration.ofMinutes(10)); // TTL 10๋ถ
return RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(cacheConfig) // ์บ์ ์ค์
.build();
}
}
@Service
@RequiredArgsConstructor
public class NewsfeedService {
private final NewsfeedRepository newsfeedRepository;
private final UserService userService;
private final ImageService imageService;
@Loggable
@Transactional(readOnly = true)
@Cacheable(
value = "listCache",
key = "#pageable.pageNumber + '-' + #pageable.pageSize + '-' + #pageable.sort.toString()"
)
public PageResponseDto<NewsfeedListResponse> getAllNewsfeeds(Pageable pageable) {
// ์์์ ์ ์ฒด ์กฐํ(์ํํธ๋๋ฆฌํธ ๋นผ๊ณ )
Page<Newsfeed> pagedNewsfeeds = newsfeedRepository.findByIsDeletedFalseWithFetchJoin(pageable);
// ํ์ด์ง์ ๋ค์ด๊ฐ ๋ํ ์ด๋ฏธ์ง ์ผ๊ด ์กฐํ
Map<Long, Image> mainImageMap = imageService.getMainImagesForNewsfeeds(pagedNewsfeeds);
List<NewsfeedListResponse> contentList = pagedNewsfeeds.stream().map(newsfeed -> {
Image mainImage = mainImageMap.getOrDefault(newsfeed.getId(), null);
return NewsfeedListResponse.of(newsfeed, mainImage != null ? mainImage.getImageUrl() : null);
})
.collect(Collectors.toList()); // ๊ฐ๋ณ ๋ฆฌ์คํธ๋ก ๋ณํ
return new PageResponseDto<>(
new PageImpl<>(contentList, pageable, pagedNewsfeeds.getTotalElements())
);
}
}
- JPA์์ ๋ฐํํ๋
Page.getContent()๋ ๋ณดํต ๋ถ๋ณ ๋ฆฌ์คํธ๋ก ๊ฐ์ธ์ ธ ์์ด์, ์ง์ ์บ์์ ๋ฃ์ผ๋ฉด ์ง๋ ฌํ ๋ฌธ์ ๊ฐ ๋ฐ์ PageResponseDto๋ด๋ถcontentํ๋๋Page.getContent()๋ฅผ ๋ฐ์์ ์ด๊ธฐํํ๋ฏ๋ก, ์ ๋ฌํ๋Page์ ๋ฆฌ์คํธ๋ก ๊ฐ๋ณ ๋ฆฌ์คํธ ํ์- ๋ฐ๋ผ์, ์๋น์ค ๋ ์ด์ด์์
Page<T>์ ๋ด์ฉ์ ๊ฐ๋ณ ๋ฆฌ์คํธ๋ก ์๋ก ์์ง ํ, ๊ทธ ๋ฆฌ์คํธ๋ฅผ ์ฌ์ฉํดPageImpl์ ์๋ก ์์ฑํ์ฌ ์ ๋ฌ PageImpl์ ๋ฆฌ์คํธ๋ฅผ ๋ณต์ฌํ๊ฑฐ๋ ๋ถ๋ณ์ผ๋ก ๊ฐ์ธ์ง ์๊ธฐ ๋๋ฌธ์, ๊ฐ๋ฐ์๊ฐ ๋๊ธด ๊ฐ๋ณ ๋ฆฌ์คํธ๋ฅผ ๊ทธ๋๋ก ์ ์ง
๊ฒฐ๊ณผ์ ํจ๊ณผ
- ๋ถ์ฐ ํ๊ฒฝ๊ณผ ํ์ฅ์ฑ: ์๋ฒ ๊ฐ ์บ์ ๋ฐ์ดํฐ ๊ณต์ ๊ฐ ๊ฐ๋ฅํ์ฌ ๋ถ์ฐ ํ๊ฒฝ ๊ตฌ์ถ๊ณผ ํ์ฅ์ด ์ฉ์ดํฉ๋๋ค.
- ๋์ ์ฒ๋ฆฌ๋๊ณผ ์์ ์ฑ: TPS ์ ์ง๋ ฅ์ด ๋ฐ์ด๋ ๋์ ๋ถํ๋ ์ํํ ์ฒ๋ฆฌํ๋ฉฐ, ๊ฐ์ฉ์ฑยท๋ณต์ ยท์์์ฑ ์ต์ ์ผ๋ก ์์ ์ ์ธ ์ด์์ด ๊ฐ๋ฅํฉ๋๋ค.
- ์ ์ฐํ ๊ด๋ฆฌ: ์บ์ ๋งค๋์ ๋ฅผ ํตํ ์ค์ ๊ด๋ฆฌ์ ๋ค์ํ ๋ชจ๋ํฐ๋ง ๋๊ตฌ๋ฅผ ์ง์ํฉ๋๋ค.
- ๋ค์ํ ๋ฐ์ดํฐ ๊ตฌ์กฐ ๋ฐ ํธํ์ฑ: Hash, List, Set, Sorted Set ๋ฑ ๋ค์ํ ๋ฐ์ดํฐ ๊ตฌ์กฐ๋ฅผ ์ ์ฅํ ์ ์์ผ๋ฉฐ, ์ฌ๋ฌ ์ธ์ด์ ํ๋ซํผ๊ณผ ํธํ๋ฉ๋๋ค.
์ฃผ์ ์ฌํญ / ํ๊ณ
- ์ธ๋ถ ์๋ฒ ์์กด: ์บ์๊ฐ ์ธ๋ถ ์๋ฒ์ ์์กดํฉ๋๋ค.
- ์ด์ ๋ฐ ์ ์ง๋ณด์ ๋ถ๋ด: ๊ด๋ฆฌ ๋น์ฉ๊ณผ ์ค์ , ์ง๋ ฌํ/์ญ์ง๋ ฌํ ๊ตฌํ์ด ํ์ํ๋ฉฐ, ํ๋ ํ์ต์ด ์๊ตฌ๋ฉ๋๋ค.
- ๋คํธ์ํฌ ์ํ: ๋คํธ์ํฌ ์ค๋ฒํค๋ ๋ฐ์ ๊ฐ๋ฅ์ฑ๊ณผ ์ฅ์ ์ ์บ์ ์ ๊ทผ ์ง์ฐ ๋๋ ์คํจ ์ํ์ด ์์ต๋๋ค.
- ์ด๊ธฐ ์ง์ฐ ๊ฐ๋ฅ์ฑ: ์บ์ ๋ฏธ์ค ๋ฐ์ ์ ์ด๊ธฐ ์ง์ฐ(์ฝ๋ ์คํํธ)์ด ๋ฐ์ํ ์ ์์ต๋๋ค.
- ์ฝ๋ ์คํํธ ๋ฌธ์ ํด๊ฒฐ
- ์ ๊ฒ์๊ธ ์์ฑ ์ ์บ์์ ๋ฏธ๋ฆฌ ์ ์ฌํ๋ ํ๋ฆฌํํ ์ ๋ต ๋์ ํฉ๋๋ค.
- TTL์ ๋์ ์ผ๋ก ์กฐ์ ํ์ฌ ์ ๊ท ์์์ ๋ฑ๋ก ์ ์บ์ฑ์ ๋ ์ค๋ ์๊ฐ(์: 1์๊ฐ) ๋์ ์ ์งํฉ๋๋ค.
- ์ฅ์ ๋์ ๊ฐํ
- ๋คํธ์ํฌ ์ฅ์ ๋ ์๋ฒ ์ค๋ฅ ์ ์์ธ ์ฒ๋ฆฌ๋ฅผ ์ถ๊ฐํฉ๋๋ค.
- ํด๋ฐฑ(fallback) ๋ฉ์ปค๋์ฆ๊ณผ ํ์์์ ์ค์ ๊ฐํ, ์๋น์ค ์ฐ์์ฑ์ ํ๋ณดํฉ๋๋ค.
- ๊ฐ์ฉ์ฑยท๋ณต์ ยท์์์ฑ ์ต์
์ ์ฉ
- ์ ๊ณต๋๋ ์ต์ ์ ์ ๊ทน ๋์ ํ์ฌ ์์คํ ์์ ์ฑ๊ณผ ๋ฐ์ดํฐ ๋ฌด๊ฒฐ์ฑ ๊ฐํํฉ๋๋ค.
โก๏ธย ์ฌ๊ณ ๊ด๋ฆฌ ๋์์ฑ ์ด์ ํด๊ฒฐ
์ํ ์ฃผ๋ฌธ ๋ฐ ์ฌ๊ณ ๊ด๋ฆฌ ์์คํ
์ค๊ฑฐ๊ฑฐ๋ ํ๋ซํผ์์ ์ฉ์๊ฐ ์ํ์ ์ฃผ๋ฌธํ ๋ ์ค์๊ฐ์ผ๋ก ์ฌ๊ณ ๋ฅผ ์ฐจ๊ฐํ๊ณ ๊ด๋ฆฌํ๋ ํต์ฌ ๊ธฐ๋ฅ์ ๋๋ค. ๋ค์์ ์ฌ์ฉ์๊ฐ ๋์์ ๊ฐ์ ์ํ์ ์ฃผ๋ฌธํ ๋ ์ ํํ ์ฌ๊ณ ๊ณ์ฐ๊ณผ ์ค๋ฒ์ ๋ง ๋ฐฉ์ง๊ฐ ์ค์ํ ๋น์ฆ๋์ค ์๊ตฌ์ฌํญ์ ๋๋ค.
๊ธฐ์กด ์์คํ ์์๋ ๋์์ฑ ์ ์ด ๋ฉ์ปค๋์ฆ์ด ์์ด ์ฌ๋ฌ ์ฌ์ฉ์๊ฐ ๋์์ ์ฃผ๋ฌธํ ๋ ์ฌ๊ฐํ ๋ฐ์ดํฐ ๋ถ์ผ์น ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ต๋๋ค.
์ํ: 5๊ฐ
์ค์ ํ๋งค: 12๊ฐ
์ฌ๊ณ ์ค์ฐจ: 7๊ฐ
์ฃผ์ ๋ฌธ์ ์
- ๋์ ์ ๊ทผ ์ ์ฌ๊ณ ๊ณ์ฐ ์ค๋ฅ
- ์ฌ๊ณ ๋ณด๋ค ๋ง์ ์๋ ํ๋งค (์ค๋ฒ์ ๋ง)
- ๋ฐ์ดํฐ ์ผ๊ด์ฑ ๊นจ์ง์ผ๋ก ์ธํ ๋น์ฆ๋์ค ๋ก์ง ์ค๋ฅ
Redisson ๋ถ์ฐ ๋ฝ + ๋น๊ด์ ๋ฝ ์กฐํฉ
๋ค์ค ์ธ์คํด์ค ํ๊ฒฝ์์ ์์ ํ ๋์์ฑ ์ ์ด๋ฅผ ์ํด ์ด์ค ๋ฝ ๋ฉ์ปค๋์ฆ์ ์ฌ์ฉ
ํด๊ฒฐ ๋ฐฉ๋ฒ
- Redisson ๋ถ์ฐ ๋ฝ์ผ๋ก ์ธ์คํด์ค ๊ฐ ๋๊ธฐํ
- DB ๋ ๋ฒจ ๋น๊ด์ ๋ฝ์ผ๋ก ๋ฐ์ดํฐ ๋ฌด๊ฒฐ์ฑ ๋ณด์ฅ
- REQUIRES_NEW ์ ํ ์ต์ ์ผ๋ก ๋ ๋ฆฝ์ ํธ๋์ญ์ ๊ด๋ฆฌ
์ ์ฉ ์ฝ๋
// ์์ ์ : ๋์์ฑ ์ด์ ๋ฐ์ ์ฝ๋
public void decreaseStock(Long itemId, Long quantity) {
Item item = itemRepository.findById(itemId).orElseThrow();
item.decreaseStock(quantity); // Race Condition ๋ฐ์ ์ง์
}
// ์์ ํ: ๋ถ์ฐ ๋ฝ ์ ์ฉ
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void decreaseStock(Long itemId, Long quantity) {
String lockKey = "Lock:" + itemId;
RLock lock = redissonClient.getLock(lockKey);
// ๋ถ์ฐ ๋ฝ + ๋น๊ด์ ๋ฝ์ผ๋ก ์์ ํ ์ฌ๊ณ ์ฐจ๊ฐ
}
Redis ๋ถ์ฐ๋ฝ ์ฌ์ฉ์ ํ ๋น๊ต
๋์์ฑ ํ ์คํธ
์ด๊ธฐ ์ฌ๊ณ : 5๊ฐ
๋์ ์์ฒญ : 100๋ช ๊ฐ 1๋ฒ์ฉ ์ํ
19๋ฒ ์์ดํ
100๋ช
์ด 1๋ฒ์ฉ ๊ตฌ๋งค ์๋

์ํ ๊ฒฐ๊ณผ
๋ถ์ฐ๋ฝ ์ ์ฉ : 5๊ฐ ์ฑ๊ณต, 95๊ฐ ์คํจ, ์ฌ๊ณ 0๊ฐ
๋ถ์ฐ๋ฝ ๋ฏธ์ ์ฉ : 12๊ฐ ์ฑ๊ณต, 88๊ฐ ์คํจ, ์ฌ๊ณ 0๊ฐ
๋์์ฑ ์ด์ ํด๊ฒฐ
- ๋ฐ์ดํฐ ์ ํฉ์ฑ 100% ๋ณด์ฅ: ์ฌ๊ณ ์ค์ฐจ ์์ ์ ๊ฑฐ
- ์ค๋ฒ์ ๋ง ๋ฐฉ์ง: ์ฌ๊ณ ํ๋ ๋ด์์๋ง ์ฃผ๋ฌธ ์ฒ๋ฆฌ
- ๋ค์ค ์ธ์คํด์ค ์์ ์ฑ: MSA ํ๊ฒฝ์์๋ ์์ ํ ๋์์ฑ ์ ์ด
- ์์ธ ์ฒ๋ฆฌ ๊ฐํ: ๋ฝ ํ๋ ์คํจ ์ ๋ช ํํ ์๋ฌ ๋ฉ์์ง ์ ๊ณต
๋์์ฑ ์ด์ ํด๊ฒฐํจ์ผ๋ก์ ์ป์ด์ง๋ ๋น์ฆ๋์ค ์ํฉํธ
- ๊ณ ๊ฐ ์ ๋ขฐ๋ ํฅ์ (์ฌ๊ณ ์ค๋ฅ๋ก ์ธํ ์ฃผ๋ฌธ ์ทจ์ 0๊ฑด)
- ์ด์ํ ์ ๋ฌด ํจ์จ์ฑ ์ฆ๋ (์๋ ์ฌ๊ณ ์ ์ ์์ ๋ถํ์)
- ๋งค์ถ ์์ค ๋ฐฉ์ง (์ ํํ ์ฌ๊ณ ๊ด๋ฆฌ๋ก ํ๋งค ๊ธฐํ ๊ทน๋ํ)
๐จย ์ปค๋ฅ์ ํ ๊ณ ๊ฐ ํ์
- Mania Place์ ์ํ ๊ฒ์ ๊ธฐ๋ฅ ๋ถํ ํ ์คํธ ์ค, ๋์ ์ฌ์ฉ์ ์ ์ฆ๊ฐ ์ ๊ฒ์ ์คํจ์จ์ด ๊ธ์ฆํ๊ณ ์๋ต ์๊ฐ์ด ํ์ ํ ์ง์ฐ๋์์ต๋๋ค.
- ๊ฒ์ ์์ฒญ ์ฒ๋ฆฌ ๋ง๋น์ ์ฃผ๋ ์์ธ์ ์ธ๊ธฐ ๊ฒ์์ด ๋ญํน ์ง๊ณ ๊ธฐ๋ฅ์ด ๊ฒ์ ํธ๋์ญ์ ์ ํฌํจ๋์ด ์์๊ธฐ ๋๋ฌธ์ ๋๋ค.
[ํ๊ฒฝ]
Docker ์ปจํ ์ด๋ ํ๊ฒฝ
(์ ํ๋ฆฌ์ผ์ด์ ์ CPU 1์ฝ์ด, ๋ฉ๋ชจ๋ฆฌ 1.5GB ์์ ํ ๋น)
[์ฌ์ฉ ๋๊ตฌ] Docker Compose, Apache JMeter
[๋ฐ์ดํฐ ์กฐ๊ฑด]
- ์ด๊ธฐ ๋ฐ์ดํฐ: ์์ดํ = 1600๊ฐ ์์ฑ ํ ์คํ
[์ค๋ ๋ ์์ฑ]
- ์ฌ์ฉ์ ์: 30๋ช
- Ramp-up ์๊ฐ: 1์ด
- ๋ฃจํ ์นด์ดํธ: 10ํ
[๋์]
- ์์ดํ ๊ฒ์
[๋ชฉ์ ]
- ์ค๋ฅ ๋ฐ์ ํ์ธ
jp@gc - Transactions per Second
- TPS (์ด๋น ์ฒ๋ฆฌ๋): 1.4 / sec
- Latency (ํ๊ท ์๋ต ์๊ฐ): 20,468 ms
- Error Rate (์๋ฌ์จ): 31.33 %
- ์์คํ ๋ง๋น ์์ค์ ์ฑ๋ฅ: 31.33%์ ๋์ ์๋ฌ์จ๊ณผ 20์ด๊ฐ ๋๋ ์๋ต ์๊ฐ์ ์ฌ์ค์ ์์คํ ์ด ๋ถํ๋ฅผ ์ ํ ๊ฐ๋นํ์ง ๋ชปํ๊ณ ๋ง๋น ์ํ์ ์ด๋ฅด๋ ์์ ์๋ฏธํฉ๋๋ค.
- DB ์ปค๋ฅ์ ํ ๊ณ ๊ฐ: ๋ถํ๋ฅผ ๊ฒฌ๋์ง ๋ชปํ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์๋ต ์ง์ฐ์ด ์์ธ์ด ๋์ด ์ปค๋ฅ์ ํ์ด ์์ ํ ๊ณ ๊ฐ๋์์ต๋๋ค. ์ด๋ก ์ธํด ์๋ก์ด ์์ฒญ์ ํธ๋์ญ์ ์กฐ์ฐจ ์์ํ์ง ๋ชปํ๊ณ ์คํจํ์ต๋๋ค.
์์คํ ๋ง๋น ๋ฐ์. 1์ด ๋ง์ 30๋ช ์ ๋์ ์ฌ์ฉ์๊ฐ ์ ์ ๋๋ ์คํ์ดํฌ ํธ๋ํฝ ์ํฉ์์, ์์คํ ์ ๋ถํ๋ฅผ ์ ํ ๊ฐ๋นํ์ง ๋ชปํ๊ณ ์ฌ์ค์ ๋ง๋น ์ํ์ ์ด๋ฅด๋ ์ต๋๋ค.
[์์ธ ๋ถ์]
-
์์ธ: ๋ฐ์ํ ์ค๋ฅ์ 100%๊ฐ HikariCP ์ปค๋ฅ์ ํ ํ์์์์ด์์ต๋๋ค.
์ด๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์๋ต์ด ๋๋ฌด ๋๋ ค ์ปค๋ฅ์ ์ ์ ๋ ๋ฐ๋ฉํ์ง ๋ชปํ๊ธฐ ๋๋ฌธ์ ๋๋ค.
- ๊ฒฐ๊ณผ: ์ปค๋ฅ์ ๋ถ์กฑ์ผ๋ก ์๋ก์ด ์์ฒญ์ ํธ๋์ญ์ ์กฐ์ฐจ ์์ํ์ง ๋ชปํ๊ณ ์คํจํ์ผ๋ฉฐ, ์ด๋ก ์ธํด 31.33%์ ์์ฒญ์ด ์ ์ค๋๊ณ , ์๋ต ๊ฐ๋ฅํ ์์ฒญ๋ง์ ํ๊ท 20์ด ์ด์์ด ์์๋์ด ์ ์์ ์ธ ์๋น์ค ์ ๊ณต์ด ๋ถ๊ฐ๋ฅํจ์ ํ์ธํ์ต๋๋ค.
- ์ ํ๋ฆฌ์ผ์ด์ ์ค๋ ๋์ DB ์ปค๋ฅ์ ํ์ ์์ ๋ถ๊ท ํ: 60๋ช ์ ๋์ ์ฌ์ฉ์ ์์ฒญ์ด ์ ์ ๋์ 100๊ฐ ์ด์์ ์ ํ๋ฆฌ์ผ์ด์ ์ค๋ ๋๊ฐ ์์ฑ๋์์ต๋๋ค. ํ์ง๋ง ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ปค๋ฅ์ ํ(HikariCP)์ ์ต๋์น๋ 10๊ฐ๋ก ์ค์ ๋์ด ์์์ต๋๋ค.
- ์ปค๋ฅ์ ๋ณ๋ชฉ ํ์: ๊ฒ์ ์์ฒญ์ด ๋ฐ์ํ ๋๋ง๋ค ์คํ๋๋ '์ธ๊ธฐ ๊ฒ์์ด ์นด์ดํธ ์ ๋ฐ์ดํธ' ๋ก์ง์ผ๋ก ์ธํด DB ํธ๋์ญ์ ์ด ๊ธธ์ด์ก์ต๋๋ค. ๋จผ์ ์ปค๋ฅ์ ์ ์ฐจ์งํ 10๊ฐ์ ์ค๋ ๋๊ฐ ์์ ์ ๋ง์น ๋๊น์ง ๋ค๋ฅธ ๋ชจ๋ ์ค๋ ๋(100๊ฐ ์ด์)๋ ์ปค๋ฅ์ ์ ๋ฌดํ์ ๋๊ธฐํ์ต๋๋ค.
- ํ์์์ ๋ฐ ์์ธ ๋ฐ์: ๋๊ธฐํ๋ ์ค๋ ๋๋ค์ ๊ฒฐ๊ตญ ์ปค๋ฅ์
ํ์์์(30์ด)์ ์ด๊ณผํ๊ฒ ๋์๊ณ , ์ด๋ก ์ธํด
SQLTransientConnectionException์ด ๋ฐ์ํ์ต๋๋ค. Spring/JPA๋ ์ด ์์ธ๋ฅผCannotCreateTransactionException์ผ๋ก ๊ฐ์ธ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ ๋ฌ, ๊ฒฐ๊ณผ์ ์ผ๋ก ๋๋์ ๊ฒ์ ์คํจ(์๋ฌ์จ 31.33%)๋ก ์ด์ด์ก์ต๋๋ค. - ๊ทผ๋ณธ ์์ธ: ๊ฒ์ ๊ธฐ๋ฅ(์ฝ๊ธฐ)๊ณผ ๋ญํน ์ง๊ณ(์ฐ๊ธฐ)๊ฐ ๋์ผํ DB ์ปค๋ฅ์ ํ์ ๊ณต์ ํ๋ฉฐ ๊ฒฝํฉํ๋ ๊ตฌ์กฐ๊ฐ ๋ฌธ์ ์ ํต์ฌ์ด์์ต๋๋ค.
- ์ปค๋ฅ์ ํ ์ฆ์ค ์๋ (10 - > 20 ์ฆ์ค)
์ ํ ์คํธ์ ๋์ผํ๊ฒ ํ์๋ ์ ์ ์๋์ ํ์ธํ์ง๋ง
2๋ฐฐ ์ฆ๊ฐํ ์ปค๋ฅ์ ํ๋งํผ 2๋ฐฐ์ ์ฌ์ฉ์๋ฅผ ์ถ๊ฐ ์์ฒญ ๋ณด๋์๋(์ฌ์ฉ์ 30 - > ์ฌ์ฉ์ 60)
๋๊ฐ์ ์ค๋ฅ๋ฅผ ํ์ธํ ์ ์์์ต๋๋ค.
- DB ์ปค๋ฅ์ ํ(HikariCP) ์ต๋์น๋ฅผ ๋๋ ค ๋์ ํธ๋์ญ์ ์ฒ๋ฆฌ ๋ฅ๋ ฅ์ ํ์ฅ
- ๊ฒฐ๊ณผ: ๋์ ์์ฒญ ์๊ฐ ์ฆ๊ฐํ๋ฉด ์ฌ์ ํ ์ปค๋ฅ์ ์ด ๊ณ ๊ฐ๋๊ณ ๊ฒ์ ์คํจ๊ฐ ๋ฐ์ โ ๊ทผ๋ณธ์ ์ธ ํด๊ฒฐ์ฑ ์๋
- ๊ทผ๋ณธ์ ํด๊ฒฐ: Redis ๋์
- ์ธ๊ธฐ ๊ฒ์์ด ์ง๊ณ ๊ธฐ๋ฅ์ RDB โ Redis ZSet์ผ๋ก ์ด์
- ํจ๊ณผ:
- Redis์ ์ธ๋ฉ๋ชจ๋ฆฌ ๊ตฌ์กฐ๋ก ๋น ๋ฅธ ์ฝ๊ธฐ/์ฐ๊ธฐ ์ฒ๋ฆฌ ๊ฐ๋ฅ
- ๊ฒ์ ๊ธฐ๋ฅ๊ณผ ๋ญํน ์ง๊ณ ๊ธฐ๋ฅ์ ์์ ๊ฒฝํฉ ํด์
- DB๋ ํต์ฌ ๊ฒ์ ํธ๋์ญ์ ์ฒ๋ฆฌ์๋ง ์ง์ค ๊ฐ๋ฅ โ ์์ ์ฑ ํ๋ณด
###[๊ฒ์ ๊ธฐ๋ฅ ๊ฐ์ ๊ฒฐ๊ณผ]
jp@gc - Transactions per Second
- TPS (์ด๋น ์ฒ๋ฆฌ๋): 59.9 / sec
- Latency (ํ๊ท ์๋ต ์๊ฐ): 380 ms
- Error Rate (์๋ฌ์จ): 0 %
- ํ๊ธฐ์ ์ธ ์ฑ๋ฅ ํฅ์: ํ๊ท ์๋ต ์๊ฐ์ด 380ms๋ก ํฌ๊ฒ ๋จ์ถ๋์๊ณ , ์ด๋น ์ฒ๋ฆฌ๋(TPS)์ ์ฝ 42๋ฐฐ ์ฆ๊ฐํ์ฌ ์ฌ์ฉ์์๊ฒ ์พ์ ํ ๊ฒ์ ๊ฒฝํ์ ์ ๊ณตํ ์ ์๊ฒ ๋์์ต๋๋ค.
- ์์คํ ์์ ์ฑ ์๋ฒฝ ํ๋ณด: ์๋ฌ์จ์ด **0%**๋ก ํด์๋์๊ณ , DB ์ปค๋ฅ์ ๋ณ๋ชฉ ํ์์ด ์ฌ๋ผ์ ธ ๋๊ท๋ชจ ํธ๋ํฝ์๋ ์์ ์ ์ผ๋ก ์๋น์ค๋ฅผ ์ด์ํ ์ ์๋ ๊ธฐ๋ฐ์ ๋ง๋ จํ์ต๋๋ค.
- ์ํคํ ์ฒ ์ค๊ณ์ ์ค์์ฑ: ์ฆ์ ์ฐ๊ธฐ์ ์ค์๊ฐ ์ง๊ณ ๊ธฐ๋ฅ์ ๋ฉ์ธ DB์ ํตํฉํ ๊ฒ์ด ์ฑ๋ฅ ์ ํ์ ์ง์ ์ ์ธ ์์ธ์ด์์ต๋๋ค.
- ์์ ์ดํด์ ํ์์ฑ: ์ ํ๋ฆฌ์ผ์ด์ ์ค๋ ๋์ DB ์ปค๋ฅ์ ํ ๊ฐ์ ์ํธ์์ฉ์ ์ดํดํ๋ ๊ฒ์ด ๋ณ๋ชฉ ํ์์ ํด๊ฒฐํ๋ ํต์ฌ์ด์์ต๋๋ค.
- ํฅํ ๋์ ๋ฐฉ์: ์ค์๊ฐ ์ง๊ณ ๊ธฐ๋ฅ์ ๊ฐ๋ฐํ ๋๋ ๊ฒ์ ๊ธฐ๋ฅ๊ณผ์ ์์กด์ฑ์ ๋ถ๋ฆฌํ๋ ๊ฒ์ ์ฐ์ ์ ์ผ๋ก ๊ณ ๋ คํด์ผ ํฉ๋๋ค.
๐จย ํ๊ทธ ์ ์ฅ ๋์์ฑ ๋ฌธ์
[https://www.notion.so/teamsparta/24e2dc3ef514805eac6cce2bafe76037](https://www.notion.so/24e2dc3ef514805eac6cce2bafe76037?pvs=21)๋ณธ ๊ธ์ ํด๋น ๊ฒ์๊ธ์ ์์ฝํ ๋ด์ฉ์ด๋ฉฐ, ์์ธํ ๋ด์ฉ์ ์ ๋งํฌ์์ ํ์ธํ์ค ์ ์์ต๋๋ค.
์ ํฌ ํ์์ ๊ฐ๋ฐ ์ค์ธ ์๋ธ์ปฌ์ฒ ์ค๊ณ ๊ฑฐ๋ ์ฌ์ดํธ์๋ ๊ฐ์ธํ ์๋น์ค๋ฅผ ์ํ 'ํ๊ทธ' ๊ธฐ๋ฅ์ด ์์ต๋๋ค.
์ฌ์ฉ์๋ ๊ด์ฌ์ฌ์ ๋ง๋ ํ๊ทธ๋ฅผ ์ต๋ 10๊ฐ๊น์ง ์ค์ ํ ์ ์์ผ๋ฉฐ, ์ด ํ๊ทธ๋ ํ์ ํ๋กํ๊ณผ ์ค๊ณ ๋ฌผํ์ ๋ชจ๋ ์ ์ฉ๋์ด ๊ฐ์ธ ๋ง์ถค ์ํ ์ถ์ฒ์ ํ์ฉ๋ฉ๋๋ค.
๊ทธ๋ฐ๋ฐ ์ฌ๋ฌ ์ฌ์ฉ์๊ฐ ๋์์ ํ๊ทธ๋ฅผ ์ ์ฅํ๋ ๊ณผ์ ์์ ๋์์ฑ ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ต๋๋ค. ํนํ ํ์๊ฐ์ ์์ ์ด๋ ์ฌ์ฉ์๊ฐ ํ๊ทธ๋ฅผ ์ผ๊ด ์์ ํ ๋, ๋ฌผํ ๋ฑ๋ก์ ํ๊ทธ๋ฅผ ์ ์ฅํ ๋ ๋ฐ์ดํฐ ์ผ๊ด์ฑ ๋ฌธ์ ์ ์ค๋ณต ์ ์ฅ ์ค๋ฅ๊ฐ ๋ฐ๋ณต์ ์ผ๋ก ๋ํ๋ฌ์ต๋๋ค.
[ ํ ์คํธ ์๋๋ฆฌ์ค ] ์ธ๊ธฐ ์ ๋๋ฉ์ด์ ๊ตฟ์ฆ์ ํ์ ํ๋งค๋ก ์ธํด ํ๋งค ์์ ์ ๋๋์ ์ฌ์ฉ์๊ฐ ๋์์ ํ์๊ฐ์ ์ ์๋ํ๋ ์ํฉ์ ์๋ฎฌ๋ ์ด์ ํ์ต๋๋ค. ์ฝ 100๋ช ์ ์ฌ์ฉ์๊ฐ ๋์์ ๊ฐ์ ์ ์งํํ๋ฉฐ, ์ด ๊ณผ์ ์์ ์ค๋ณต๋๋ ํ๊ทธ๋ฅผ ์ ๋ ฅํ๋ ๊ฒฝ์ฐ๊ฐ ๋ฐ์ํ ์ ์๋ค๊ณ ๊ฐ์ ํ์ต๋๋ค.
- ์ ์ฒด ์์ฒญ ์ค ์ฝ 16%์ ์๋ฌ์จ ๋ฐ์
- ์ฃผ์ ์๋ฌ :
Duplicate entry ... for key- ๊ฐ์ ํ๊ทธ๋ช ์ด ์ด๋ฏธ ์กด์ฌํ๋๋ฐ INSERT ์๋
- MySQL์
UNIQUE์ ์ฝ์กฐ๊ฑด์ ๊ฑธ๋ คSQLIntegrityConstraintViolationException๋ฐ์
Deadlock found when trying to get lock- ๋์์ ์ฌ๋ฌ ํธ๋์ญ์ ์ด ๊ฐ์ ํ ์ด๋ธ/์ธ๋ฑ์ค๋ฅผ ๊ฐฑ์ ํ๋ ค๋ค๊ฐ MySQL์ ์ ๊ธ ๊ฒฝํฉ์ผ๋ก ๋ฐ๋๋ฝ ๋ฐ์
- MySQL์ด ๊ต์ฐฉ ์ํ๋ฅผ ๊ฐ์งํ๊ณ ํ ํธ๋์ญ์ ์ ๊ฐ์ ๋กค๋ฐฑ
๋์์ฑ ๋ฐ์ ์ฃผ์ ์์ธ์ธ findOrCreateTag ๋ฉ์๋ ์ฝ๋์ ์๋ ๋ฐฉ์ :
public Tag findOrCreateTag(String tagName) {
return tagRepository.findByTagName(tagName) // SELECT
.orElseGet(() -> tagRepository.save(Tag.of(tagName))); //INSERT
}
- ์ฌ์ฉ์๊ฐ ์ ๋ ฅํ ํ๊ทธ ์ด๋ฆ์ DB์์ ๋จผ์ ์กฐํ๋ฅผ ํฉ๋๋ค.
- ์กด์ฌํ์ง ์์ผ๋ฉด ํด๋น ํ๊ทธ๋ฅผ ์ ์ฅํฉ๋๋ค.
์ด๊ธฐ ๊ฐ๋ฐ ๋น์์๋ findOrCreateTag() ๋ฉ์๋์์ ํ๊ทธ์ ์ค๋ณต ์ฌ๋ถ๋ฅผ ํ์ธํ ํ ์ ์ฅํ๋ ๋ก์ง์ผ๋ก ๊ตฌํํ๊ธฐ ๋๋ฌธ์, ์ค๋ณต๋ ํ๊ทธ๊ฐ ์์ฑ๋ ๊ฐ๋ฅ์ฑ์ ๊ณ ๋ คํ์ง ๋ชปํ์ต๋๋ค.
SELECT ์ฟผ๋ฆฌ๋ก ํ๊ทธ๋ฅผ ์กฐํํ ํ, ํ๊ทธ๊ฐ ์กด์ฌํ์ง ์์ผ๋ฉด **INSERT**์ฟผ๋ฆฌ๋ก ํ๊ทธ๋ฅผ ์ ์ฅํ๋ ๋ฐฉ์์ธ๋ฐ,
์ด๋ฌํ ๋ก์ง์ ์ฌ๋ฌ ์์ฒญ์ด ๋์์ ์คํํ ๊ฒฝ์ฐ ์๋์ ๊ฐ์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๊ฒ์ด์์ต๋๋ค.
1๋ฒ ์ค๋ฅ -> UNIQUE ์ ์ฝ์กฐ๊ฑด ์๋ฐ
Thread A : SELECT tag WHERE name='์๋ฐ' โ ์์
Thread B : SELECT tag WHERE name='์๋ฐ' โ ์์
Thread A : INSERT '์๋ฐ'
Thread B : INSERT '์๋ฐ' โ UNIQUE ์ ์ฝ์กฐ๊ฑด ์ค๋ฅ ๋ฐ์ , ๋กค๋ฐฑ
2๋ฒ ์ค๋ฅ -> ๊ต์ฐฉ์ํ ๋ฐ์
Thread A : SELECT tag WHERE name='์๋ฐ' โ ์์
Thread B : SELECT tag WHERE name='์คํ๋ง' โ ์์
Thread A : INSERT '์๋ฐ'
Thread B : INSERT '์คํ๋ง'
Thread A : SELECT tag WHERE name='์คํ๋ง' โ ์์
Thread B : SELECT tag WHERE name='์๋ฐ' โ ์์
Thread A : INSERT '์คํ๋ง'
Thread B : INSERT '์๋ฐ' -> ๊ต์ฐฉ์ํ ๋ฐ์ , ๋กค๋ฐฑ
-
S Lock๊ณผ X Lock ์ด๋?
โ MySQL์ S-lock๊ณผ X-lock์ ๋น๊ด์ ๋ฝ์ ๊ตฌํํ ๊ฒ์ ๋๋ค.
๊ตฌ๋ถ ์ด๋ฆ ์ค๋ช ๋์์ ์ ๊ทผ ๊ฐ๋ฅํ ํธ๋์ญ์ S-lock Shared Lock (๊ณต์ ๋ฝ) ์ฝ๊ธฐ ์ ์ฉ ๋ฝ. ๋ค๋ฅธ ํธ๋์ญ์ ๋ ์ฝ๊ธฐ๋ ๊ฐ๋ฅํ์ง๋ง, ์ฐ๊ธฐ๋ ๋ถ๊ฐ๋ฅ ๋ค๋ฅธ S-lock์ ํ์ฉ / X-lock์ ๋ถ๊ฐ X-lock Exclusive Lock (๋ฐฐํ ๋ฝ) ์ฐ๊ธฐ ์ ์ฉ ๋ฝ. ์ฝ๊ธฐยท์ฐ๊ธฐ ๋ชจ๋ ๋ค๋ฅธ ํธ๋์ญ์ ์ด ์ ๊ทผ ๋ถ๊ฐ ์๋ฌด๋ ์ ๊ทผ ๋ถ๊ฐ -
์ด๋ค ํธ๋์ญ์ ์ด ๋ฐ์ดํฐ์ S-lock์ ๊ฑธ๋ฉด, ๋ค๋ฅธ ํธ๋์ญ์ ์ ํด๋น ๋ฐ์ดํฐ๋ฅผ ์ฝ์ ์๋ ์์ง๋ง ์์ ์ ๋ชปํจ.
-
์:
SELECT * FROM tags WHERE tag_name = 'choco' LOCK IN SHARE MODE;
โ ๋ค๋ฅธ ํธ๋์ญ์ ์
UPDATE๋DELETE,INSERT(ํด๋น ๋ ์ฝ๋ ํค ๊ฐ) ๋ถ๊ฐ.
-
์ด๋ค ํธ๋์ญ์ ์ด ๋ฐ์ดํฐ์ X-lock์ ๊ฑธ๋ฉด, ๋ค๋ฅธ ํธ๋์ญ์ ์ ์ฝ๊ธฐยท์ฐ๊ธฐ ๋ชจ๋ ๋ถ๊ฐ๋ฅ.
-
์:
SELECT * FROM tags WHERE tag_name = 'choco' FOR UPDATE;
โ ํด๋น ํ์ ๋ค๋ฅธ ํธ๋์ญ์ ์ด ์ฝ์ผ๋ ค๊ณ ํด๋ ๋๊ธฐ ์ํ.
- INSERT: ์๋ก์ด ํ์ X-lock (๋ฐฐํ ๋ฝ)
- UPDATE: ํด๋น ํ์ X-lock
- DELETE: ํด๋น ํ์ X-lock
- SELECT: ๊ธฐ๋ณธ์ ์ผ๋ก ๋ฝ ์์ (๋จ,
LOCK IN SHARE MODE๋๋FOR UPDATE์ต์ ์ฌ์ฉ ์ S/X-lock ๊ฑธ๋ฆผ)
-
๊ฐ์ฅ ๋จผ์ ์๊ฐํ ํด๊ฒฐ์ฑ ์ ํ๊ทธ ์กฐํ ์์ ์ X-lock์ ๊ฑธ์ด์ ๋ค๋ฅธ ํธ๋์ญ์ ์ด ๋์ผํ ํ๊ทธ์ ๋ํด ์กฐํ์กฐ์ฐจ ๋ชปํ๊ฒ ํ๋ ๊ฒ์ด์์ต๋๋ค.
S-lock(๊ณต์ ๋ฝ)์ ๊ฒฝ์ฐ ์ฌ๋ฌ ํธ๋์ญ์ ์ด ๋์์ ๊ฐ์ ๋ฐ์ดํฐ๋ฅผ ์กฐํํ ์ ์๊ธฐ ๋๋ฌธ์, ์กฐํ ํ ๊ฐ์ INSERT๋ฅผ ์๋ํ๋ฉด UNIQUE ์ ์ฝ์กฐ๊ฑด ์๋ฐ์ ๋ง์ ์ ์๊ฒ ์ง๋ง ์ฌ์ ํ ๋ฐ๋๋ฝ์ด ๋ฐ์ํ ๊ฒ์ผ๋ก ์์ํ์ต๋๋ค.
์์๋๋ก ๋ ์ง ํ์ธํ๊ธฐ ์ํด S-lock๊ณผ X-lock์ ๋ฒ๊ฐ์ ์ ์ฉํ๋ฉฐ ํ ์คํธ๋ฅผ ์งํํ์ต๋๋ค.
-
S-lock, X-lock ํ ์คํธ ๊ณผ์ ๊ณผ Gap lock
S-lock ์ ์ฉ ํ ์คํธ:
@Repository public interface TagRepository extends JpaRepository<Tag, Long> { @Lock(LockModeType.PESSIMISTIC_READ) // S Lock Optional<Tag> findByTagName(String tagName); boolean existsByTagName(String tagName); }
findByTagName๋ฉ์๋์ S-lock์ ์ ์ฉํ๊ณ ๋์์ฑ ํ ์คํธ๋ฅผ ์ํํ์ต๋๋ค.ํ ์คํธ ๊ฒฐ๊ณผ :
- ์์๋๋ก ๋ฐ๋๋ฝ์ด ๋ฐ์ํ์ต๋๋ค.
์ฝ์ ์๋ฎฌ๋ ์ด์ :
ํธ๋์ญ์ A
ํธ๋์ญ์ B
์ฝ์์์ ์ง์ ์๋ฎฌ๋ ์ด์ ํ ๊ฒฐ๊ณผ, ๋ ํธ๋์ญ์ ์ด ๋์์ 'hojun'์ด๋ผ๋ ๋์ผํ ํ๊ทธ๋ช ์ ๋ํด S-lock์ ํ๋ํ ํ ๊ฐ์ INSERT๋ฅผ ์๋ํ๋ฉด์ ๋ฐ๋๋ฝ์ ๋น ์ง๋ ๊ฒ์ ํ์ธํ์ต๋๋ค. S-lock์ผ๋ก๋ ์ด ๋ฌธ์ ๋ฅผ ๋ฐฉ์งํ ์ ์๋ค๋๊ฒ์ ํ์ธํ์ต๋๋ค.
X-lock ์ ์ฉ ํ ์คํธ:
@Repository public interface TagRepository extends JpaRepository<Tag, Long> { @Lock(LockModeType.PESSIMISTIC_WRITE) // X Lock Optional<Tag> findByTagName(String tagName); boolean existsByTagName(String tagName); }
S-lock๊ณผ ๋์ผํ๊ฒ
findByTagName๋ฉ์๋์ X-lock์ ์ ์ฉํ๊ณ ๋์์ฑ ํ ์คํธ๋ฅผ ์ํํ์ต๋๋ค.ํ ์คํธ ๊ฒฐ๊ณผ :
- ์์๊ณผ ๋ค๋ฅด๊ฒ ์ด์ ๊ณผ ๋๊ฐ์ด ๋ฐ๋๋ฝ์ด ๋ฐ์ํ์ต๋๋ค.
์ฝ์ ์๋ฎฌ๋ ์ด์ :
ํธ๋์ญ์ A
ํธ๋์ญ์ B
์ฒ์ ์๋ฎฌ๋ ์ด์ ํ์ ๋ ์์ํ๋ ์ ์, ํธ๋์ญ์ A์์ X-lock์ ๊ฑธ์์์๋ ๋ถ๊ตฌํ๊ณ ํธ๋์ญ์ B์์ ๊ฐ์ ํ๊ทธ๋ฅผ ์กฐํํ๋ ๊ฒ์ด ๊ฐ๋ฅํ๋ค๋ ๊ฒ์ ๋๋ค. ๊ทธ๋ฆฌ๊ณ INSERT ๋จ๊ณ์์๋ S-lock๊ณผ ๋์ผํ ๋ฉ์ปค๋์ฆ์ผ๋ก ๋ฐ๋๋ฝ์ด ๋ฐ์ํ์ต๋๋ค.
'์ฟผ๋ฆฌ๋ฌธ์ ์๋ชป ์์ฑํ๋?' ํ๋ ์๋ฌธ์ด ๋ค์ด ๋ค์ ๊ฒํ ํด๋ณด๋, ๊ณ์ SELECT๋ก ์กฐํํ๋ ํ๊ทธ๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์กด์ฌํ์ง ์๋ ๊ฐ์ด์์ต๋๋ค.
๊ฒ์ฆ ํ ์คํธ: ์ด๋ฏธ ์กด์ฌํ๋ ํ๊ทธ์ X-lock์ ๊ฑธ๊ณ ์กฐํํด๋ณด๋, ๋ค๋ฅธ ํธ๋์ญ์ ์์๋ ์กฐํ๊ฐ ๋ธ๋ก๋๋ ๊ฒ์ ํ์ธํ ์ ์์์ต๋๋ค. ๊ทธ๋ ๋ค๋ฉด ์กด์ฌํ์ง ์๋ ํ๊ทธ๋ฅผ INSERTํ ๋๋ ์ด๋ป๊ฒ ๋ฝ์ด ์๋ํ๋ ๊ฑธ๊น์?
ํ์ฌ ๊ฑธ๋ ค์๋ ๋ฝ์ ์ข ๋ฅ๋ฅผ ํ์ธํ๊ธฐ ์ํด ๋ค์ ์ฟผ๋ฆฌ๋ฅผ ์คํํ์ต๋๋ค
SELECT object_schema, object_name, lock_type, lock_mode FROM performance_schema.data_locks;
- S-lock ์ ์ฉ์
- X-lock ์ ์ฉ์
S๋ฝ๊ณผ X๋ฝ๋ง ๊ฑธ๋ฆฐ๊ฒ ์๋ GAP ์ด๋ผ๋ Lock์ด ๊ฑธ๋ฆฐ๊ฒ์ ํ์ธํ์ต๋๋ค.
Gap Lock์ ์๋ ์๋ฆฌ :
์ธ๋ฑ์ค ๋ ์ฝ๋ ์ฌ์ด์ ์กด์ฌํ Gap๋ค
- Gap Lock์ ์ธ๋ฑ์ค ๋ ์ฝ๋ ์ฌ์ด์ "๋น ๊ณต๊ฐ"์ ๊ฑธ๋ฆฌ๋ ๋ฝ์ ๋๋ค. ์ ์ฌ์ง์์๋ ๋ ์ฝ๋ ์ฌ์ด์ 1,2,3,4๋ฒ์ ํด๋นํ๋ ๊ณต๊ฐ์ ์ง์นญํฉ๋๋ค. ์กด์ฌํ์ง ์๋ ํ๊ทธ๋ช ์ ์กฐํํ ๋ ํด๋น ์ธ๋ฑ์ค ๊ฐญ์ ๋ฝ์ด ๊ฑธ๋ ค์, ๊ทธ ๋ฒ์์ ์๋ก์ด ๋ ์ฝ๋๊ฐ ์ฝ์ ๋๋ ๊ฒ์ ๋ฐฉ์งํฉ๋๋ค.
- ์ด๋ก ์ธํด ๋ ํธ๋์ญ์ ์ด ๋์ผํ ๊ฐญ์ ์๋ก ๋ค๋ฅธ ๋ฝ์ ์์ฒญํ๋ฉด์ ๋ฐ๋๋ฝ์ด ๋ฐ์ํ๊ฒ ๋ฉ๋๋ค.
๊ฒฐ๋ก : S-lock๊ณผ X-lock ๋ชจ๋ Gap Lock ๋ฉ์ปค๋์ฆ์ผ๋ก ์ธํด, ์ฒ์ ์์ฑ๋๋ ํ๊ทธ๋ค์ ๋ํ ๋์ INSERT ์ ๋ฐ๋๋ฝ ๋ฌธ์ ๋ฅผ ๊ทผ๋ณธ์ ์ผ๋ก ํด๊ฒฐํ ์ ์์์ ํ์ธํ์ต๋๋ค.
REQUIRES_NEW๋ฅผ ํตํ ์๋ก์ด ํธ๋์ญ์ ์ฒ๋ฆฌ ์๋:
๋ฝ ๊ธฐ๋ฐ ํด๊ฒฐ์ฑ ์ ํ๊ณ๋ฅผ ํ์ธํ ํ, ํ๊ทธ ์ ์ฅ ๋ถ๋ถ๋ง ์๋ก์ด ํธ๋์ญ์ ์ผ๋ก ๋ถ๋ฆฌํ์ฌ
: @Transactional(propagation = Propagation.REQUIRES_NEW)
์ค๋ฅ ๋ฐ์ ์ ์ฌ์๋ํ๋ ๋ฐฉ๋ฒ์ ์๋ํ์ต๋๋ค. ํ์ง๋ง ์ด ๋ฐฉ๋ฒ์ ์์๊ณผ ๋ฌ๋ฆฌ ์ ๋๋ก ์๋ํ์ง ์์์ต๋๋ค. ์์ธ์ ๋ถ์ํด๋ณด๋, ์๋ก์ด ํธ๋์ญ์ ์์ฑ์ผ๋ก ์ธํด
์ปค๋ฅ์ ํ ๋ถ์กฑ ํ์์ด ๋ฐ์ํ์ต๋๋ค.
- ๊ธฐ์กด ํธ๋์ญ์ (ํ์๊ฐ์ /๋ฌผํ๋ฑ๋ก)์ด ์ปค๋ฅ์ ์ ์ ์ ํ ์ํ
- ํ๊ทธ ์ ์ฅ์ฉ ์๋ก์ด ํธ๋์ญ์ ์ด ์ถ๊ฐ ์ปค๋ฅ์ ์ ์๊ตฌ
- ๋์ ์ ์์๊ฐ ๋ง์ ๋ ์ฌ์ฉ ๊ฐ๋ฅํ ์ปค๋ฅ์ ๋ถ์กฑ์ผ๋ก ์์ ์ฒ๋ฆฌ๊ฐ ๋ถ๊ฐ๋ฅํ ์ํฉ ๋ฐ์
@Retryable ์ด๋ ธํ ์ด์ ์ ์ฉ:
๊ทธ๋์ ํธ๋์ญ์
์ ๋ถ๋ฆฌํ์ง ์๊ณ , ๊ธฐ์กด์ ๋จ์ผ ํธ๋์ญ์
๋ด์์ ๋์์ฑ ์ค๋ฅ ๋ฐ์ ์ ์ฌ์๋ํ๋ ๋ฐฉํฅ์ผ๋ก ์ ๊ทผํ์ต๋๋ค. Spring์์ ์ ๊ณตํ๋ @Retryable ์ด๋
ธํ
์ด์
์ ํ์ฉํ์ต๋๋ค.
@Retryable(
retryFor = {CannotAcquireLockException.class, SQLTransientException.class,
IllegalStateException.class},
maxAttempts = 3, // ๊ธฐ๋ณธ๊ฐ
backoff = @Backoff(delay = 200) // ms ๋จ์
)
@Transactional
public UserRegisterResponse register(UserRegisterRequest userRegisterRequest,
UserRole userRole) {
... // ํ์๊ฐ์
๋ก์ง
}
@Retryable์ ๋์ ๋ฐฉ์:
retryFor: ์ง์ ๋ ์์ธ(๋ฝ, ๋ฐ์ ์์๋ง ์ฌ์๋ ์ํmaxAttempts: ์ต๋ ์ฌ์๋ ํ์ ์ค์ (๊ธฐ๋ณธ๊ฐ 3ํ)backoff: ์ฌ์๋ ๊ฐ๊ฒฉ ์ค์ (200ms ์ง์ฐ์ผ๋ก ์๊ฐ์ ์ธ ๋์์ฑ ์ถฉ๋ ํํผ)
ํ ์คํธ ๊ฒฐ๊ณผ: ๋์์ฑ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ ๋ 200ms ๋๋ ์ด๋ฅผ ๋๊ณ ์๋์ผ๋ก ์ฌ์๋ํ๊ฒ ํจ์ผ๋ก์จ, JMeter ํ ์คํธ์์ ๋ชจ๋ ํ์๊ฐ์ ๊ณผ ๋ฌผํ๋ฑ๋ก์ด ์ฑ๊ณต์ ์ผ๋ก ์๋ฃ๋๋ ๊ฒ์ ํ์ธํ์ต๋๋ค.
@Retryable ์ ์ฅ์ :
- ๋ณต์กํ ๋ฉ์ปค๋์ฆ ์์ด ๊ฐ๋จํ ์ค์ ์ผ๋ก ๋์์ฑ ๋ฌธ์ ํด๊ฒฐ๊ฐ๋ฅ
- ๊ธฐ์กด ํธ๋์ญ์ ๊ตฌ์กฐ ์ ์ง๋ก ์ปค๋ฅ์ ํ ๋ถ์กฑ ๋ฌธ์ ๋ฐฉ์ง
@Retryable ์ ๋จ์ :
๊ทผ๋ณธ์ ํด๊ฒฐ์ด ์๋ ์ฐํ ๋ฐฉ์
- ๋์์ฑ ๋ฌธ์ ์ ์์ธ์ ์ ๊ฑฐํ ๊ฒ์ด ์๋๋ผ ์ค๋ฅ ๋ฐ์ ์ ์ฌ์๋๋ก ํํผํ๋ ๋ฐฉ์
- ๋์ ์ ์์๊ฐ ๋งค์ฐ ๋ง์์ง ๊ฒฝ์ฐ ์ฌ์๋ ํ์๊ฐ ์ฆ๊ฐํ์ฌ ์ฑ๋ฅ ์ ํ ๊ฐ๋ฅ์ฑ
์์ธก ๋ถ๊ฐ๋ฅํ ์๋ต ์๊ฐ
- ์ฌ์๋๋ก ์ธํด ์ฌ์ฉ์ ์์ฒญ ์ฒ๋ฆฌ ์๊ฐ์ด ๋ถ๊ท์นํด์ง (200ms ~ 600ms ์ถ๊ฐ ์ง์ฐ ๊ฐ๋ฅ)
- ํ์ฌ๋ 3๋ฒ ๋ฐ๋ณต์ผ๋ก ๋ฌธ์ ๊ฐ ์์๊ฒผ์ง๋ง ์ต์ ์ ๊ฒฝ์ฐ 3๋ฒ ๋ชจ๋ ์คํจํ๋ฉด ์ฌ์ ํ ์๋ฌ ๋ฐ์ ๊ฐ๋ฅ
๊ฐ์ ๋ฐฉํฅ :
ํ์ฌ์ @Retryable ๋ฐฉ์์ ์์๋ฐฉํธ์ด๋ผ๊ณ ํ๋จ๋์ด, ํฅํ์๋ MySQL์ ๊ตฌ๋ฌธ์ ํ์ฉํ์ฌ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ ๋ฒจ์์ ๋์์ฑ ๋ฌธ์ ๋ฅผ ๊ทผ๋ณธ์ ์ผ๋ก ํด๊ฒฐํ๊ฑฐ๋, Redis๋ฅผ ํ์ฉํ ํ๊ทธ ์บ์ฑ ๋ฐ ๋ถ์ฐ ๋ฝ ๊ตฌํ์ ํตํด ๋ ์ ๊ตํ ๋์์ฑ ์ ์ด๋ฅผ ๋์
ํ ์์ ์
๋๋ค. ๋ํ ์ธ๊ธฐ ํ๊ทธ ์ฌ์ ์ ์์ฑ(ํ์ค์ ์ผ๋ก ๊ฐ์ฅ ์ฌ์ด ๋ฐฉ๋ฒ)ํ์ฌ ์ค๋ณต์ ์์ ๋ ๋ฐฉ๋ฒ๊ณผ ์ฌ์๋ ๋น๋ ํ
์คํธํ์ฌ ๋์์ฑ ์ถฉ๋ ์์ฒด๋ฅผ ์๋ฐฉํ๊ณ ์์คํ
์ฑ๋ฅ์ ์ง์์ ์ผ๋ก ๊ด์ฐฐํ ๊ณํ์
๋๋ค.
๋๋์ :
์์ผ๋ก ์๋ก์ด ๋น์ฆ๋์ค ๋ก์ง์ ์ค๊ณํ ๋๋ ๋์์ฑ๊ณผ ํธ๋์ญ์ ์ ํ ๋ฒ ๋ ๊ผผ๊ผผํ ๊ณ ๋ คํ ๋ค์ ๊ตฌํํด์ผ๊ฒ ๋ค๊ณ ๋๊ผ์ต๋๋ค.
๐จย Redis ์ง๋ ฌํ ๋ฌธ์
์ฒ์์๋ Redis ์บ์ ๋งค๋์ ์ null ๊ฐ ์บ์ฑ ๋ฐฉ์ง์ TTL ๋ง๋ฃ ์๊ฐ๋ง ์ค์ ํ ๋ค,
์์์ ์ ์ฒด ์กฐํ ๋ฉ์๋์ @Cacheable์ ์ ์ฉํด ์บ์ฑ์ ๊ตฌํํ์ต๋๋ค.
ํ์ง๋ง ์์์ ์ ์ฒด ์กฐํ๋ฅผ ํธ์ถํ์ ๋ค์๊ณผ ๊ฐ์ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.
-
์ต์ด ์ฝ๋
// build.gradle dependencies { // redis ์บ์ implementation 'org.springframework.boot:spring-boot-starter-data-redis' //json ์ง๋ ฌํ๋ฅผ ์ํ jackson implementation 'com.fasterxml.jackson.core:jackson-databind' implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' }
@Configuration @EnableCaching public class CacheConfig { @Bean public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) { RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig() .disableCachingNullValues() // null์ ์บ์์ ์ ์ฅํ์ง ์์ .entryTtl(Duration.ofMinutes(10)); // TTL 10๋ถ return RedisCacheManager.builder(redisConnectionFactory) .cacheDefaults(cacheConfig) .build(); } }
@Service @RequiredArgsConstructor public class NewsfeedService { private final NewsfeedRepository newsfeedRepository; private final UserService userService; private final ImageService imageService; @Loggable @Transactional(readOnly = true) @Cacheable( value = "listCache", key = "#pageable.pageNumber + '-' + #pageable.pageSize + '-' + #pageable.sort.toString()" ) public PageResponseDto<NewsfeedListResponse> getAllNewsfeeds(Pageable pageable) { // ์์์ ์ ์ฒด ์กฐํ(์ํํธ๋๋ฆฌํธ ๋นผ๊ณ ) Page<Newsfeed> pagedNewsfeeds = newsfeedRepository.findByIsDeletedFalseWithFetchJoin(pageable); // ํ์ด์ง์ ๋ค์ด๊ฐ ๋ํ ์ด๋ฏธ์ง ์ผ๊ด ์กฐํ Map<Long, Image> mainImageMap = imageService.getMainImagesForNewsfeeds(pagedNewsfeeds); // NewsfeedListResponse์ ์ ์ฉ (+ ์ด๋ฏธ์ง ๋งตํ) Page<NewsfeedListResponse> dtoPage = pagedNewsfeeds.map(newsfeed -> { Image mainImage = mainImageMap.getOrDefault(newsfeed.getId(), null); return NewsfeedListResponse.of(newsfeed, mainImage.getImageUrl()); }); return new PageResponseDto<>(dtoPage); } }
2025-08-12 14:52:35.328 15137 ERROR [http-nio-8080-exec-3] o.a.c.c.C.[.[.[.[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.data.redis.serializer.SerializationException: Cannot serialize] with root cause
java.lang.IllegalArgumentException: DefaultSerializer requires a Serializable payload but received an object of type [com.example.place.common.dto.PageResponseDto]Redis ์บ์ฑ ๊ณผ์ ์์ ๊ฐ์ฒด ์ง๋ ฌํ ์ค์ ์ด ์์ด์ ๊ธฐ๋ณธ JdkSerializationRedisSerializer ๋ฐฉ์์ด ์ฌ์ฉ๋์๊ณ , ์ด๋ก ์ธํด DTO๊ฐ ์ง๋ ฌํ/์ญ์ง๋ ฌํ๋์ง ๋ชปํด ์์ธ๊ฐ ๋ฐ์ํ์์ต๋๋ค.
-
Spring CacheManager ์ง๋ ฌํ ๋ฐฉ์์ JSON์ผ๋ก ๋ณ๊ฒฝ
// CacheConfig.cacheManager() // ์บ์ ์ค์ RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig() .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) // ํค๋ ๋ฌธ์์ด ์ง๋ ฌํ .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())) // ๊ฐ์ JSON ์ง๋ ฌํ .disableCachingNullValues() // null์ ์บ์์ ์ ์ฅํ์ง ์์ .entryTtl(Duration.ofMinutes(10)); // TTL 10๋ถ
- ์ง๋ ฌํ ๋ฐฉ์์ ์ง์ :
- ํค ์ง๋ ฌํ๋ StringRedisSerializer ์ฌ์ฉํด ๋ฌธ์์ด๋ก ์ง์
- ๊ฐ ์ง๋ ฌํ GenericJackson2JsonRedisSerializer ์ฌ์ฉํด Jsonํํ๋ก ์ง์
- ์ง๋ ฌํ ๋ฐฉ์์ ์ง์ :
-
๊ฐ๋ณ ๋ฆฌ์คํธ ๋ณํ
// newsfeedService.getAllNewsfeeds() List<NewsfeedListResponse> contentList = pagedNewsfeeds.stream().map(newsfeed -> { Image mainImage = mainImageMap.getOrDefault(newsfeed.getId(), null); return NewsfeedListResponse.of(newsfeed, mainImage != null ? mainImage.getImageUrl() : null); }) .collect(Collectors.toList()); // ๊ฐ๋ณ ๋ฆฌ์คํธ๋ก ๋ณํ return new PageResponseDto<>( new PageImpl<>(contentList, pageable, pagedNewsfeeds.getTotalElements()) );
Page์content๊ฐ ๋ถ๋ณ ๋ฆฌ์คํธ์ผ ๊ฒฝ์ฐ ์ง๋ ฌํ ๊ณผ์ ์์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์๋ค๊ณ ํ๋จํ์ฌ,Collectors.toList()๋ฅผ ์ฌ์ฉํด ๊ฐ๋ณ ๋ฆฌ์คํธ๋ก ๋ณํ ํ ์๋ต๊ฐ์ผ๋ก ์ ๋ฌ
-
LocalDateTime ์ง๋ ฌํ ์ฒ๋ฆฌ
// CacheConfig.cacheManager() // ์๋ฐ ํ์ ๋ชจ๋ ๋ฑ๋กํ์ฌ LocalDateTime -> ISO-8601 ํํ๋ก ์ง๋ ฌํ ObjectMapper mapper = new ObjectMapper(); mapper.registerModule(new JavaTimeModule()); mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(mapper); // ์บ์ ์ค์ RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig() .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) // ํค๋ ๋ฌธ์์ด ์ง๋ ฌํ .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer)) // ๊ฐ์ JSON ์ง๋ ฌํ .disableCachingNullValues() // null์ ์บ์์ ์ ์ฅํ์ง ์์ .entryTtl(Duration.ofMinutes(10)); // TTL 10๋ถ
java.time.LocalDateTime์ ๊ธฐ๋ณธ ์ค์ ์์ ์ง๋ ฌํ๊ฐ ์ง์๋์ง ์์ ์ฌ์ ํ ์์ธ๊ฐ ๋ฐ์- ์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด
JavaTimeModule์ ๋ฑ๋กํ๊ณ ,WRITE_DATES_AS_TIMESTAMPS์ต์ ์ ๋นํ์ฑํํด ISO-8601 ๋ฌธ์์ด๋ก ์ง๋ ฌํ/์ญ์ง๋ ฌํ๋๋๋ก ์ค์
-
ํ์ ์ ๋ณด ์ ์ง
// CacheConfig.cacheManager() // ํ์ ์ ๋ณด ํฌํจ (์ง๋ ฌํ/์ญ์ง๋ ฌํ ์ ํด๋์ค ์ ๋ณด ์ ์ง) mapper.activateDefaultTyping( LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY );
- ์ง๋ ฌํ ๊ฐ๋ฅ.
๊ทธ๋ฌ๋ ์ฌ์ ํ ์ญ์ง๋ ฌํ ์ ๋ฐํ์์ ์ ๋ค๋ฆญ ํ์
์ ๋ณด๋ฅผ ์ ๋๋ก ์ ์ ์์ด,
Jackson์ด
LinkedHashMap๊ฐ์ ๊ธฐ๋ณธ ๊ตฌ์กฐ๋ก ๋ณํํด๋ฒ๋ฆฌ๋ ๋ฌธ์ ๋ฐ์ - ์ด๋ฅผ ๋ฐฉ์งํ๊ธฐ ์ํด
ObjectMapper.activateDefaultTyping์ ์ฌ์ฉํด JSON์ ํด๋์ค ํ์ ์ ๋ณด๋ฅผ ํจ๊ป ์ ์ฅํ๋๋ก ์ค์
- DTO ์์ฑ์์ @JsonCreator/@JsonProperty ์ ์ฉ
// PageResponseDto @JsonCreator private PageResponseDto( @JsonProperty("content") List<T> content, @JsonProperty("page") int page, @JsonProperty("totalPages") int totalPages) { this.content = content; this.page = page; this.totalPages = totalPages; }
- ๋ถ๋ณ ๊ฐ์ฒด๋
finalํ๋๋ง ์๋ DTO๋ Jackson์ด ๊ธฐ๋ณธ ์์ฑ์ ์์ด ์ญ์ง๋ ฌํํ ์ ์๋ ๋ฌธ์ ๋ฐ์ @JsonCreator์@JsonProperty๋ฅผ ์ฌ์ฉํด ์ญ์ง๋ ฌํ ์ ํ๋ ๋งคํ์ด ๊ฐ๋ฅํ๋๋ก ๋ช ์
- ์ง๋ ฌํ ๊ฐ๋ฅ.
๊ทธ๋ฌ๋ ์ฌ์ ํ ์ญ์ง๋ ฌํ ์ ๋ฐํ์์ ์ ๋ค๋ฆญ ํ์
์ ๋ณด๋ฅผ ์ ๋๋ก ์ ์ ์์ด,
Jackson์ด
-
์ต์ข ์ฝ๋
// build.gradle dependencies { // redis ์บ์ implementation 'org.springframework.boot:spring-boot-starter-data-redis' //json ์ง๋ ฌํ๋ฅผ ์ํ jackson implementation 'com.fasterxml.jackson.core:jackson-databind' implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' }
@Configuration @EnableCaching public class CacheConfig { @Bean public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) { // ์๋ฐ ํ์ ๋ชจ๋ ๋ฑ๋กํ์ฌ LocalDateTime -> ISO-8601 ํํ๋ก ์ง๋ ฌํ ObjectMapper mapper = new ObjectMapper(); mapper.registerModule(new JavaTimeModule()); mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); // ํ์ ์ ๋ณด ํฌํจ (์ง๋ ฌํ/์ญ์ง๋ ฌํ ์ ํด๋์ค ์ ๋ณด ์ ์ง) mapper.activateDefaultTyping( LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY ); GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(mapper); // ์บ์ ์ค์ RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig() .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) // ํค๋ ๋ฌธ์์ด ์ง๋ ฌํ .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer)) // ๊ฐ์ JSON ์ง๋ ฌํ .disableCachingNullValues() // null์ ์บ์์ ์ ์ฅํ์ง ์์ .entryTtl(Duration.ofMinutes(10)); // TTL 10๋ถ return RedisCacheManager.builder(redisConnectionFactory) .cacheDefaults(cacheConfig) // ์บ์ ์ค์ .build(); } }
@Service @RequiredArgsConstructor public class NewsfeedService { private final NewsfeedRepository newsfeedRepository; private final UserService userService; private final ImageService imageService; @Loggable @Transactional(readOnly = true) @Cacheable( value = "listCache", key = "#pageable.pageNumber + '-' + #pageable.pageSize + '-' + #pageable.sort.toString()" ) public PageResponseDto<NewsfeedListResponse> getAllNewsfeeds(Pageable pageable) { // ์์์ ์ ์ฒด ์กฐํ(์ํํธ๋๋ฆฌํธ ๋นผ๊ณ ) Page<Newsfeed> pagedNewsfeeds = newsfeedRepository.findByIsDeletedFalseWithFetchJoin(pageable); // ํ์ด์ง์ ๋ค์ด๊ฐ ๋ํ ์ด๋ฏธ์ง ์ผ๊ด ์กฐํ Map<Long, Image> mainImageMap = imageService.getMainImagesForNewsfeeds(pagedNewsfeeds); List<NewsfeedListResponse> contentList = pagedNewsfeeds.stream().map(newsfeed -> { Image mainImage = mainImageMap.getOrDefault(newsfeed.getId(), null); return NewsfeedListResponse.of(newsfeed, mainImage != null ? mainImage.getImageUrl() : null); }) .collect(Collectors.toList()); // ๊ฐ๋ณ ๋ฆฌ์คํธ๋ก ๋ณํ return new PageResponseDto<>( new PageImpl<>(contentList, pageable, pagedNewsfeeds.getTotalElements()) ); } }
- JPA์์ ๋ฐํํ๋
Page.getContent()๋ ๋ณดํต ๋ถ๋ณ ๋ฆฌ์คํธ๋ก ๊ฐ์ธ์ ธ ์์ด์, ์ง์ ์บ์์ ๋ฃ์ผ๋ฉด ์ง๋ ฌํ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์์. PageResponseDto๋ด๋ถcontentํ๋๋Page.getContent()๋ฅผ ๋ฐ์์ ์ด๊ธฐํํ๋ฏ๋ก, ์ ๋ฌํ๋Page์ ๋ฆฌ์คํธ๊ฐ ๊ฐ๋ณ ๋ฆฌ์คํธ์ฌ์ผ ํจ.- ์๋น์ค ๋ ์ด์ด์์
Page<T>์ ๋ด์ฉ์ ๊ฐ๋ณ ๋ฆฌ์คํธ๋ก ์๋ก ์์ง ํ, ๊ทธ ๋ฆฌ์คํธ๋ฅผ ์ฌ์ฉํดPageImpl์ ์๋ก ๋ง๋ค๋ฉด, ๊ทธ ๋ด๋ถ ๋ฆฌ์คํธ๋ ๊ฐ๋ณ ๋ฆฌ์คํธ๊ฐ ๋จ. PageImpl์ ๋ฆฌ์คํธ๋ฅผ ๋ณต์ฌํ๊ฑฐ๋ ๋ถ๋ณ์ผ๋ก ๊ฐ์ธ์ง ์๊ธฐ ๋๋ฌธ์, ๊ฐ๋ฐ์๊ฐ ๋๊ธด ๊ฐ๋ณ ๋ฆฌ์คํธ๋ฅผ ๊ทธ๋๋ก ์ ์งํจ.
- JPA์์ ๋ฐํํ๋
์ด ๊ณผ์ ์ ํตํด Redis ์บ์์์ DTO๋ฅผ ์์ ํ๊ฒ ์ ์ฅยท์กฐํํ๋ ค๋ฉด,
๊ฐ ์ง๋ ฌํ ์ JSON ๋ณํ + ํ์ ์ ๋ณด ํฌํจ + ์ญ์ง๋ ฌํ๋ฅผ ์ํ ์์ฑ์ ์ค์ ์ด ํจ๊ป ๊ณ ๋ ค๋์ด์ผ ํ๋ค๋ ๊ฒ์ ์ ์ ์์์ต๋๋ค.
๐จย ์ํ ์ฐธ์กฐ ๋ฌธ์
- OrderService์์ ItemService ์์กด, ItemService์์๋ ์ฌ๊ณ ๊ด๋ฆฌ๋ฅผ ์ํด OrderService ์ฐธ์กฐ ํ์
- ์๋ฐฉํฅ ์์กด์ฑ์ผ๋ก ์ธํ ์ํ์ฐธ์กฐ ๋ฐ์
- OrderService โ ItemService(์ํ ์ ๋ณด ์กฐํ)
- ItemService โ OrderService (์ฌ๊ณ ๊ด๋ฆฌ ๋ก์ง)
- ๋ ์๋น์ค ๊ฐ ์ํธ ์ฐธ์กฐ๋ก ์ธํ ์์กด์ฑ ์ํ ๊ตฌ์กฐ
- ์ฌ๊ณ ๊ด๋ฆฌ ๋ก์ง์ ๋ณ๋์
StockService๋ก ๋ถ๋ฆฌ - OrderService โ StockService, ItemService โ StockService(์์กด์ฑ ๊ตฌ์กฐ ๊ฐ์ )
์ด๊ธฐ ์ค๊ณ๋จ๊ณ์์ ์์กด์ฑ ๊ด๊ณ๋ฅผ ์ถฉ๋ถํ ๊ฒํ ํ์ง ๋ชปํ์์ต๋๋ค
ํฅํ ์๋ก์ด ์๋น์ค ์ถ๊ฐ ์ ์์กด์ฑ ๋ค์ด์ด๊ทธ๋จ ์ฌ์ ์์ฑ์ ์๋ํด ๋ณด๊ณ ์ ํฉ๋๋ค.
๐จย ๋ถ์ฐ ๋ฝ ๊ตฌํ ์ ํธ๋์ญ์ ์ ํ ์ด์
๋ถ์ฐ ๋ฝ๊ณผ ํธ๋์ญ์
๊ฒฝ๊ณ ์ถฉ๋ ๋ฌธ์
๋ถ์ฐ ๋ฝ์ ๋์
ํ๋ฉด์ ๊ธฐ์กด ํธ๋์ญ์
๊ด๋ฆฌ ๋ฐฉ์๊ณผ ์ถฉ๋์ด ๋ฐ์ํ์ต๋๋ค. ํนํ OrderService์์ StockService๋ฅผ ํธ์ถํ ๋ ํธ๋์ญ์
๊ฒฝ๊ณ์ ๋ฝ์ ์๋ช
์ฃผ๊ธฐ๊ฐ ๋ง์ง ์์ ์์๊ณผ ๋ค๋ฅธ ๋์์ด ๋ฐ์ํ์ต๋๋ค.
๋ฐ์ ๋ฌธ์
- ๋ฝ์ด ํด์ ๋ ํ ํธ๋์ญ์ ์ด ์ปค๋ฐ๋์ด ๋์์ฑ ์ ์ด๊ฐ ๋ฌด์๋ฏธํด์ง
- ํธ๋์ญ์ ๋กค๋ฐฑ ์ ๋ฝ์ ์ด๋ฏธ ํด์ ๋์ด ๋ค๋ฅธ ์ค๋ ๋๊ฐ ์๋ชป๋ ๋ฐ์ดํฐ์ ์ ๊ทผ
- ์ธ๋ถ ํธ๋์ญ์ ๊ณผ ๋ด๋ถ ํธ๋์ญ์ ์ ๊ฒฝ๊ณ๊ฐ ๋ชจํธํ์ฌ ๋ฐ์ดํฐ ์ผ๊ด์ฑ ๋ฌธ์ ๋ฐ์
๋ฌธ์ ๋ฐ์ ๊ตฌ๊ฐ
// ๋ฌธ์ ๋ฐ์ ๊ตฌ๊ฐ
@Transactional // ์ธ๋ถ ํธ๋์ญ์
์์
public CreateOrderResponseDto createOrder(...) {
stockService.decreaseStock(itemId, quantity); // ๋ด๋ถ์์ ๋ฝ ํ๋/ํด์
// ์ฃผ๋ฌธ ์์ฑ ๋ฐ ์ ์ฅ
orderRepository.save(order);
// ์ธ๋ถ ํธ๋์ญ์
์ปค๋ฐ (๋ฝ์ ์ด๋ฏธ ํด์ ๋ ์ํฉ)
}๋ฌธ์ ์ ์ํ์ค
- ์ฌ์ฉ์A: ์ธ๋ถ ํธ๋์ญ์ ์์ โ ๋ฝ ํ๋ โ ์ฌ๊ณ ์ฐจ๊ฐ โ ๋ฝ ํด์
- ์ฌ์ฉ์B: ๋ฝ ํ๋ ๊ฐ๋ฅ โ ์ฌ๊ณ ์ฐจ๊ฐ (์ฌ์ฉ์A ํธ๋์ญ์ ๋ฏธ์ปค๋ฐ ์ํ)
- ์ฌ์ฉ์A: ํธ๋์ญ์ ์ปค๋ฐ
- ์ฌ์ฉ์B: ํธ๋์ญ์ ์ปค๋ฐ
- ๊ฒฐ๊ณผ: ๋์์ฑ ์ ์ด ์คํจ, ์ฌ๊ณ ์ค์ฐจ ๋ฐ์
ํธ๋์ญ์ ์ ํ ๋ฐฉ์์ ๋ฌธ์
- ๊ธฐ์กด์๋ ๊ธฐ๋ณธ ์ ํ ๋ฐฉ์์ธ
REQUIRED๋ฅผ ์ฌ์ฉํ๋๋ฐ, ์ด๋ ์ธ๋ถ ํธ๋์ญ์ ์ ์ฐธ์ฌํ๋ ๋ฐฉ์์ด์์ต๋๋ค.
- ๋ฝ์ ์๋ช ์ฃผ๊ธฐ: ๋ฉ์๋ ์คํ ์๊ฐ ๋์๋ง ์ ์ง
- ํธ๋์ญ์ ์๋ช ์ฃผ๊ธฐ: ์ธ๋ถ ๋ฉ์๋ ์๋ฃ๊น์ง ์ ์ง
- ๊ฒฐ๊ณผ: ๋ฝ์ด ํด์ ๋ ํ์๋ ํธ๋์ญ์ ์ด ์ด์์์ด ๋ฐ์ดํฐ ์ผ๊ด์ฑ ๋ณด์ฅ ๋ถ๊ฐ
๋ ๋ฆฝ์ ์ธ ํธ๋์ญ์ ์ ์์ฑํ์ฌ ๋ฝ๊ณผ ํธ๋์ญ์ ์ ์๋ช ์ฃผ๊ธฐ๋ฅผ ์ผ์น์์ผฐ์ต๋๋ค.
// ํด๊ฒฐ๋ ์ฝ๋
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void decreaseStock(Long itemId, Long quantity) {
String lockKey = "Lock:" + itemId;
RLock lock = redissonClient.getLock(lockKey);
try {
boolean acquired = lock.tryLock(3000, 10000, TimeUnit.MILLISECONDS);
if (!acquired) {
throw new CustomException(ExceptionCode.STOCK_LOCK_FAILED);
}
try {
// ๋
๋ฆฝ์ ์ธ ํธ๋์ญ์
์์ DB ์์
์ํ
Item item = itemRepository.findByIdWithLock(itemId)
.orElseThrow(() -> new CustomException(ExceptionCode.NOT_FOUND_ITEM));
item.decreaseStock(quantity);
// ๋ฉ์๋ ์ข
๋ฃ ์ ํธ๋์ญ์
์ฆ์ ์ปค๋ฐ
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock(); // ํธ๋์ญ์
์ปค๋ฐ ํ ๋ฝ ํด์
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new CustomException(ExceptionCode.OPERATION_INTERRUPTED);
}
}๊ฐ์ ์ฌํญ
1. ํธ๋์ญ์ ๊ฒฉ๋ฆฌ
- ์ฌ๊ณ ๊ด๋ฆฌ ๋ก์ง์ ๋ ๋ฆฝ์ ์ธ ํธ๋์ญ์ ์ผ๋ก ๋ถ๋ฆฌ
- ๋ฝ ํด์ ์ ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ณ๊ฒฝ์ฌํญ ํ์คํ ์ปค๋ฐ
- ์ธ๋ถ ํธ๋์ญ์ ๊ณผ ๋ฌด๊ดํ๊ฒ ์ฌ๊ณ ์ฒ๋ฆฌ ์๋ฃ
2. ์์์ฑ ๋ณด์ฅ
- ๋ฝ ํ๋ โ DB ์์ โ ํธ๋์ญ์ ์ปค๋ฐ โ ๋ฝ ํด์ ์์ ๋ณด์ฅ
- ์ค๊ฐ์ ์คํจ ์ ํธ๋์ญ์ ๋กค๋ฐฑ ํ ๋ฝ ํด์
- ๋ค๋ฅธ ์ค๋ ๋๋ ์์ ํ ์ฒ๋ฆฌ๋ ๋ฐ์ดํฐ๋ง ์ ๊ทผ ๊ฐ๋ฅ
์ํ๋ ์
- ๋ฌธ์ ๋ถ์
- ๋จ์ํ "๋์์ฑ ๋ฌธ์ "๋ก ๋๋ด์ง ์๊ณ ํธ๋์ญ์ ์ ํ ๋ฐฉ์๊น์ง ๊น์ด ์๊ฒ ๋ถ์
- ๋ฝ๊ณผ ํธ๋์ญ์ ์ ์๋ช ์ฃผ๊ธฐ ์ฐจ์ด๋ฅผ ์ ํํ ํ์ ํ๊ณ ํด๊ฒฐ๋ฐฉ์ ๋์ถ
- ์์ ์ฑ ๊ณ ๋ ค
- REQUIRES_NEW๋ก ์ธํ ์ฑ๋ฅ ์ค๋ฒํค๋๋ฅผ ์ธ์งํ๋ฉด์๋ ๋ฐ์ดํฐ ์ผ๊ด์ฑ์ ์ฐ์ ์ํจ
์์ฌ์ด์
- ์ด๊ธฐ ์ค๊ณ ๋จ๊ณ์์ ์ง์๋ถ์กฑ
- ๋ถ์ฐ ๋ฝ ๋์ ์ ํธ๋์ญ์ ์ ํ ๋ฐฉ์์ ๋ํ ์ฌ์ ๊ฒํ ๋ถ์กฑ
- ๋์์ฑ ํ ์คํธ ์๋๋ฆฌ์ค๋ฅผ ์ถฉ๋ถํ ๊ตฌ์ฑํ์ง ๋ชปํด ๋ฆ์ ๋ฐ๊ฒฌ
| ๊ตฌ๋ถ | ๊ธฐ๊ฐ | ํ๋ | ๋น๊ณ |
|---|---|---|---|
| ๊ธฐํ | 2025.07.16 ~ 07.20 | ์์ด๋์ด ํ์ ๋ฐ S.A ์์ฑ | S.A ํผ๋๋ฐฑ |
| MVP | 2025.07.21 ~ 07.28 | ์ต์ ๊ธฐ๋ฅ ๊ฐ๋ฐ | MVP ์์ฐ |
| ์คํ๋ฆฐํธ 1์ฐจ | 2025.07.29 ~ 08.05 | ์ถ๊ฐ ๊ธฐ๋ฅ ๊ฐ๋ฐ | ์คํ๋ฆฐํธ ํ๊ณ ํ ๋ฐฐํฌ |
| ์คํ๋ฆฐํธ 2์ฐจ | 2025.08.06 ~ 08.14 | ๊ณ ๋ํ | ์คํ๋ฆฐํธ ํ๊ณ ํ ๋ฐฐํฌ & 5๋ถ ๋ธ๋ฆฌํ |
| ๋ฐํ ์ค๋น | 2025.08.16 ~ 08.24 | ๋ธ๋ก์ ๋ฐ ๋ฐํ์๋ฃ ์ ์ | ์ต์ข ํ๋ก์ ํธ ์ ์ถ |
์ค์ ํ๋ฐฐ์ฌ API ์ฐ๋ - API ์ธ์ฆ ์ ์ฐจ ๋ฐ ๋ฐฐ์ก ์ถ์ ๊ธฐ๋ฅ ๊ตฌํ
๊ฒฐ์ ์์คํ ์์ฑ - ์ค์ ๊ฒฐ์ API ์ฐ๋์ผ๋ก ์์ฑ๋ ํฅ์
ํ์๊ฐ์ API ์ฑ๋ฅ ์ต์ ํ - ํ์ฌ ๋๋ฆฐ ์๋ต ์๋ ๊ฐ์
๊ฒ์์ด ๋ญํน ๊ธฐ๋ฅ ์ต์ ํ - ๊ฒ์ ์ฑ๋ฅ ๋ฐ ์ ํ๋ ํฅ์
์บ์ฑ fallback ๋ฉ์ปค๋์ฆ & Pre-heating ์์คํ ๊ตฌ์ถ - ์์คํ ์์ ์ฑ ํ๋ณด
๋ชจ๋ํฐ๋ง ๋ฐ ์๋ฆผ ์ฒด๊ณ ๊ตฌ์ถ - ์ค์๊ฐ ์์คํ ์ํ ๋ชจ๋ํฐ๋ง
์ฑํ ๊ธฐ๋ฅ ์์ธ ์ฒ๋ฆฌ ๋ณด๊ฐ - ๋ฉ์์ง ์ ์ก ์คํจ ์ ์ฌ์๋ ๋ก์ง ๋ฐ Dead Letter Queue ๋์
ํ ์คํธ ์ฝ๋ ํ์ถฉ - ์ด์ ํ๊ฒฝ ๊ฐ์ ํ ๋ค์ํ ํ ์คํธ๋ก ์์ ์ฑ ๊ฐํ