์ฑ์ฉ๊ณต๊ณ ์ ์ด๋ ฅ์๋ฅผ ํ์คํํด, AI๊ฐ ๊ฐ์ฅ โ์ ๋ง๋ ์ธ์ฌโ๋ฅผ ์ฐพ์์ฃผ๋ ์ฑ์ฉ ์ฝํ์ผ๋ฟ
์ก๋ค(JobDa) ๋ ์ฑ์ฉ ๋ด๋น์์ ๋ฉด์ ๊ด์ ์
๋ฌด ํจ์จ์ ํ๊ธฐ์ ์ผ๋ก ๋์ด๋
AI ๊ธฐ๋ฐ ์ฑ์ฉ ์ถ์ฒ & ๋ฉด์ ์ง์ ์์คํ
์
๋๋ค.
ํ์ฌ์ ์ฑ์ฉ ์์คํ
์์ ์ฑ์ฉ๋ด๋น์๋ ์๋ฐฑ ๊ฐ ์ด๋ ฅ์์์ ์ํ๋ ์๊ตฌ์คํฌยท๊ฒฝ๋ ฅ ์ ํฉ๋๋ฅผ ๋น ๋ฅด๊ฒ ํ๋ณํ๊ธฐ ์ด๋ ต์ต๋๋ค.
๊ธฐ์กด ์ฑ์ฉ์์๋ ์ด๋ ฅ์์ ๊ณต๊ณ ์ ํํ ๋ฐฉ์์ด ์ ๊ฐ๊ฐ์ด๋ผ ์ ํฉํ ์ธ์ฌ๋ฅผ ์ฐพ๊ธฐ ์ด๋ ต๊ณ ,
๋ฉด์ ์ ์ง๋ฌธ ๊ตฌ์ฑ, ๊ธฐ๋ก, ํผ๋๋ฐฑ์ด ์ผ๊ด๋์ง ์์ ์ฑ์ฉ ๊ฒฐ์ ์ด ๋นํจ์จ์ ์ด์์ต๋๋ค.
์ก๋ค๋ ์ด๋ฌํ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด, ๋ค์๊ณผ ๊ฐ์ ๊ธฐ๋ฅ์ ์ ๊ณตํฉ๋๋ค:
- โจ JD(์ฑ์ฉ๊ณต๊ณ ) & ์ด๋ ฅ์ ์๋ ํ์คํ
- ๐ ๊ฒ์ ์์ง ๊ธฐ๋ฐ์ ๊ฐ์ฅ ์๋ง๋ ์ด๋ ฅ์/ํ๋ณด ์ถ์ฒ
- ๐ค AI ๊ธฐ๋ฐ ์ง๋ฌธ ์ธํธ ์์ฑ + ์ค์๊ฐ ๋ฉด์ ์งํ
- ๐ ๊ณต๊ณ ํํฉ/๋ฉด์ ์ผ์ ์บ๋ฆฐ๋ ๋ฑ ์๊ฐํ๋ ๋์๋ณด๋
- ๐ง ๋ฉด์ ๊ธฐ๋ก ์๋ ์์ฝ ๋ฐ ๊ฐ์ /๊ฐ์ ์ ๋์ถ
- "์ ํฉํ ์ง์์๋ฅผ ๋น ๋ฅด๊ฒ ์ฐพ๊ณ ์ถ์ด์"
- "๋ฉด์ ์ ์ผ๊ด๋๊ณ ํจ์จ์ ์ผ๋ก ์งํํ๊ณ ์ถ์ด์"
- "ํ ๋ด ์ฑ์ฉ ํ๋ก์ธ์ค๋ฅผ ์ ๋์ ์ผ๋ก ๊ด๋ฆฌํ๊ณ ์ถ์ด์"
์ฑ์ฉ์ ์ ํ๋๋ ๋์ด๊ณ , ์๊ฐ์ ์ค์ด์ธ์.
๋ฉด์ ๊ด์ ์๊ฐ์ ์๋ผ๊ณ , ์ฑ์ฉ์ ํจ์จ์ ์ผ๋ก ์งํํ๋ ์ฑ์ฉ ํ๋ก์ธ์ค์ ์ง์ง ์ฝํ์ผ๋ฟ, ์ก๋ค(JobDa) โจ
- ํ๋ก์ ํธ ์๊ฐ
- ์์คํ ์ํคํ ์ฒ
- ERD
- ๊ธฐ์ ์คํ ๋ฐ ๋์ ์ด์
- ํต์ฌ ๊ธฐ๋ฅ ์๊ฐ
- ํธ๋ฌ๋ธ ์ํ
- ํ์ ๊ตฌ์ฑ
| ์ค์๊ฐ ์๋ฆผ |
|---|
![]() |
| ์ค์๊ฐ ์ฑํ |
|---|
![]() |
| ์ถ์ฒ ์ง์์ ์กฐํ |
|---|
![]() |
| ๋ฉด์ ์ง๋ฌธ ์์ฑ |
|---|
![]() |
| AI ๋ฉด์ ์์ฝ |
|---|
![]() |
๐ ๋ฌธ์ ์ํฉ
์ด๋ ฅ์ ์ถ์ฒ API ํธ์ถ ์, ๋งค ์์ฒญ๋ง๋ค ๋์ผํ ์ถ์ฒ ๋ก์ง๊ณผ AI ํธ์ถ์ด ๋ฐ๋ณต๋๋ฉฐ ์ด๊ธฐ ์๋ต ์๊ฐ์ด 5์ด ์ด์ (5400ms) ์์๋๋ ์ฑ๋ฅ ๋ฌธ์ ๊ฐ ๋ฐ์
ํนํ:
- Elasticsearch ๊ฒ์
- ๋ค์์ JPA ์ฐ๊ด ์ํฐํฐ ์กฐํ
- JD ํค์๋ ์ถ์ถ / ์ด๋ ฅ์ ์์ฝ / ์ถ์ฒ ์ฌ์ ์์ฑ ๋ฑ OpenAI ๊ธฐ๋ฐ ์ฐ์ฐ ์ด ํ ์์ฒญ ์์์ ๋ชจ๋ ์ํ๋๋ฉฐ ์๋ต ์๋ ์ธก๋ฉด์์ ๋ฌธ์ ๊ฐ ๋ช ํํ ๋๋ฌ๋ฌ์.
๐ ์์ธ ๋ถ์
- ์ค๋ณต ์ฐ์ฐ ๋ฌธ์
- ๋์ผ JD์ ๋ํด ์ถ์ฒ ๊ฒฐ๊ณผ๊ฐ ๋งค๋ฒ ์๋ก ๊ณ์ฐ๋จ
- N+1 ๋ฌธ์
- Resume ์กฐํ ์ ๋ค์์ ์ฐ๊ด ์ํฐํฐ๊ฐ Lazy ๋ก๋ฉ
- AI ํธ์ถ ๋น์ฉ - ์ฒซ ํธ์ถ ์ OpenAI ๊ธฐ๋ฐ ์ฐ์ฐ์ด ๋ค์ ํฌํจ๋์ด ์ต์ 3์ด ์ด์ ์์
1๏ธโฃ 1์ฐจ ๊ฐ์ โ Redis ์บ์ ๋์ (์ถ์ฒ ๊ฒฐ๊ณผ ์บ์ฑ)
โ๏ธ ์ ์ฉ ๋ด์ฉ
- JD ๊ธฐ์ค ์ถ์ฒ ๊ฒฐ๊ณผ๋ฅผ Redis์ ์บ์ฑ
- ๋ ๋ฒ์งธ ํธ์ถ๋ถํฐ๋ ๊ณ์ฐ ์์ด ์ฆ์ ๋ฐํ
redisTemplate.opsForValue().set("recommend:jd_" + jdId, data, TTL);๐ ๊ฒฐ๊ณผ
| ํธ์ถ ํ์ | ์คํ ์๊ฐ |
|---|---|
| ์ฒซ ํธ์ถ | 5409ms |
| ๋ ๋ฒ์งธ ํธ์ถ | 737ms |
| ์ธ ๋ฒ์งธ ํธ์ถ | 592ms |
๐ ์บ์ ํจ๊ณผ๋ ๋ช ํํ์ง๋ง, ์ฒซ ํธ์ถ์ด ์ฌ์ ํ ๋๋ฆผ
2๏ธโฃ 2์ฐจ ๊ฐ์ โ N+1 ๋ฌธ์ ํด๊ฒฐ ์๋ (Fetch Join) โ๏ธ ์๋
์ฐ๊ด ์ํฐํฐ๋ฅผ ํ ๋ฒ์ ๋ก๋ฉํ๊ธฐ ์ํด Fetch Join ์ ์ฉ
@Query("""
SELECT r FROM Resume r
LEFT JOIN FETCH r.educations
LEFT JOIN FETCH r.experiences
LEFT JOIN FETCH r.skills
WHERE r.id = :resumeId
""")โ ๋ฌธ์ ๋ฐ์
MultipleBagFetchException:
cannot simultaneously fetch multiple bags- Hibernate๋ 2๊ฐ ์ด์์ List ์ปฌ๋ ์ Fetch Join์ ํ์ฉํ์ง ์์
โ๏ธ ์ต์ข ํด๊ฒฐ
- 1๊ฐ ์ปฌ๋ ์ ๋ง Fetch Join
- ๋๋จธ์ง๋ @BatchSize ์ ์ฉ
@Query("""
SELECT DISTINCT r FROM Resume r
LEFT JOIN FETCH r.experiences
WHERE r.id = :resumeId
""")
Optional<Resume> findWithEssentialDetailsById(Long resumeId);3๏ธโฃ ํต์ฌ ํด๊ฒฐ โ ๋น๋๊ธฐ ์บ์ฑ + Lazy ์ถ์ฒ ์ ๊ณต
๐ก ์ค๊ณ ์ ํ -> โ์ฒซ ์์ฒญ์์ ๋ชจ๋ ๊ฑธ ๊ณ์ฐํ์ง ๋ง์โ
๊ธฐ์กด ๋ฐฉ์
- ์์ฒญ โ ๋ชจ๋ ๊ณ์ฐ ์ํ โ ์๋ต
๊ฐ์ ๋ฐฉ์
Client Request
โ
Redis Cache ํ์ธ
โ โ
Cache Hit โ ์ฆ์ ๋ฐํ
โ
Cache Miss โ ๋น๋๊ธฐ ์ถ์ฒ ๊ณ์ฐ ์์
โ "์ถ์ฒ ์ค๋น ์ค" ์๋ต
โ๏ธ ๋น๋๊ธฐ ์บ์ฑ ์ ์ฉ
@Async
public void warmUpRecommendation(Long jdId) {
List<ResumeRecommendationDto> result =
recommendationCoreService.calculateRecommendations(jdId);
redisRecommendationCacheService.saveRecommendations(jdId, result);
}๐ ์ต์ข ์ฑ๋ฅ ๊ฒฐ๊ณผ
| ์์ | ์คํ ์๊ฐ |
|---|---|
| ์ฒซ ์์ฒญ (๋น๋๊ธฐ ํธ๋ฆฌ๊ฑฐ) | ~1700ms |
| ๋ ๋ฒ์งธ ํธ์ถ | 102ms |
| ์ธ ๋ฒ์งธ ํธ์ถ | 14ms |
๐ ์ฒด๊ฐ ์ฑ๋ฅ ๋ํญ ๊ฐ์ + ์๋ฒ ๋ถํ ๊ฐ์
4๏ธโฃ ์ถ๊ฐ ๋ฆฌํฉํ ๋ง โ AI ๊ฒฐ๊ณผ ์์์ฑ ๊ฐ์ ๐ง ๋ฌธ์ ์ธ์
- JD ํค์๋, ์ด๋ ฅ์ ์์ฝ, ์ถ์ฒ ์ฌ์ ๋ ์์์ฑ์ด ๋์ ๋ฐ์ดํฐ๋ก, Redis์๋ง ์ ์ฅํ๋ ๊ตฌ์กฐ๋ ์ ์ ํ์ง ์์
โ๏ธ ๊ฐ์ ๊ตฌ์กฐ
์์ฒญ
โ
Redis ์กฐํ
โ โ
HIT MISS
โ โ
๋ฐํ DB ์กฐํ
โ โ
HIT MISS
โ โ
Redis ์บ์ฑ AI ํธ์ถ
โ
DB ์ ์ฅ + Redis ์บ์ฑ
โ๏ธ ํจ๊ณผ
- AI ํธ์ถ ํ์ ์ต์ํ
- ์ฌ์์/TTL ๋ง๋ฃ์๋ ๋ฐ์ดํฐ ์ ์ง
- Redis๋ ๊ฐ์ ๋ ์ด์ด ์ญํ ๋ง ์ํ
- Redis ์บ์ฑ์ผ๋ก ์ค๋ณต ์ฐ์ฐ ์ ๊ฑฐ
- Fetch Join + BatchSize๋ก N+1 ๋ฌธ์ ์ํ
- ๋น๋๊ธฐ ์บ์ฑ์ผ๋ก ์ฒซ ํธ์ถ ์ฒด๊ฐ ์ฑ๋ฅ ๊ฐ์
- AI ๊ฒฐ๊ณผ๋ DB์ ์์ ์ ์ฅํ์ฌ ๊ตฌ์กฐ์ ์์ ์ฑ ํ๋ณด
๐ ๋ฌธ์ ์ํฉ
๋ก์ปฌ ํ๊ฒฝ์์๋ ์ ์ ๋์ํ๋ SSE(Server-Sent Events) ์๋ฆผ์ด ๋ฐฐํฌ ํ๊ฒฝ(Vercel + Nginx Reverse Proxy)์์ ์ค์๊ฐ์ผ๋ก ๋ฐ์๋์ง ์๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ค.
- ์๋ฆผ์ด ์ฆ์ ์์ ๋์ง ์์
- ํ์ด์ง ์๋ก๊ณ ์นจ ์์๋ง ์๋ฆผ์ด ๋์ฐฉ
- ์๋ฒ ๋ก๊ทธ ์ ์ด๋ฒคํธ๋ ์ ์์ ์ผ๋ก ๋ฐํ๋จ
๐ ํ๋ก ํธ/๋ฐฑ์๋ ์ฝ๋์๋ ๋ฌธ์ ๊ฐ ์์ด ๋ณด์์ง๋ง, ์ค์ ์ฌ์ฉ์ ๊ฒฝํ์ ์ค์๊ฐ์ด ์๋์์
๐ ์์ธ ๋ถ์
1๏ธโฃ ๋ฐฐํฌ ํ๊ฒฝ์์๋ง ๋ฐ์ํ๋ ํ์
- ๋ก์ปฌ(Spring Boot ์ง์ ์ฐ๊ฒฐ) โ ์ ์
- ๋ฐฐํฌ(Vercel โ Nginx โ Spring Boot) โ ์ง์ฐ ๋ฐ์
โ ์ธํ๋ผ ๋ ๋ฒจ ์ด์ ๊ฐ๋ฅ์ฑ ํ๋จ
2๏ธโฃ Nginx Reverse Proxy ๊ธฐ๋ณธ ๋์
- Nginx๋ ๊ธฐ๋ณธ์ ์ผ๋ก ์๋ต ๋ฒํผ๋ง(proxy buffering)์ ํ์ฑํ
- ์ผ์ ํฌ๊ธฐ ๋๋ ์กฐ๊ฑด์ด ์ถฉ์กฑ๋ ๋๊น์ง ์๋ต์ ๋ชจ์์ ์ ์ก
3๏ธโฃ SSE ํน์ฑ๊ณผ์ ์ถฉ๋
-
SSE๋ ์คํธ๋ฆฌ๋ฐ ๋ฐฉ์์ผ๋ก ์ด๋ฒคํธ๋ฅผ ์ฆ์ ์ ์กํด์ผ ํจ
-
๊ทธ๋ฌ๋:
- ์๋ฆผ payload๊ฐ ์์
- ๋ฒํผ ์กฐ๊ฑด์ ์ถฉ์กฑํ์ง ๋ชปํด ์ ์ก์ด ์ง์ฐ
- ๊ฒฐ๊ณผ์ ์ผ๋ก โ์ ์ค๋ ๊ฒ์ฒ๋ผ ๋ณด์ด๋โ ํ์ ๋ฐ์
๐ SSE๋ ์๋ฒ ์ฝ๋๋ฟ ์๋๋ผ ํ๋ก์ ์ค์ ์ ๊ฐํ๊ฒ ์์กดํ๋ ๊ธฐ์
๐ ํด๊ฒฐ ๋ฐฉ๋ฒ
Nginx SSE ์ ์ฉ ์ค์ ๋ถ๋ฆฌ
SSE ์๋ํฌ์ธํธ(/api/v1/sse/subscribe)์ ๋ํด์๋ง
์๋ต ๋ฒํผ๋ง ๋นํ์ฑํ ๋ฐ ์คํธ๋ฆฌ๋ฐ ์ค์ ์ ์ฉ
location /api/v1/sse/subscribe {
proxy_pass http://backend;
proxy_buffering off;
proxy_cache off;
proxy_set_header Connection keep-alive;
proxy_set_header Cache-Control no-cache;
proxy_set_header X-Accel-Buffering no;
}์ ์ฉ ํฌ์ธํธ
proxy_buffering off: ์๋ต ์ฆ์ ์ ๋ฌX-Accel-Buffering: no: Nginx ๋ด๋ถ ๋ฒํผ๋ง ์์ ์ฐจ๋จConnection: keep-alive: SSE ์ฐ๊ฒฐ ์ ์ง- ์ผ๋ฐ API์ SSE ์ค์ ์ ๋ถ๋ฆฌํ์ฌ ์ํฅ ์ต์ํ
โ ๊ฒฐ๊ณผ
| ํญ๋ชฉ | ๊ฐ์ ์ | ๊ฐ์ ํ |
|---|---|---|
| ์๋ฆผ ์์ ์์ | ์๋ก๊ณ ์นจ ํ์ | ์ฆ์ ์์ |
| SSE ์ฐ๊ฒฐ ์์ ์ฑ | ๋ถ์์ | ์์ ์ ์ ์ง |
| ๋ก์ปฌ/๋ฐฐํฌ ํ๊ฒฝ | ๋์ ๋ถ์ผ์น | ๋์ ๋์ผ |
๐ ๋ฐฐํฌ ํ๊ฒฝ์์๋ SSE ์ค์๊ฐ ์๋ฆผ ์ ์ ๋์ ํ์ธ
๐ก ๋ฐฐ์ด ์
- SSE๋ ๋ฐฑ์๋ ์ฝ๋๋ง์ผ๋ก ์์ฑ๋์ง ์๋ ๊ธฐ์
- ํ๋ก์ยท๋คํธ์ํฌยท์ธํ๋ผ ์ค์ ์ด ํต์ฌ ์์
- ๋ก์ปฌ ํ ์คํธ๋ง์ผ๋ก๋ ์ค์๊ฐ ๊ธฐ๋ฅ์ ์์ฑ๋๋ฅผ ๋ณด์ฅํ ์ ์์
- ์ค์๊ฐ ๊ธฐ๋ฅ(SSE, WebSocket)์ ๋ฐ๋์ ๋ฐฐํฌ ํ๊ฒฝ ๊ธฐ์ค์ผ๋ก ๊ฒ์ฆํด์ผ ํจ
- ๋ฌธ์ ์์ธ์ ์ฝ๋๊ฐ ์๋ Nginx Reverse Proxy ์๋ต ๋ฒํผ๋ง
- SSE ์๋ํฌ์ธํธ์ ํํด ์คํธ๋ฆฌ๋ฐ ์ค์ ๋ถ๋ฆฌ ์ ์ฉ
- ๋ฐฐํฌ ํ๊ฒฝ์์๋ ์ค์๊ฐ ์๋ฆผ ์์ ํ
- ์ค์๊ฐ ์์คํ ์ค๊ณ ์ ์ธํ๋ผ๊น์ง ํฌํจํ ๊ด์ ์ ์ค์์ฑ ์ฒด๊ฐ
๐ ๋ฌธ์ ์ํฉ
๋ก์ปฌ์์๋ ๋ฉด์ ์์ฑ/์ข ๋ฃ ์ AI๊ฐ MCP tool์ ํธ์ถํด ์ง๋ฌธ/์์ฝ์ ์ ์ฅํ๋ ํ๋ฆ์ด ์ ์์ธ๋ฐ, ๋ฐฐํฌ ํ๊ฒฝ์์ ๊ฐํ์ ์ผ๋ก ํ๋ฆ์ด ๊นจ์ก๋ค.
- LLM์ด get_interview_context, save_interview_questions ๊ฐ์ tool์ ์ ๋ถ๋ฅด๊ณ ํ ์คํธ๋ก๋ง ๋ตํจ(์ค์ ๋ก tool ํธ์ถ ์คํจ/๋ฏธ์ ์ฉ์ผ๋ก ๋ณด์)
- ๋ก์ปฌ์ ์ ์์ธ๋ฐ ๋ฐฐํฌ์์๋ง ๊ฐํ ์คํจ
- tool calling์ ์๋ณต์ด ๋ง์ ์ฒด๊ฐ ์ง์ฐ์ด ์ปค์ง
- ์ด๋์ ๋งํ๋์ง ์ถ์ ์ด ์ด๋ ค์(ํ๋ก ํธ/ํ๋ก์/๋ฐฑ์๋/LLM/์๊ฒฉ MCP)
๐ ์์ธ ๋ถ์
1๏ธโฃ ์ธ์ฆ/์ธ์ ์ ๋ฌ ๋จ์ (CORS + Cookie/SameSite + ํ๋ก์ ํค๋)
๋ฐฐํฌ ํ๊ฒฝ์์๋ ์์ฒญ์ด ํฌ๋ก์ค ์ฌ์ดํธ + ํ๋ก์ ๊ฒฝ์ ๊ฐ ๋๋ฉด์, ๋ก์ปฌ์์ ๋ฌธ์ ์๋ ์ธ์ฆ ์ ๋ฌ์ด tool ๊ตฌ๊ฐ์์๋ง ๋๊ฒผ๋ค.
๋ํ ํจํด:
credentials๊ฐ ํฌํจ๋ CORS ์ค์ ๋ฏธํก(Origin/Allow-Credentials ๋ถ์ผ์น)- Cookie๊ฐ
SameSite/Secure์กฐ๊ฑด ๋๋ฌธ์ ๋ธ๋ผ์ฐ์ ์์ ์ ์ก๋์ง ์์ - Nginx๊ฐ
Authorization,Cookie,X-Forwarded-*๊ฐ์ ํค๋๋ฅผ ์ ๋๋ก ์ ๋ฌํ์ง ์์ ๋ฐฑ์๋์์ ์ธ์ฆ ์คํจ โ tool endpoint ์ง์ ์ 401/403 ์๋ฌ โ LLM์ tool ๊ฒฐ๊ณผ๋ฅผ ๋ชป ๋ฐ์ fallback ๋ต๋ณ ์์ฑ
2๏ธโฃ ์ถ๊ฐ HTTP ์๋ณต์ผ๋ก ์ธํ ์ง์ฐ/ํ์์์ (tool calling ๊ตฌ์กฐ์ ํน์ฑ)
tool calling์ ์ผ๋ฐ LLM ํธ์ถ ๋๋น ์๋ณต์ด ํฌ๊ฒ ๋์ด๋๋ค.
๋ก์ปฌ์์ ๋คํธ์ํฌ/ํ๋ก์๊ฐ ์์ผ๋ ๋ฒํ ผ์ง๋ง, ๋ฐฐํฌ์์ ์๋๊ฐ ๋์์ ํฐ์ก๋ค.
-
LLM ์๋ต โ tool ํธ์ถ โ ๋ด๋ถ API/DB ์กฐํ โ ๋ค์ LLM๋ก ๊ฒฐ๊ณผ ์ ๋ฌ
์ด ์ฒด์ธ์์ ํ ๊ตฐ๋ฐ๋ง ๋๋ ค๋ ์ ์ฒด๊ฐ timeout ๋๋ ํ์
-
Nginx ๊ธฐ๋ณธ timeout(proxy_read_timeout ๋ฑ) / upstream timeout์ด tool ์๋ณต ์๊ฐ์ ๋ชป ๋ฒํ
-
๋ก๊ทธ๊ฐ ๋ถ์ฐ๋์ด โ์ด๋์ ๋๋ ค์ก๋์งโ ํ ๋ฒ์ ์ ๋ณด์ โ timeout/์ฌ์๋/๊ฐํ ์คํจ โ โ๋ฐฐํฌ์์๋ง ํ๋ฆ์ด ๊นจ์งโ์ผ๋ก ์ฒด๊ฐ
๐ ํด๊ฒฐ ๋ฐฉ๋ฒ
1๏ธโฃ ์ธ์ฆ ์ ๋ฌ ์์ ํ (๋ฐฐํฌ ๊ธฐ์ค์ผ๋ก ๊ณ ์ )
- CORS: ํน์ Origin ๋ช
์ +
allowCredentials(true)์ ๋ฆฌ - Cookie ์ ์ฑ
: ํฌ๋ก์ค ์ฌ์ดํธ ์๊ตฌ์ฌํญ์ ๋ง๊ฒ
SameSite=None; Secure๋ฑ ์ ๋ฆฌ(์ฌ์ฉ ๊ตฌ์กฐ์ ๋ง์ถฐ) - Nginx ํ๋ก์ ํค๋ ์ ๋ฌ ๊ฐํ
Authorization,Cookie,X-Forwarded-Proto,X-Forwarded-For,Host๋ฑ ์ ๋ฌ ๋๋ฝ ๋ฐฉ์ง
- tool endpoint์์ ์ธ์ฆ ์คํจ ์ ์ฆ์ ์๋ณ๋๊ฒ ์๋ฌ/๋ก๊ทธ ๋ณด๊ฐ
2๏ธโฃ ํ์์์/์๋ณต ๋์ (tool route ์ค์ฌ์ผ๋ก)
- Nginx / ๋ฐฑ์๋ timeout ์ํฅ(ํนํ tool ํธ์ถ ๊ฒฝ๋ก)
- keep-alive ๋ฐ upstream ์ค์ ์ ๋ฆฌ
- ๋ก๊ทธ ์ถ์ ์ฅ์น ์ถ๊ฐ
- ์์ฒญ ๋จ์ Correlation ID
- tool ํธ์ถ ์์/์ข ๋ฃ/์์์๊ฐ ๋ก๊ทธ
- ์ธ๋ถ ํธ์ถ(OpenAI/๋ด๋ถ API) ์์์๊ฐ ๋ถ๋ฆฌ ๋ก๊น
๋ฐฐํฌ ํ๊ฒฝ์์ โ๋๊ตฌ๊ฐ ์ค์ ๋ก ๋ก๋ฉ/์ฃผ์ ๋๋์งโ๋ฅผ ๋จผ์ ํ์ธํ๊ธฐ ์ํด, ์๊ฒฉ MCP Tool ๋ชฉ๋ก์ ๋ก๊น ํ๊ณ ChatClient ๊ธฐ๋ณธ ToolCallbacks๋ก ์ธํ ํ๋ค.
@Bean
public ChatClient chatClient(ChatClient.Builder builder,
SyncMcpToolCallbackProvider mcpToolCallbackProvider) {
ToolCallback[] callbacks = mcpToolCallbackProvider.getToolCallbacks();
log.info("[MCP] ์๊ฒฉ MCP Tool ๊ฐ์ = {}", callbacks.length);
for (ToolCallback cb : callbacks) {
log.info("[MCP] ToolCallback = {}", cb);
}
return builder
.defaultToolCallbacks(callbacks)
.defaultAdvisors(new SimpleLoggerAdvisor())
.build();
}
์ถ๊ฐ๋ก tool endpoint์์ โ์ง์ /์ฑ๊ณต/์คํจโ ๋ก๊ทธ๋ฅผ ๊ณ ์ ํด LLM์ด ๋๊ตฌ๋ฅผ ์ ๋ถ๋ฅธ ๊ฑด์ง vs ๋ถ๋ ๋๋ฐ ์คํจํ ๊ฑด์ง๋ฅผ ๋ถ๋ฆฌํ๋ค.
log.info("[MCP] saveInterviewQuestions called interviewId={}, rawSize={}",
interviewId, questionList != null ? questionList.size() : -1);
try {
...
interviewQuestionService.updateQuestionsBySystem(interviewId, requestDto);
log.info("[MCP] saveInterviewQuestions success interviewId={}, savedCount={}",
interviewId, items.size());
} catch (Exception e) {
log.error("[MCP] saveInterviewQuestions FAILED interviewId={}, reason={}",
interviewId, e.getMessage(), e);
throw e;
}
โ ๊ฒฐ๊ณผ
- ๋ฐฐํฌ ํ๊ฒฝ์์ tool calling ํฌํจ ์์ฒญ์ 401/403/timeout ๋น๋ ๊ฐ์
- LLM์ด tool ๊ฒฐ๊ณผ๋ฅผ ์ ์ ์์ ํ๋ฉด์ โ์์ ๋ต๋ณ ์์ฑโ ํ์ ํด์
- ๋ณ๋ชฉ ๊ตฌ๊ฐ์ด ๋ก๊ทธ๋ก ๋ถํด๋์ด, ์ดํ ์ฑ๋ฅ/์์ ํ ์์ ์ด ๊ฐ๋ฅํด์ง
๐ก ๋ฐฐ์ด ์
- tool calling์ โ๋ชจ๋ธ ๊ธฐ๋ฅโ์ด ์๋๋ผ โ๋ถ์ฐ ์์ฒญ ์ฒด์ธโ์ด๋ค
- ๋ก์ปฌ ์ฑ๊ณต์ ์๋ฏธ๊ฐ ์ฝํ๊ณ , ๋ฐฐํฌ ํ๊ฒฝ(CORS/์ฟ ํค/ํ๋ก์/timeout)์ด ์ค์ ์ ๋ต์ด๋ค
- tool ๊ธฐ๋ฐ ๊ธฐ๋ฅ์ ๊ด์ธก(๋ก๊ทธ/์๊ฐ์ธก์ /ID ์ถ์ ) ์์ผ๋ฉด ๋๋ฒ๊น ์ด ๊ฑฐ์ ๋ถ๊ฐ๋ฅํ๋ค
- ๋ฐฐํฌ ํ๊ฒฝ์์ ๋๊ธฐ๋ ์ธ์ฆ/์ธ์ ์ ๋ฌ(CORS, Cookie SameSite/Secure, ํ๋ก์ ํค๋)์ ์ ๋ฆฌํด tool endpoint์ 401/403์ ์ ๊ฑฐ
- ์๊ฒฉ MCP ToolCallback ๋ก๋ฉ ์ฌ๋ถ๋ฅผ ๋ก๊น ํด โ๋๊ตฌ๊ฐ ๋ถ์๋์งโ๋ฅผ ๋ฐฐํฌ์์ ์ฆ์ ๊ฒ์ฆ ๊ฐ๋ฅํ๊ฒ ํจ
- ๊ฒฐ๊ณผ์ ์ผ๋ก ๋ฐฐํฌ ํ๊ฒฝ์์๋ tool calling ํ๋ฆ์ด ์์ ํ๋์ด ์ง๋ฌธ/์์ฝ ์๋ ์์ฑ ํ์ดํ๋ผ์ธ์ ์ผ๊ด๋๊ฒ ์ ์ง
| ์ ์น์ธ (ํ์ฅ) | ๊น์ ํธ | ๊น์ง์ค | ์ฌ์๋ฏผ | ์ ์น์ฌ | ์ ๋ค์ |
| FE, BE | FE, BE | FE, BE | FE, BE | FE, BE | FE, BE |





