- ์๋น์ค๋ช : DailyQ
- ๋ชฉํ: ์ทจ์
์ค๋น์์ด ํ๋ฃจ 5๋ถ, ํ๋ฃจ์ ํ๋์ฉ ๋ฉด์ ์ง๋ฌธ์ ๋ต๋ณํ๋ฉด์
- ๊พธ์คํ ์ฐ์ตํ๋ ์ต๊ด์ ๋ง๋ค๊ณ
- AI ํผ๋๋ฐฑ์ ํตํด ๋ถ์กฑํ ์ ์ ๋ณด์ํ๋ฉฐ
- ๋ฉด์ ์ค๋ ฅ์ ๋์ด์ฌ๋ฆด ์ ์๊ฒ ํจ.
- ํต์ฌ ๊ฐ์น: ๋ฌด๊ฒ์ง ์์ '1์ผ 1๋ฌธํญ' ๋ฃจํด โ ๊พธ์คํ ํ์ต โ ์ฑ์ฅ ์ฒด๊ฐ โ ๋๊ธฐ ์ ์ง
- ์ทจ์
์ค๋น์์ ํ์ต ํจํด ๋ฌธ์
- ๋ฉด์ ๋๋น๋ฅผ ๊พธ์คํ ํ๊ณ ์ถ์ง๋ง, ๋งค์ผ ๋ฌด์์ ์ค๋นํด์ผ ํ ์ง ์ฒด๊ณ๊ฐ ์์
- ์๊ธฐ ๋ต๋ณ์ ๊ธฐ๋กํ๊ฑฐ๋ ํผ๋๋ฐฑ ๋ฐ์ ๊ธฐํ๊ฐ ๋ถ์กฑํด, ์์ ์ ์ฑ์ฅ ์ ๋๋ฅผ ์ธก์ ํ๊ธฐ ์ด๋ ค์
- ํผ์ ๊ณต๋ถํ๋ค ๋ณด๋ ์ง๋ฃจํด์ง๊ณ , ๋๊ธฐ๋ถ์ฌ๊ฐ ๊ธ๋ฐฉ ๋จ์ด์ ธ ํ์ต์ ์ค๋จํ๋ ๊ฒฝ์ฐ๊ฐ ๋ง์
- ๊ธฐ์กด ์๋น์ค์ ํ๊ณ
- ๋ชจ์ ๋ฉด์ (์: ์ฌ๋์ธ AI ๋ฉด์ )
- 1ํ ๋จ๊ฐ๊ฐ ๋์ ์์ฃผ ์ด์ฉํ๊ธฐ ์ด๋ ต๊ณ ,
- ์ค์ ํ์ต ๋ฃจํด์ผ๋ก ์ด์ด์ง๊ธฐ๋ณด๋ค๋ ์ผํ์ฑ ์ฒดํ์ ๋จธ๋ฌด๋ ๊ฒฝ์ฐ๊ฐ ๋ง์
- ๋ฉ์ผ ๊ธฐ๋ฐ ํ์ต(์: ๋งค์ผ๋ฉ์ผ)
- ์ฌ์ฉ์๊ฐ ๋ฉ์ผ์ ์ด์ด๋ณด์ง ์์ผ๋ฉด ํ์ต์ด ๋๊น โ ์ฌ์ฉ์ ์ฃผ๋ํ(passive) ๊ตฌ์กฐ๋ผ ์ง์์ฑ์ด ๋ฎ์
- ์งง์ ํ ์์ฃผ์ ์ ๋ณด ์ ๊ณต์ด๋ผ, ์ค์ ๋ต๋ณ ํ๋ จ/ํผ๋๋ฐฑ ๋ฃจํ๊ฐ ๋ถ์กฑํจ
- ๋ชจ์ ๋ฉด์ (์: ์ฌ๋์ธ AI ๋ฉด์ )
- ๊ฒฐ๊ณผ์ ์ผ๋ก
- ์ทจ์ ์ค๋น์์ ๊พธ์คํ ์ฐ์ตํ ์ ์๋ ๊ฐ๋ฒผ์ด ๋ฃจํด๊ณผ
- ์ฆ๊ฐ์ ์ธ ํผ๋๋ฐฑ๊ณผ ์ฑ์ทจ๊ฐ์ ์ ๊ณตํ๋ ๋๊ตฌ๋ฅผ ์ฐพ์ง ๋ชปํด
- ํ์ต ์ง์๋ฅ ์ด ๋ฎ๊ณ , ์ค์ง์ ์ธ ์ญ๋ ํฅ์์ผ๋ก ์ด์ด์ง์ง ๋ชปํจ
- ๋ธ๋ผ์ฐ์ /์ฑ ์คํ ์ ์๋์ผ๋ก ํ๋ฃจ 1๋ฌธํญ ๋ ธ์ถ
- ๋ต๋ณ์ ํ ์คํธ/์์ฑ ์ค ์ ํ ๊ฐ๋ฅ โ ์์ฑ์ ๊ฒฝ์ฐ ์๊ฐ ์ ํ ์ต์ ์ ๊ณต
- AI ํผ๋๋ฐฑ์ผ๋ก ๋ต๋ณ์ ๊ตฌ์กฐยท๋ช ํ์ฑยท๊ทผ๊ฑฐ ๊ฐํ๋ฅผ ์ง์
- ์์นด์ด๋ธ/์คํธ๋ฆญ/๋ผ์ด๋ฒ ๊ธฐ๋ฅ์ผ๋ก ์ฑ์ทจ๊ฐยท๊ฒฝ์์ฌ ์ ๊ณต
- ๊ผฌ๋ฆฌ ์ง๋ฌธ ๊ธฐ๋ฅ์ผ๋ก ์ค์ ๋ฉด์ ๊ณผ ์ ์ฌ๋ ํ๋ณด
- ํ์/์จ๋ณด๋ฉ
- ์์ ๋ก๊ทธ์ธ(๊ตฌ๊ธ/์นด์นด์ค)
- ์ํ๋ ์ง๊ตฐ ์ ํ โ ์ง๊ตฐ ๋ด ์ธ๋ถ์ง๊ตฐ ๋ค์ค ์ ํ
- ํ๋ฃจ ์ง๋ฌธ ๋ฃจํด
- ๋ฉ์ธ ํ์ด์ง [์ค๋์ ์ง๋ฌธ] โ ๋ต๋ณ ์ ๋ ฅ(ํ์/์์ฑ)
- ๋ต๋ณ ์ ๋ ฅ(ํ์/์์ฑ), ์์ฑ์ ๊ฒฝ์ฐ ์๊ฐ ์ ํ
- ์์ฑ ๋ต๋ณ์ ์๋ STT ์ฒ๋ฆฌ
- AI ํผ๋๋ฐฑ + ์ฌ์ฉ์๊ฐ ๋์ด๋ ์ฒดํฌ
- ์คํธ๋ฆญ ์ฆ๊ฐ
- ์์นด์ด๋ธ
- ๋ด๊ฐ ํผ ๋ต๋ณ/ํผ๋๋ฐฑ ํ์คํ ๋ฆฌ
- ์ฆ๊ฒจ์ฐพ๊ธฐ(ํ)/๋์ด๋ ํํฐ
- ๊ฐ๋จํ ๋ฉ๋ชจ ์์ฑ ๊ธฐ๋ฅ
- ๋๊ธฐ ๋ถ์ฌ
- ์คํธ๋ฆญ(์ฐ์ ๊ธฐ๋ก)
- ๋ผ์ด๋ฒ ์ง์
- ์ ๋ฃ ์๋น์ค
- (๊ตฌ๋
) ์ผ์ผ ์ง๋ฌธ ํ๋ ์ถ๊ฐ
- ๋ฉด์ ์ด ์๋ฐํ ์ฌ์ฉ์์ ํธ์์ฑ ์ฆ๋
- (๊ตฌ๋
) ์ผ์ผ ์ง๋ฌธ ํ๋ ์ถ๊ฐ
์ ๊ท ์ฌ์ฉ์๋ ์์ ๋ก๊ทธ์ธ์ ํตํด ๊ฐํธํ๊ฒ ๊ฐ์ ํ๊ณ , ์จ๋ณด๋ฉ ๊ณผ์ ์ ํตํด ๋ง์ถคํ ์๋น์ค๋ฅผ ์ค์ ํฉ๋๋ค.
์ฃผ์ ๋จ๊ณ:
- ์์ ๋ก๊ทธ์ธ ์์: ์ฌ์ฉ์๊ฐ ๊ตฌ๊ธ ๋๋ ์นด์นด์ค ๋ก๊ทธ์ธ ๋ฒํผ์ ํด๋ฆญํฉ๋๋ค.
- OAuth2 ์ธ์ฆ: Spring Security๊ฐ OAuth2 ์ธ์ฆ ํ๋ก์ฐ๋ฅผ ์์ํ๊ณ , ์ฌ์ฉ์๋ฅผ ๊ตฌ๊ธ/์นด์นด์ค ์ธ์ฆ ํ์ด์ง๋ก ๋ฆฌ๋ค์ด๋ ํธํฉ๋๋ค.
- ์ฌ์ฉ์ ์ธ์ฆ: ์ฌ์ฉ์๊ฐ ๊ตฌ๊ธ/์นด์นด์ค์์ ๋ก๊ทธ์ธํ๊ณ ์ ๋ณด ์ ๊ณต์ ๋์ํฉ๋๋ค.
- ์ฌ์ฉ์ ์ ๋ณด ์กฐํ:
CustomOAuth2UserService๊ฐ Authorization Code๋ฅผ Access Token์ผ๋ก ๊ตํํ๊ณ , ์ฌ์ฉ์ ์ ๋ณด API๋ฅผ ํธ์ถํฉ๋๋ค. - ์ฌ์ฉ์ ์ ์ฅ/์ ๋ฐ์ดํธ: ์ด๋ฉ์ผ๋ก ๊ธฐ์กด ์ฌ์ฉ์๋ฅผ ์กฐํํ๊ณ , ์์ผ๋ฉด ์ ๊ท ์์ฑ, ์์ผ๋ฉด ์ด๋ฆ๋ง ์ ๋ฐ์ดํธํฉ๋๋ค.
- ์ ๊ท ์ฌ์ฉ์ ์ด๊ธฐํ: ์ ๊ท ์ฌ์ฉ์์ธ ๊ฒฝ์ฐ
UserPreferences์UserFlowProgress๋ฅผ ๊ธฐ๋ณธ๊ฐ์ผ๋ก ์์ฑํฉ๋๋ค. - JWT ํ ํฐ ๋ฐ๊ธ:
OAuth2AuthenticationSuccessHandler์์ Access Token๊ณผ Refresh Token์ ์์ฑํฉ๋๋ค. - ํ ํฐ ์ ์ฅ: Refresh Token์ HttpOnly ์ฟ ํค์ ์ ์ฅ๋๊ณ , Access Token์ URL ํ๋ผ๋ฏธํฐ๋ก ํ๋ก ํธ์๋์ ์ ๋ฌ๋ฉ๋๋ค.
- ์จ๋ณด๋ฉ ์งํ: ์ ๊ท ์ฌ์ฉ์๋ ์ง๊ตฐ ์ ํ์ ์งํํฉ๋๋ค.
- ์๋น์ค ์ด์ฉ ์์: ์จ๋ณด๋ฉ ์๋ฃ ํ ๋ฉ์ธ ํ์ด์ง์์ ์ค๋์ ์ง๋ฌธ์ ๋ฐ์ ๋ต๋ณ์ ์์ํ ์ ์์ต๋๋ค.
ํ ํฐ ๊ฐฑ์ ํ๋ก์ฐ:
- Access Token ๋ง๋ฃ ์:
POST /api/token/refresh๋ก Refresh Token์ ์ฌ์ฉํด ์๋ก์ด Access Token ๋ฐ๊ธ - Refresh Token์ ์ฟ ํค์์ ์๋์ผ๋ก ์ฝ์ด์ด
์ฌ์ฉ์๊ฐ ๋ฉ์ธ ํ์ด์ง์์ ์ค๋์ ์ง๋ฌธ์ ๋ฐ์์ค๋ ๊ณผ์ ์ ๋๋ค.
์ฃผ์ ๋จ๊ณ:
- ์ง๋ฌธ ์์ฒญ: ํด๋ผ์ด์ธํธ๊ฐ
GET /api/questions/random์๋ํฌ์ธํธ๋ก ๋๋ค ์ง๋ฌธ์ ์์ฒญํฉ๋๋ค. - ์ฌ์ฉ์ ์ค์ ์กฐํ: ์๋ฒ๊ฐ
UserPreferences๋ฅผ ์กฐํํ์ฌ ์ฌ์ฉ์์ ์ง๋ฌธ ๋ชจ๋(TECH/FLOW), ์ง๊ตฐ ๋ฑ์ ํ์ธํฉ๋๋ค. - ์ผ์ผ ํ๋ ๊ฒ์ฆ: ์ค๋ ์ด๋ฏธ ๋ต๋ณํ ์ง๋ฌธ ์๋ฅผ ํ์ธํ์ฌ ์ผ์ผ ์ง๋ฌธ ํ๋๋ฅผ ์ด๊ณผํ์ง ์์๋์ง ๊ฒ์ฆํฉ๋๋ค.
- ์ง๋ฌธ ๋ชจ๋๋ณ ์ฒ๋ฆฌ:
- TECH ๋ชจ๋: ์ฌ์ฉ์์ ์ง๊ตฐ์ ๋ง๋ ๊ธฐ์ ์ง๋ฌธ์ ๋๋ค์ผ๋ก ์ ํํฉ๋๋ค. ์ด๋ฏธ ๋ต๋ณํ ์ง๋ฌธ์ ์ ์ธ๋ฉ๋๋ค.
- FLOW ๋ชจ๋: ์ฌ์ฉ์์ ํ์ฌ ๋ฉด์ ๋จ๊ณ(INTRO โ MOTIVATION โ TECH โ PERSONALITY)์ ๋ง๋ ์ง๋ฌธ์ ์ ํํฉ๋๋ค.
UserFlowProgress๋ฅผ ํตํด ํ์ฌ ๋จ๊ณ๋ฅผ ์ถ์ ํฉ๋๋ค.
- ๊ผฌ๋ฆฌ ์ง๋ฌธ ์ฐ์ ์ฒ๋ฆฌ: ๋ฏธ๋ต๋ณ ๊ผฌ๋ฆฌ ์ง๋ฌธ์ด ์์ผ๋ฉด ์ผ๋ฐ ์ง๋ฌธ๋ณด๋ค ์ฐ์ ์ ์ผ๋ก ์ ๊ณตํฉ๋๋ค.
- ์ง๋ฌธ ๋ฐํ: ์ ํ๋ ์ง๋ฌธ๊ณผ ํจ๊ป ์ง๋ฌธ ๋ชจ๋, ํ์ฌ ๋จ๊ณ, ์๊ฐ ์ ํ ๋ฑ์ ์ ๋ณด๋ฅผ ํฌํจํ
RandomQuestionResponse๋ฅผ ๋ฐํํฉ๋๋ค.
์์ฑ ๋ต๋ณ์ ๊ฒฝ์ฐ ๋น๋๊ธฐ STT ์ฒ๋ฆฌ์ ์ค์๊ฐ ์๋ฆผ์ ํตํด ์ฌ์ฉ์ ๊ฒฝํ์ ์ต์ ํํฉ๋๋ค.
์ฃผ์ ๋จ๊ณ:
- ์์ฑ ๋ น์: ์ฌ์ฉ์๊ฐ ๋ธ๋ผ์ฐ์ ์์ ์์ฑ์ ๋ น์ํฉ๋๋ค.
- Presigned URL ์์ฒญ: ํด๋ผ์ด์ธํธ๊ฐ ์๋ฒ์ ์ ๋ก๋์ฉ Presigned URL์ ์์ฒญํฉ๋๋ค.
- ์ง์ ์ ๋ก๋: ํด๋ผ์ด์ธํธ๊ฐ Presigned URL์ ์ฌ์ฉํด NCP Object Storage์ ์์ฑ ํ์ผ์ ์ง์ ์ ๋ก๋ํฉ๋๋ค.
- ๋ต๋ณ ๋ฑ๋ก: ์์ฑ ํ์ผ URL๊ณผ ํจ๊ป
POST /api/answers์๋ํฌ์ธํธ๋ก ๋ต๋ณ์ ๋ฑ๋กํฉ๋๋ค. - SSE ์ฐ๊ฒฐ: ๋ต๋ณ ์ ์ถ๊ณผ ๋์์, ํด๋ผ์ด์ธํธ๊ฐ SSE ์ ์ฉ ์ผํ์ฉ ํ ํฐ์ ๋ฐ๊ธ๋ฐ๊ณ ์ด๋ฅผ ์ฌ์ฉํด
GET /api/sse/connect๋ก SSE ์ฐ๊ฒฐ์ ์๋ฆฝํ์ฌ ์ค์๊ฐ ์ด๋ฒคํธ๋ฅผ ๋ฐ์ ์ค๋น๋ฅผ ํฉ๋๋ค. - Answer ์์ฑ: ์๋ฒ์์ Answer ์ํฐํฐ๋ฅผ ์์ฑํ์ง๋ง, ์์ง ํ
์คํธ๋ ์๊ณ ์ํ๋
PENDING_STT์ ๋๋ค. - STT ์์
์์:
SttTask๋ฅผ ์์ฑํ๊ณ NCP CLOVA SPEECH API๋ฅผ ํธ์ถํ์ฌ ๋น๋๊ธฐ ๋ณํ ์์ ์ ์์ํฉ๋๋ค. - STT ์ฝ๋ฐฑ ์ฒ๋ฆฌ: NCP CLOVA๊ฐ ๋ณํ์ ์๋ฃํ๋ฉด
POST /api/stt/callback์ผ๋ก ์ฝ๋ฐฑ์ ๋ณด๋ ๋๋ค. - ํ ์คํธ ์ ๋ฐ์ดํธ: ์ฝ๋ฐฑ์์ ๋ฐ์ ํ ์คํธ๋ก Answer๋ฅผ ์ ๋ฐ์ดํธํ๊ณ SSE๋ฅผ ํตํด ํด๋ผ์ด์ธํธ์ STT ์๋ฃ ์ด๋ฒคํธ๋ฅผ ์ ์กํฉ๋๋ค.
- AI ํผ๋๋ฐฑ ์์ฑ:
FeedbackService๊ฐ OpenAI API๋ฅผ ํธ์ถํ์ฌ ํผ๋๋ฐฑ์ ์์ฑํฉ๋๋ค. - ๊ฒฐ๊ณผ ์กฐํ: ํด๋ผ์ด์ธํธ๊ฐ
GET /api/answers/{id}๋ก ์ต์ข ๋ต๋ณ๊ณผ ํผ๋๋ฐฑ์ ์กฐํํฉ๋๋ค. ์๋ต์๋answerText,feedback(AI ํผ๋๋ฐฑ),question์ ๋ณด๊ฐ ํฌํจ๋ฉ๋๋ค.
์๋ฌ ์ฒ๋ฆฌ:
- STT ์คํจ ์:
SttFailedEvent๋ฐํ โ SSE๋ก ์๋ฆผ โ ํด๋ผ์ด์ธํธ๊ฐ/api/answers/{id}/retry-stt๋ก ์ฌ์๋ ๊ฐ๋ฅ - ํผ๋๋ฐฑ ์์ฑ ์คํจ ์: Feedback ์ํ๋ฅผ
FAILED๋ก ์ ๋ฐ์ดํธ โ ์ฌ์๋ ๊ฐ๋ฅ
ํ ์คํธ ๋ต๋ณ์ ๋ ๋จ์ํ ๋๊ธฐ ํ๋ก์ฐ๋ฅผ ๋ฐ๋ฆ ๋๋ค.
์ฃผ์ ๋จ๊ณ:
- ํ ์คํธ ์ ๋ ฅ: ์ฌ์ฉ์๊ฐ ๋ธ๋ผ์ฐ์ ์์ ํ ์คํธ๋ก ๋ต๋ณ์ ์ ๋ ฅํฉ๋๋ค.
- ๋ต๋ณ ๋ฑ๋ก: ํด๋ผ์ด์ธํธ๊ฐ
POST /api/answers์๋ํฌ์ธํธ๋ก ๋ต๋ณ์ ๋ฑ๋กํฉ๋๋ค. - Answer ์์ฑ: ์๋ฒ์์ Answer ์ํฐํฐ๋ฅผ ์์ฑํฉ๋๋ค. ์ด ์์ ์ ์ด๋ฏธ ํ
์คํธ๊ฐ ํฌํจ๋์ด ์์ผ๋ฉฐ,
feedback์ํ๋PENDING์ ๋๋ค. - AI ํผ๋๋ฐฑ ์์ฑ:
FeedbackService๊ฐ OpenAI GAPI๋ฅผ ํธ์ถํ์ฌ ํผ๋๋ฐฑ์ ์์ฑํฉ๋๋ค. - ๊ฒฐ๊ณผ ์กฐํ: ํด๋ผ์ด์ธํธ๊ฐ
GET /api/answers/{id}๋ก ์ต์ข ๋ต๋ณ๊ณผ ํผ๋๋ฐฑ์ ์กฐํํฉ๋๋ค. ์๋ต์๋answerText,feedback(AI ํผ๋๋ฐฑ),question์ ๋ณด๊ฐ ํฌํจ๋ฉ๋๋ค.
์ฌ์ฉ์์ ๋ต๋ณ์ ๋ฐํ์ผ๋ก AI๊ฐ ์ถ๊ฐ ์ง๋ฌธ์ ์์ฑํ์ฌ ์ค์ ๋ฉด์ ๊ณผ ์ ์ฌํ ๊ฒฝํ์ ์ ๊ณตํฉ๋๋ค.
์ฃผ์ ๋จ๊ณ:
- ๊ผฌ๋ฆฌ ์ง๋ฌธ ์์ฑ ์์ฒญ: ์ฌ์ฉ์๊ฐ ๋ต๋ณ์ ๋ํ ํผ๋๋ฐฑ์ ํ์ธํ ํ,
POST /api/questions/followUp/{answerId}์๋ํฌ์ธํธ๋ก ๊ผฌ๋ฆฌ ์ง๋ฌธ ์์ฑ์ ์์ฒญํฉ๋๋ค. - ๋ต๋ณ ๋ฐ ์ง๋ฌธ ์กฐํ: ์๋ฒ๊ฐ ํด๋น Answer์ ์๋ณธ Question ์ ๋ณด๋ฅผ ์กฐํํฉ๋๋ค.
- AI ์ง๋ฌธ ์์ฑ:
FollowUpQuestionService๊ฐ OpenAI API๋ฅผ ํธ์ถํ์ฌ ์ฌ์ฉ์์ ๋ต๋ณ์ ๋ถ์ํ๊ณ , ๋ต๋ณ์ ๋ ๊น์ด ์๊ฒ ํ๊ตฌํ ์ ์๋ ๊ผฌ๋ฆฌ ์ง๋ฌธ์ ์์ฑํฉ๋๋ค. - ๊ผฌ๋ฆฌ ์ง๋ฌธ ์ ์ฅ: ์์ฑ๋ ๊ผฌ๋ฆฌ ์ง๋ฌธ๋ค์
FollowUpQuestion์ํฐํฐ๋ก ์ ์ฅํ๊ณ , ์๋ณธ Answer์ ์ฐ๊ฒฐํฉ๋๋ค. - ์๋ต ๋ฐํ: ์์ฑ๋ ๊ผฌ๋ฆฌ ์ง๋ฌธ์ ๊ฐ์๋ฅผ ํฌํจํ
FollowUpGenerationResponse๋ฅผ ๋ฐํํฉ๋๋ค. - ์ง๋ฌธ ์กฐํ ์ ์ฐ์ ์ ๊ณต: ์ดํ ํด๋ผ์ด์ธํธํธ๊ฐ
GET /api/questions/random์ ํธ์ถํ๋ฉด, ๋ฏธ๋ต๋ณ ๊ผฌ๋ฆฌ ์ง๋ฌธ์ด ์ผ๋ฐ ์ง๋ฌธ๋ณด๋ค ์ฐ์ ์ ์ผ๋ก ์ ๊ณต๋ฉ๋๋ค.
๊ผฌ๋ฆฌ ์ง๋ฌธ์ ํน์ง:
- ์ฌ์ฉ์์ ๋ต๋ณ ๋ด์ฉ์ ๋ฐํ์ผ๋ก ๋งฅ๋ฝ์ ๋ง๋ ์ถ๊ฐ ์ง๋ฌธ ์์ฑ
- ์ค์ ๋ฉด์ ์์ ๋ฉด์ ๊ด์ด ํ ์ ์๋ ์ฌํ ์ง๋ฌธ ์๋ฎฌ๋ ์ด์
- ๋ต๋ณ์ ๊น์ด์ ์์ฑ๋๋ฅผ ๋์ด๋ ๋ฐ ๋์
SSE + virtual thread
- CLOVA STT ๋ณํ์ ๋น๋๊ธฐ ์์ ์ผ๋ก ์ด ๊ฒฐ๊ณผ๋ฅผ ํด๋ผ์ด์ธํธ์๊ฒ '์ค์๊ฐ'์ผ๋ก ์๋ ค์ฃผ๊ธฐ ์ํด SSE๋ฅผ ๋์
- ์ด๋, SSE๋ ํน์ฑ์ ๊ฐ ์ฌ์ฉ์๊ฐ ์ ์ํด ์๋ ๋ด๋ด ์๋ฒ ์ค๋ ๋ 1๊ฐ๋ฅผ ์ ์ ํจ
- ํ ์คํธ์์ ๋์ ์ ์์๊ฐ 200๋ช ๋ง ๋์ด๊ฐ๋ ๋ชจ๋ OS ์ฐ๋ ๋๊ฐ ๊ณ ๊ฐ๋๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ฌ ์๋ฒ ์ ์ฒด๊ฐ ๋ค์ด๋๋ ํ์ ๋ฐ์
- Virtual Thread๋ฅผ ๋์ ํ์ฌ SSE์ฐ๊ฒฐ์ ์ ์งํ๋๋ผ๋ OS๋ฅผ ์ ์ ํ์ง ์๊ณ 10~20๊ฐ์ ์ ์ OS์ฐ๋ ๋๋ง์ผ๋ก n์ฒ๊ฐ์ SSE ๋์ ์ฐ๊ฒฐ์ ์ฒ๋ฆฌํ๋๋ก ํจ
- 30์ด๊ฐ ์ฐ๋ ๋๋ฅผ ๊ฐ์ ๋ก ๋๊ธฐ์ํค๋ ์ฆ, SSE ์ฐ๊ฒฐ ํ ๋๊ธฐ ์๋ฎฌ๋ ์ด์ ์ ์ํ ํ ์คํธ์ฉ API๋ฅผ ๊ตฌํ
- ๊ธฐ์กด OS ์ฐ๋ ๋์ ํ๊ณ์น๋ฅผ ๋๊ธฐ๊ฒ 3000๋ช ์ด ๋์์์ฒญํ๋๋ก ์ค์ ํ์ฌ ํ ์คํธ
- ์ฆ ์๋ฒ๊ฐ 3000๊ฐ์ ์ฐ๋ ๋๊ฐ ๋์์ ๋ธ๋กํน๋ ์ํ๋ฅผ ๋ฒํฐ๋๋ก ๊ตฌ์ฑ
- ์ฌ์ง(์) virtual thread๋ฅผ ์ผ ๊ฒฝ์ฐ OS ์ฐ๋ ๋ ๊ณ ๊ฐ ์์ด ๋ชจ๋ ์ฑ๊ณต
- ์ฌ์ง (์๋) ๊ฐ์์ ์ ๊ฐ 204๋ช ์ ๋๋ฌํ์๋ง์ OS ์ฐ๋ ๋ ๊ณ ๊ฐ๋ก ๋๋จธ์ง ์์ฒญ ๋ชจ๋ ์คํจ
nGrinder ๋์
- FE๊ฐ API๊ฐ ๋๋ฆฌ๋ค๊ณ ๋ฆฌํฌํธํ์ฌ, BEํ์ ์ ํํ ์์ธ(API ๋ฌธ์ /DB ๋ฌธ์ /์ธํ๋ผ ๋ฌธ์ )์ ์ฌํํ๊ธธ ์ํจ
- Groovy ์คํฌ๋ฆฝํธ ๊ธฐ๋ฐ ์๋๋ฆฌ์ค: VUser(๊ฐ์ ์ ์ )๋ณ๋ก ๋ณต์กํ ๋ก์ง์ ์ฝ๋๋ก ์์ฑ
- ์ถ์ ๋ฐ ๊ด๋ฆฌ : API ์๋ํฌ์ธํธ๋ณ๋ก ๋ถ๋ฆฌ
- ๋ช ํํ ๋ณ๋ชฉ ์๊ฐํ: VUser ์ฆ๊ฐ์ ๋ฐ๋ฅธ Error Rate, TPS, Mean Test Time(MTT)์ ์ค์๊ฐ ๊ทธ๋ํ๋ก ํ์ธ
- ๋ช ํํ SLA์ ๊ณต(BE) - ๋ฐ์ดํฐ์ ๊ธฐ๋ฐํ ๋ช ํํ SLA(Service Level Agreement) ์ ๊ณต
- ์ํฐํจํด ๋ฐฉ์ง ๋ฐ ์ ๋ขฐ ๊ตฌ์ถ(FE) - FEํ์ด "API๊ฐ ๋๋ฆด ๊ฒ"์ด๋ผ ์ง๋ ์ง์ํ์ฌ ๋ถํ์ํ ํด๋ผ์ด์ธํธ ์ฌ์ด๋ ์บ์ฑ์ด๋ ๋ณต์กํ ์ํ ๊ด๋ฆฌ๋ฅผ ๊ตฌํํ๋ ์ํฐํจํด์ ๋ฐฉ์ง
JWT ๊ธฐ๋ฐ ๋ก๊ทธ์ธ ์ธ์ฆ/์ธ๊ฐ
- JWT(JSON Web Token) ๋์ ์ ์ฃผ๋ ์ด์ ๋ ๊ธฐ์กด ์ธ์ (Session) ๊ธฐ๋ฐ ์ธ์ฆ ๋ฐฉ์์ ํ๊ณ์ ์ ๊ทน๋ณตํ๊ณ , ํ์ฅ์ฑ ๋ฐ ํจ์จ์ฑ์ ๊ฐ์ ํ๊ธฐ ์ํจ
- ๋ก๊ทธ์ธ ์ฑ๊ณต ์ ์๋ฒ๊ฐ ๋น๋ฐํค๋ฅผ ์ฌ์ฉํด JWT๋ฅผ ๋ฐ๊ธํ๊ณ , ํด๋ผ์ด์ธํธ๋ ํด๋น ํ ํฐ์ ์ ์ฅ ํ ์์ฒญ ์ ํค๋
(Authorization: Bearer <token>)์ ํฌํจ - ์๋ฒ๋ ํ ํฐ ๊ฒ์ฆ๋ง ์ํํ๋ฉฐ, ๋ณ๋์ ์ธ์ ์ํ๋ฅผ ์ ์งํ์ง ์์ (Stateless ์ธ์ฆ ๊ตฌ์กฐ)
- ํ ํฐ์ ์ฌ์ฉ์ ๊ถํ(Role) ๋ฐ ๋ง๋ฃ ์๊ฐ(Expiration Time)์ ํฌํจํ์ฌ ์ธ๊ฐ(Authorization) ๋ฅผ ๊ฐํธํ๊ฒ ์ฒ๋ฆฌ
- Refresh Token์ ํ์ฉํด Access Token ์ฌ๋ฐ๊ธ ํ๋ก์ธ์ค ๊ตฌํ์ผ๋ก ๋ณด์์ฑ๊ณผ ํธ์์ฑ ๊ฐํ
- ์๋ฒ์ ์ธ์ ์ ์ฅ์ด ๋ถํ์ํ๋ฏ๋ก ํ์ฅ์ฑ๊ณผ ์ฑ๋ฅ ํฅ์
- REST API, ๋ชจ๋ฐ์ผ, ํ๋ก ํธ์๋ ๋ฑ ๋ค์ํ ํด๋ผ์ด์ธํธ ํ๊ฒฝ์์ ์ผ๊ด๋ ์ธ์ฆ ์ฒด๊ณ ์ ์ง
- ํ ํฐ ๊ธฐ๋ฐ ๊ฒ์ฆ์ผ๋ก ๋ณด์์ฑ ๊ฐํ(๋ง๋ฃ ์๊ฐ, ์๋ช ๊ฒ์ฆ, HTTPS ์ฐ๋ ๋ฑ)
๊ด๋ฆฌ์ ํ์ด์ง ๊ตฌํ
- ์์คํ ์ด์ ์ค ์ง์ DB ์ ๊ทผ์ด๋ ๊ฐ๋ฐ์ ์์กด์ ์ ์ฐจ๋ก ์ํํด์ผ ํ๋ ๋นํจ์จ ์กด์ฌ
- ๋ณด์ยท์ ์ฑ ยท์ฝํ ์ธ ๊ด๋ฆฌ ๋ฑ ์ด์์๊ฐ ์ง์ ์ ์ดํ ์ ์๋ ์ธํฐํ์ด์ค ๋ถ์ฌ
- JWT ๊ธฐ๋ฐ ์ธ์ฆ/์ธ๊ฐ ์ฐ๋์ผ๋ก ์ ๊ทผ ์ ์ด ๊ฐํ
- ํ์ ์กฐํ ๋ฐ ์์ /์ญ์
- ์ด๋ฆ ๋ฐ ์ญํ (Role)๋ณ ์ ๊ทผ ๊ถํ ๊ด๋ฆฌ ๊ธฐ๋ฅ ์ ๊ณต (๊ด๋ฆฌ์/๊ตฌ๋ ์/์ผ๋ฐ์ฌ์ฉ์)
- ์ง๊ตฐ/์ง์ ์กฐํ ๋ฐ ์ถ๊ฐ/์ญ์
- ์ง๋ฌธ ์กฐํ ๋ฐ ์ถ๊ฐ/์์ /์ญ์
- ์ง๋ฌธ ๋ด์ฉ, ์ง๋ฌธ ํ์ (TECH/INTRO/MOTIVATION/PERSONALITY), ์ฐ๊ฒฐ๋ ์ง์ ์ข ๋ฅ, ํ์ฑํ ์ฌ๋ถ(ํ์ฑ/๋นํ์ฑ) ๊ด๋ฆฌ ์ ๊ณต
- ์ด์์๊ฐ ์ง์ ๊ด๋ฆฌ ๊ฐ๋ฅํ ์ธํฐํ์ด์ค ์ ๊ณต์ผ๋ก ์ด์ ํจ์จ์ฑ ํฅ์
- ๊ด๋ฆฌ์ ๊ถํ ๋ถ๋ฆฌ ๋ฐ ์ธ์ฆ ๊ฐํ๋ก ๋ณด์ ๊ฐํ
- ๋ฐ์ดํฐ ๋ฐ ์ฌ์ฉ์ ๊ด๋ฆฌ ์๋ํ๋ก ๊ฐ๋ฐ์ ์์กด๋ ๊ฐ์
- ์ฅ์ ๋์ ๋ฐ ์ ์ฑ ๋ณ๊ฒฝ ์๋ ํฅ์์ผ๋ก ์ด์ ๋ฏผ์ฒฉ์ฑ ํ๋ณด
์ ์ญ ์๋ฌ ํธ๋ค๋ฌ
- ๋ณด์ผ๋ฌ ํ๋ ์ดํธ ์ฝ๋ ๋ฐ์ : API ์ปจํธ๋กค๋ฌ๋ง๋ค
try-catch๋ฌธ์ด ๋ฐ๋ณต - ์ผ๊ด์ฑ ์๋ ์๋ฌ ์๋ต
- ๋๋ฒ๊น ์ ๋์์ด ๋์ง ์๋ ๋ก๊ทธ : log.error("์ ์ ๋ฅผ ์ฐพ์ง ๋ชปํจ")์ ๋์์ด ๋์ง ์์
- ExceptionHandlerAdvice๋ฅผ ํตํด ์๋ฌ ํธ๋ค๋ง
- BusinissException๊ณผ InfraException ํจํด์ ํตํ ์๋ฌ ์ฒ๋ฆฌ
- ์ฝ๋ ์ค๋ณต ์ ๊ฑฐ: ์ปจํธ๋กค๋ฌ์์ ์์ธ ์ฒ๋ฆฌ ์ฝ๋ ์ ๊ฑฐ
- ์ผ๊ด๋ ์๋ต ํ์: ํด๋ผ์ด์ธํธ๊ฐ ๋์ผํ ํ์์ผ๋ก ์ฒ๋ฆฌ ๊ฐ๋ฅ
- ์ ์ง๋ณด์์ฑ ํฅ์: ์์ธ ์ฒ๋ฆฌ ๋ก์ง ๋ณ๊ฒฝ ์ ํ ๊ณณ๋ง ์์
- ๋๋ฒ๊น ์ฉ์ด: ๋ชจ๋ ์์ธ๊ฐ ๊ตฌ์กฐํ๋ ๋ก๊ทธ๋ก ๊ธฐ๋ก
- ๋ณด์: ์์์น ๋ชปํ ์์ธ์ ์์ธ ์ ๋ณด ๋ ธ์ถ ๋ฐฉ์ง
ํ ํ๋ฆฟ ๋ฉ์๋ ํจํด
AnswerCommandService์ submitAnswer ๋ฉ์๋์์ ์ผ๋ฐ ์ง๋ฌธ๊ณผ ๊ผฌ๋ฆฌ ์ง๋ฌธ ์ฒ๋ฆฌ๊ฐ ํ๋์ ๋ฉ์๋์ ํผ์ฌ
- ์ฝ๋ ์ค๋ณต: ๊ณตํต ๋ก์ง(๋ต๋ณ ๊ฐ์ฒด ์์ฑ, ์ ์ฅ, STT ์ฒ๋ฆฌ)์ด ๋ฐ๋ณต๋จ
- ๋ณต์กํ ๋ถ๊ธฐ: if-else๋ก ์ผ๋ฐ/๊ผฌ๋ฆฌ ์ง๋ฌธ์ ๊ตฌ๋ถํ๋ฉฐ ๊ฐ๋ ์ฑ ์ ํ
- ํ์ฅ์ฑ ๋ถ์กฑ: ์๋ก์ด ๋ต๋ณ ํ์ ์ถ๊ฐ ์ ๋ฉ์๋ ์์ ํ์
- ๋จ์ผ ์ฑ ์(SRP) ์๋ฐ: ํ๋์ ๋ฉ์๋๊ฐ ์ฌ๋ฌ ํ์ ์ ๋ต๋ณ ์ฒ๋ฆฌ ๋ก์ง์ ํฌํจ
- ํ ํ๋ฆฟ ๋ฉ์๋ ํจํด์ ์ ์ฉํด ๊ณตํต ํ๋ฆ์ ์ถ์ ํด๋์ค์์ ์ ์ํ๊ณ , ์ฐจ์ด์ ์ ํ์ ํด๋์ค์์ ๊ตฌํํ๋๋ก ๋ถ๋ฆฌ
- ์ฝ๋ ์ฌ์ฌ์ฉ์ฑ ํฅ์: ๊ณตํต ๋ก์ง์ ํ ๊ณณ์์ ๊ด๋ฆฌ
- ๊ฐ๋ ์ฑ ๊ฐ์ : ๊ฐ ํธ๋ค๋ฌ๊ฐ ์์ ์ ์ฑ ์๋ง ๋ด๋น
- ํ์ฅ์ฑ: ์๋ก์ด ๋ต๋ณ ํ์ ์ถ๊ฐ ์ ํธ๋ค๋ฌ๋ง ์ถ๊ฐ
- ์ ์ง๋ณด์์ฑ: ๋ณ๊ฒฝ ์ํฅ ๋ฒ์๊ฐ ๋ช ํ
- ํ ์คํธ ์ฉ์ด์ฑ: ๊ฐ ํธ๋ค๋ฌ๋ฅผ ๋ ๋ฆฝ์ ์ผ๋ก ํ ์คํธ ๊ฐ๋ฅ
- ํ์ฌ
AnswerCommandService๋ ํฉํ ๋ฆฌ์์ ํธ๋ค๋ฌ๋ฅผ ๋ฐ์handle()๋ง ํธ์ถ
cursor ๊ธฐ๋ฐ ๋๋ค ์ง๋ฌธ
- ๊ธฐ์กด ๋ฐฉ์(์ ์ฒด ์ง๋ฌธ ์กฐํ ํ ๋๋ค ์ ํ)์ ์ง๋ฌธ ์๊ฐ ์ฆ๊ฐํ ์๋ก O(n) ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๊ณผ ๋๋ฆฐ ์ฟผ๋ฆฌ ์ฑ๋ฅ ๋ฌธ์ ๊ฐ ๋ฐ์
- ํนํ ์ฌ์ฉ์๊ฐ ์ด๋ฏธ ๋ต๋ณํ ์ง๋ฌธ์ ์ ์ธํ๋ ์กฐ๊ฑด์ ํฌํจํ ๊ฒฝ์ฐ ์ฟผ๋ฆฌ ๋น์ฉ์ด ๋น์ฝ์ ์ผ๋ก ์ฆ๊ฐ
- MAX ID๋ฅผ ๋จผ์ ์กฐํํ์ฌ ์ฌ์ฉ ๊ฐ๋ฅํ ์ง๋ฌธ์ ID ๋ฒ์๋ฅผ ํ์
- 1๋ถํฐ MAX ID ์ฌ์ด์ ๋๋ค ID ์์ฑ
- Cursor ๊ธฐ๋ฐ ์กฐํ(id >= randomId)๋ก ํด๋น ๋ฒ์์์ ์ฒซ ๋ฒ์งธ ์ง๋ฌธ ์ ํ
- ์ธ๋ฑ์ค๋ฅผ ํ์ฉํ ํจ์จ์ ์ธ ์ฟผ๋ฆฌ (ORDER BY id, LIMIT 1)
- O(1) ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ: ์ ์ฒด ์ง๋ฌธ์ ๋ฉ๋ชจ๋ฆฌ์ ๋ก๋ํ์ง ์์
- ๋น ๋ฅธ ์ฟผ๋ฆฌ ์ฑ๋ฅ: ์ธ๋ฑ์ค ๊ธฐ๋ฐ ๋ฒ์ ์ค์บ์ผ๋ก O(log n) ์๊ฐ ๋ณต์ก๋
- ํ์ฅ์ฑ ํฅ์: ์ง๋ฌธ ์๊ฐ ์๋ง ๊ฐ๋ก ์ฆ๊ฐํด๋ ์ผ์ ํ ์ฑ๋ฅ ์ ์ง
- ๋๋ค์ฑ ๋ณด์ฅ: ๊ฐ ์ง๋ฌธ์ด ๋์ผํ ํ๋ฅ ๋ก ์ ํ๋จ
QueryDSL์ด ์๋ JPA Specification ๋์ ์ผ๋ก cursor ๊ธฐ๋ฐ ๋ฌดํ์คํฌ๋กค ๊ตฌํ + Slice
- ์ ํต์ Pagination(Offset)์ ํ๊ณ: '์์นด์ด๋ธ' ๊ธฐ๋ฅ์ ์ฌ์ฉ์์ ๋ชจ๋ ๋ต๋ณ์ ์กฐํํด์ผ ํจ
- ํ์ด์ง ๋ฒํธ๊ฐ ๋์ด๋ ์๋ก OFFSET N๊ฐ ๋งํผ์ ๋ฐ์ดํฐ ์ค์บ์ด ๋ฐ์ํ์ฌ ์กฐํ ์ฑ๋ฅ ์ ํ
- UX ๊ด์ ์์ ์ํฐํจํด์ด ๋ฐ์
- ๋์ ํํฐ ์๊ตฌ : FE์์ ๋ ์ง, ์ง๋ฌด, ์ง๋ฌธ ํ์ , ์ฆ๊ฒจ์ฐพ๊ธฐ, ๋์ด๋ ๋ฑ ๋ค์ํ ์กฐ๊ฑด์ผ๋ก ๋์ ๊ฒ์ ์๊ตฌ
- JPA Specification : JPA ํ์ค ๊ธฐ๋ฅ์ผ๋ก ์์กด์ฑ, ๋น๋, ํ์ต ๋น์ฉ ์ต์ํ
DTO๋ฅผ ๊ตฌ์ฑํ์ฌPredicate๋ฅผ ์กฐํฉํ๋createSpecification๋ฉ์๋๋ฅผ ํตํด ์ ์ง๋ณด์ ํ๋ณด
- ๋ฌดํ ์คํฌ๋กค : Cursor + Slice
- OFFSET ๋์ lastId์ lastCreatedAt์ ์กฐํฉํ์ฌ ๋ง์ง๋ง์ผ๋ก ๋ณธ ๋ฐ์ดํฐ ๋ค์ 10๊ฐ๋ฅผ ์กฐํํ๋ ๋ฐฉ์ ์ฑํ
- Page๋ ๋ถํ์ํ COUNT(*) ์ฟผ๋ฆฌ๋ฅผ ์ถ๊ฐ๋ก ์คํํ์ฌ ์ฑ๋ฅ ์ ํ -> Slice๋ฅผ ํตํด COUNT์ฟผ๋ฆฌ๋ฅผ ์ ๊ฑฐํ๊ณ hasNext๋ฅผ FE์๊ฒ ์ ๋ฌ
- BE ์ฑ๋ฅ: ๋ฐ์ดํฐ๊ฐ ์๋ง ๊ฑด์ด ์กด์ฌํ์ฌ๋ OFFSET ์ค์บ์ด ์์ผ๋ฏ๋ก, ํญ์ ์ผ์ ํ๊ณ ๋น ๋ฅธ ์กฐํ ์ฑ๋ฅ ๋ณด์ฅ
- BE ์ ์ง๋ณด์: QueryDSL ๋์ ๋๋น ํ์ต ๋น์ฉ ๋ฎ์ถ๊ธฐ
- FE/UX: ์ฌ์ฉ์์๊ฒ ๋์ ์๋ ๋ฌดํ ์คํฌ๋กค ๊ฒฝํ ์ ๊ณต
NCP Object Storage ๋ณด์: Pre-Signed URL ๋์
- CLOVA SPEECH API๋ ํ์ผ ๊ฒฝ๋ก(dataKey)๋ฅผ ์ธ์๋ก ๋ฐ๊ธฐ ๋๋ฌธ์, ์์ฑ ํ์ผ์ด Object Storage์ ๋จผ์ ์ ๋ก๋๋์ด์ผ ํจ
- ์๋ฒ ๋ถํ: FE โ BE โ Storage๋ก ํ์ผ์ ์ค๊ณํ๋ฉด ๋ฐฑ์๋ ๋ถํ๊ฐ ์ฌ๊ฐ
- ๋ณด์ ์ทจ์ฝ: FE โ Storage๋ก ์ง์ ์ ๋ก๋ํ๋ ค๋ฉด, FE์ Secret Key๊ฐ ๋ ธ์ถ๋์ด ์น๋ช ์
-
๋ฐฑ์๋๊ฐ Secret Key๋ฅผ ์์ ํ๊ฒ ๋ณด๊ดํ๋ฉด์, FE์๊ฒ "10๋ถ ๋์, ํน์ ๊ฒฝ๋ก์, PUT ์์ฒญ๋ง ํ์ฉํ๋" ์์ ์ ๋ก๋ URL์ ๋ฐ๊ธ.
-
FE โ BE: ์ ๋ก๋ URL ์์ฒญ (/api/answers/upload-url)
-
BE โ FE: Pre-signed URL ์์ฑ ๋ฐ ๋ฐํ
-
FE โ NCP Storage: FE๊ฐ ์ด ์์ URL์ ์ฌ์ฉํด ์คํ ๋ฆฌ์ง๋ก ํ์ผ์ ์ง์ PUT (์ ๋ก๋)
-
(์ดํ) FE โ BE: "์ ๋ก๋ ์๋ฃ" ์๋ต ์ดํ STT ์์ฒญ (์ด๋ BE๊ฐ Clova API ํธ์ถ)
-
์ถ๊ฐ ํต์ฌ ๋ณด์ ๊ฐํ ์กฐ์น
- ์ฌ์ฉ์๋ณ ๊ฒฝ๋ก ๊ฒฉ๋ฆฌ (User Scoping)
- ๋ฌธ์ : ๋ชจ๋ ์ฌ์ฉ์๊ฐ ๊ฐ์ ๊ณณ์ ์ ๋ก๋ํ๋ฉด ํ์ผ์ด ๋ฎ์ด์จ์ง๊ฑฐ๋ ๊ฒฝ๋ก ์กฐ์ ๊ณต๊ฒฉ์ด ๊ฐ๋ฅ
- ํด๊ฒฐ: ํ์ผ ๊ฒฝ๋ก(objectKey)๋ฅผ **uploads/{userId}/{UUID}.[ํ์ฅ์]**๋ก ๊ฐ์ ํ์ฌ ๋ ผ๋ฆฌ์ ์ผ๋ก ๊ฒฉ๋ฆฌ
- ํ์ผ ํ์ฅ์ ๊ฒ์ฆ (Allow-list)
- ๋ฌธ์ : .html, .exe ๊ฐ์ ์ ์ฑ ํ์ผ ์ ๋ก๋ ์๋ ์ํ
- ํด๊ฒฐ: Clova๊ฐ ์ง์ํ๋ ์ค๋์ค ํ์(.mp3, .m4a ๋ฑ)์ 'ํ์ฉ ๋ชฉ๋ก'๊ณผ ๋น๊ต ๊ฒ์ฆ ํ ๋ชฉ๋ก์ ์์ผ๋ฉด 400 Bad Request๋ฅผ ๋ฐํํ์ฌ ์ ๋ก๋๋ฅผ ์์ฒ ์ฐจ๋จ
- ์ฌ์ฉ์๋ณ ๊ฒฝ๋ก ๊ฒฉ๋ฆฌ (User Scoping)
CORS์ ์ฑ ๋ฐ Preflight ํด๊ฒฐ
- CORS ์ ์ฑ ์๋ฐ: ๋ธ๋ผ์ฐ์ ์ ์๋ฒ์ Origin์ด ๋ฌ๋ผ, ๋ธ๋ผ์ฐ์ ์ ๋์ผ ์ถ์ฒ ์ ์ฑ (SOP)์ ์ํด API ์์ฒญ์ด ๊ธฐ๋ณธ์ ์ผ๋ก ์ฐจ๋จ๋จ
- Preflight (OPTIONS) ์์ฒญ ๋ฐ์: ๋ณธ ์์ฒญ(POST, PUT ๋ฑ)์ Authorization ํค๋(JWT ํ ํฐ)๋ฅผ ํฌํจ์ํด
- Authorization ํค๋๋ Simple Request ์กฐ๊ฑด์ ํด๋นํ์ง ์์ผ๋ฏ๋ก, ๋ธ๋ผ์ฐ์ ๋ ๋ณธ ์์ฒญ ์ ์๋ฒ์ ํ์ฉ ์ฌ๋ถ๋ฅผ ๋ฌป๋ OPTIONS ๋ฉ์๋(Preflight) ์์ฒญ์ ๋จผ์ ์ ์กํจ
- ๋ธ๋ผ์ฐ์ ๊ฐ ๋ณด๋ธ OPTIONS ์์ฒญ์๋ ์ธ์ฆ ํ ํฐ(Authorization ํค๋)์ด ์์
- ๋ฐ๋ผ์ JwtAuthenticationFilter ์ด์ ์ Spring Security์ ์ธ์ฆ ์ฒด์ธ์ด ๋จผ์ ๋์ํ์ฌ, ์ด OPTIONS ์์ฒญ์ 401 Unauthorized ๋๋ 403 Forbidden์ผ๋ก ์ฐจ๋จํจ
- ๋ธ๋ผ์ฐ์ ๋ Preflight ์์ฒญ์ด ์คํจ(200 OK๊ฐ ์๋)ํ์ผ๋ฏ๋ก, ๋ณธ ์์ฒญ(POST ๋ฑ)์ ๋ณด๋ด์ง ์๊ณ CORS ์๋ฌ๋ฅผ ๋ฐ์์ํด
- OPTIONS ์์ฒญ์ ๋ํ Spring Security ์ธ์ฆ ํด์
- filterChain ๋ฉ์๋ ๋ด์์ authorizeHttpRequests ์ค์ ์ ํตํด ๋ชจ๋ OPTIONS ๋ฉ์๋ ์์ฒญ์ ์ธ์ฆ ์ ์ฐจ ์์ด ํต๊ณผ(permit)์ํด
- ๊ตฌ์ฒด์ ์ธ CORS ์ ์ฑ
์ ์ ๋ฐ ์ ์ฉ
- ํ์ฉ ์ถ์ฒ (Origins): application.yml์ ์ ์๋ ํ๋ก ํธ์๋ ๋๋ฉ์ธ ๋ชฉ๋ก์ ํ์ฉ
- ํ์ฉ ๋ฉ์๋ (Methods): OPTIONS๋ฅผ ํฌํจํ GET, POST, PUT, DELETE ๋ฑ ๋ชจ๋ ๋ฉ์๋ ํ์ฉ
- ํ์ฉ ํค๋ (Headers): Authorization ํค๋๋ฅผ ํฌํจํ ๋ชจ๋ ํค๋ ํ์ฉ (setAllowedHeaders(List.of("*")))
- ์๊ฒฉ ์ฆ๋ช (Credentials): ์ฟ ํค(์: OAuth refresh_token)๋ฅผ ์ฃผ๊ณ ๋ฐ์ ์ ์๋๋ก ํ์ฉ (setAllowCredentials(true))
- filterChain ๋ด์์ .cors(cors -> cors.configurationSource(corsConfigurationSource))๋ฅผ ํธ์ถํ์ฌ, Spring Security๊ฐ ์ด CORS ๊ท์น์ ์ฌ์ฉํ๋๋ก ์ค์
- Preflight ์์ฒญ ์ฒ๋ฆฌ: ๋ธ๋ผ์ฐ์ ๊ฐ OPTIONS ์์ฒญ์ ๋ณด๋ด๋ฉด, Spring Security์ permitAll ๊ท์น ๋๋ถ์ ์ธ์ฆ ์์ด ํต๊ณผ
- ๋ณธ ์์ฒญ ์ฒ๋ฆฌ: ๋ธ๋ผ์ฐ์ ๊ฐ Preflight ์ฑ๊ณต์ ํ์ธํ๊ณ Authorization ํค๋๊ฐ ํฌํจ๋ ์ค์ POST ์์ฒญ์ ์ ์ก
- ์ด ์์ฒญ์ OPTIONS๊ฐ ์๋๋ฏ๋ก permitAll ๊ท์น์ ๊ฑธ๋ฆฌ์ง ์๊ณ , anyRequest().authenticated() ๊ท์น์ ๋ฐ๋ผ ์ธ์ฆ์ด ํ์
- JwtAuthenticationFilter๊ฐ ํ ํฐ์ ์ฑ๊ณต์ ์ผ๋ก ๊ฒ์ฆํ์ฌ ์ธ์ฆ์ ์๋ฃ์ํค๊ณ , ์ปจํธ๋กค๋ฌ๊น์ง ์์ฒญ์ด ์ ์์ ์ผ๋ก ๋๋ฌ
์ฃผ์ ์ปดํฌ๋ํธ:
- ํด๋ผ์ด์ธํธ: Vite React ๊ธฐ๋ฐ ์น์ฑ ๋๋ Chrome Extension
- Spring Boot ์๋ฒ: ๋น์ฆ๋์ค ๋ก์ง ์ฒ๋ฆฌ ๋ฐ API ์ ๊ณต
- NCP Object Storage: ์์ฑ ํ์ผ ์ ์ฅ์
- NCP CLOVA STT: ์์ฑ์ ํ ์คํธ๋ก ๋ณํ
- OpenAI GPT: ๋ต๋ณ์ ๋ํ AI ํผ๋๋ฐฑ ์์ฑ
- MySQL: Answer, Feedback, Question ๋ฑ ๋ฐ์ดํฐ ์๊ตฌ ์ ์ฅ
| Java v21 | MySQL v8.0 | Spring v3.5.5 | Docker v27.3.1 | H2 v2.2.224 | nGrinder v3.5.9 |
|---|
- ํ๋ ์์ํฌ: Spring Boot 3.5.5
- ์ธ์ด: Java 21
- ๋น๋ ๋๊ตฌ: Gradle 8.11
- ์ธ์ฆ: Spring Security OAuth2 Client, JWT (jjwt 0.11.5)
- ๋ฐ์ดํฐ๋ฒ ์ด์ค: MySQL 8.0 (InnoDB)
- ORM: Spring Data JPA
- AI ์ฐ๋:
- OpenAI GPT (Spring AI 1.0.0)
- NCP CLOVA STT (์์ฑโํ ์คํธ ๋ณํ)
- ์คํ ๋ฆฌ์ง: NCP Object Storage (AWS S3 ํธํ)
- ๋ชจ๋ํฐ๋ง: Spring Actuator, nGrinder 3.5.9
- ๋ฌธ์ํ: SpringDoc OpenAPI 2.8.1
- ๋น๋๊ธฐ ํต์ : Server-Sent Events (SSE)
- ํ๋ ์์ํฌ: React (TypeScript)
- ํ์ฅ ํ๋ก๊ทธ๋จ: Chrome Extension
- ์ปจํ ์ด๋ํ: Docker, Docker Compose
- ์ด๋ฏธ์ง ๋น๋: Jib
- CI/CD: GitHub Actions
- ๋ฐฐํฌ: SSH ๊ธฐ๋ฐ ์๋ ๋ฐฐํฌ
dailyq/
โโโ src/main/java/com/knuissant/dailyq/
โ โโโ config/ # ์ค์ ํด๋์ค (Security, OAuth2, JWT ๋ฑ)
โ โโโ controller/ # REST API ์ปจํธ๋กค๋ฌ
โ โโโ domain/ # ๋๋ฉ์ธ ์ํฐํฐ (Answer, Question, User ๋ฑ)
โ โโโ dto/ # ๋ฐ์ดํฐ ์ ์ก ๊ฐ์ฒด
โ โโโ event/ # ์ด๋ฒคํธ ์ฒ๋ฆฌ
โ โโโ exception/ # ์์ธ ์ฒ๋ฆฌ ๋ฐ ์๋ฌ ์ฝ๋
โ โโโ external/ # ์ธ๋ถ API ์ฐ๋ (GPT, NCP)
โ โโโ jwt/ # JWT ํ ํฐ ์์ฑ ๋ฐ ๊ฒ์ฆ
โ โโโ repository/ # ๋ฐ์ดํฐ ์ ๊ทผ ๊ณ์ธต (JPA Repository)
โ โโโ service/ # ๋น์ฆ๋์ค ๋ก์ง ๊ณ์ธต
โโโ src/main/resources/
โ โโโ application.yml # ์ ํ๋ฆฌ์ผ์ด์
์ค์
โ โโโ prompts/ # AI ํ๋กฌํํธ ํ
ํ๋ฆฟ
โ โโโ static/ # SQL ์คํฌ๋ฆฝํธ (์คํค๋ง, ๋ชฉ ๋ฐ์ดํฐ)
โโโ build.gradle # ์์กด์ฑ ๊ด๋ฆฌ
- Java 21 ์ด์
- Gradle 8.11 ์ด์
- Docker ๋ฐ Docker Compose
- MySQL 8.0
- ์ฌ์ฉ์: ๋งค์ผ ๊พธ์คํ ์ฐ์ต โ ์ฑ์ฅ ์ฒด๊ฐ โ ๋๊ธฐ ์ ์ง โ ์ทจ์ ์ฑ๊ณต ํ๋ฅ โ
- ์ด์์: ๋ฐ์ดํฐ ์ถ์ (๋ต๋ณ/๋์ด๋/์์์) โ AI ํ์ต ์์ ํ๋ณด โ ์๋น์ค ๊ณ ๋ํ
- ์์ฅ ๊ฒฝ์๋ ฅ: ๊ธฐ์กด ๋ชจ์๋ฉด์ ๋๋น ๊ฐ๋ณ๊ณ , ๋ฉ์ผ ๊ธฐ๋ฐ ๋๋น ๊ฐ์ ์ฑ์ด ์๋ '๋ฐ์ผ๋ฆฌ ๋ฃจํด' ์ฐจ๋ณํ
- ๋ฉํฐ๋ชจ๋ฌ ํผ๋๋ฐฑ
- ํ์ฌ๋ ํ ์คํธ ์์ฃผ์ ํผ๋๋ฐฑ โ ํค ๋ถ์๊น์ง ํ์ฅ
- ์์ฑ ๋ฐ์ดํฐ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ํ๋ยท๋ชฉ์๋ฆฌ ์์ ์ฑ๊น์ง ๋ถ์
- ์์์ ๊ธฐ๋ฐ ๋ง์ถค ์ง๋ฌธ ์์ฑ
- ์ฌ์ฉ์ ์์์ ์ ๋ก๋ โ N๊ฐ ํต์ฌ ๋ฌธ์ฅ ์ถ์ถ โ ์ง๊ตฐ/๊ธฐ์ ์ปจํ ์คํธ๋ก ์ง๋ฌธ ์์ฑ







