[๐Spring] Spring โ FastAPI ํ๋ก์ JSESSIONID ์ ๋ฌ ์ด์
Spring Boot๊ฐ FastAPI ํ๋ก์ ์ญํ ์ ํ ๋ JSESSIONID๋ฅผ ํ์ ์๋น์ค๋ก ์ ๋ฌํ์ง ์์ ๋ฐ์ํ 401 ๋ฒ๊ทธ โ ์์ธ ๋ถ์๋ถํฐ MockMvc ํ ์คํธ ํจ์ ๊น์ง ์ ๋ฆฌํฉ๋๋ค.
Spring Boot๊ฐ FastAPI ์๋จ ํ๋ก์ ์ญํ ์ ํ๋ ๊ตฌ์กฐ์์, FastAPI๊ฐ Spring์ ์ญํธ์ถํ ๋ 401์ด ๋ฐ์ํ๋ค. ์์ธ์
JSESSIONID๋ฅผ FastAPI๋ก ์ ๋ฌํ์ง ์์ ๊ฒ์ด์๊ณ , MockMvc๋ก ์ฌํํ๋ ๊ณผ์ ์์๋requestedSessionId๋ฅผ ์ง์ ์ค์ ํด์ผ ํ๋ ํจ์ ์ด ์์๋ค.
์ด ๊ธ์์ ๋ค๋ฃจ๋ ๋ด์ฉ
- ์ด ๊ธ์ ์ฝ๊ธฐ ์ ์ ์์๋๋ฉด ์ข์ ๊ธฐ๋ณธ ๊ฐ๋
- ๋ฌธ์ ์ํฉ ๋ฐ ์์ฒญ ํ๋ฆ ์ ๋ฆฌ
JSESSIONID๋ฏธ์ ๋ฌ์ ๊ทผ๋ณธ ์์ธHttpServletRequest๋ก ์ฟ ํค ํฌ์๋ฉ ๊ตฌํ- MockMvc์์
getRequestedSessionId()๊ฐnull์ ๋ฐํํ๋ ์ด์ ์ ํด๊ฒฐ๋ฒ
์ด ๊ธ์ ์ฝ๊ธฐ ์ ํ์ํ ๋ฐฐ๊ฒฝ
์ด ๊ธ์ Spring Boot๊ฐ FastAPI ์๋จ์์ ํ๋ก์ ์ญํ ์ ํ๋ ๊ตฌ์กฐ๋ฅผ ์ ์ ๋ก ํ๋ค. ํต์ฌ์ ๋ธ๋ผ์ฐ์ ๊ฐ FastAPI๋ฅผ ์ง์ ํธ์ถํ์ง ์๊ณ , ํญ์ Spring์ ๋จผ์ ํธ์ถํ๋ค๋ ์ ์ด๋ค.
1
2
3
๋ธ๋ผ์ฐ์
โโโบ Spring Boot
โโโบ FastAPI
์ด ๊ตฌ์กฐ์์ Spring์ ์ฌ์ฉ์ ์ธ์ ๊ฒ์ฆ๊ณผ API ์ง์ ์ ์ ๋ด๋นํ๊ณ , FastAPI๋ ์ฑ๋ดยท์ถ์ฒ ๊ฐ์ ๋ด๋ถ ๊ธฐ๋ฅ์ ๋ด๋นํ๋ค. ๊ทธ๋์ ํ๋ก ํธ์๋๋ FastAPI ์ฃผ์๋ ๋ด๋ถ ์ธ์ฆ ํค๋ฅผ ์ ํ์๊ฐ ์๋ค.
Spring Proxy
๐ Proxy: ํด๋ผ์ด์ธํธ ์์ฒญ์ ๋์ ๋ฐ์์ ๋ค๋ฅธ ์๋ฒ๋ก ์ ๋ฌํ๋ ์ค๊ฐ ์๋ฒ
์ฌ๊ธฐ์๋ Spring Boot๊ฐ ํ๋ก์ ์ญํ ์ ํ๋ค.
์๋ฅผ ๋ค์ด ํ๋ก ํธ๊ฐ ์๋ Spring API๋ฅผ ํธ์ถํ๋ฉด:
1
POST /api/v1/chat/sessions/1/messages
Spring์ ๋ด๋ถ์์ FastAPI์ ์ค์ API๋ฅผ ํธ์ถํ๋ค.
1
POST http://nutriagent-fastapi:8000/api/v1/chat/sessions/1/messages
์ด๋ ๊ฒ ํ๋ฉด ๋ธ๋ผ์ฐ์ ๋ Spring๋ง ๋ฐ๋ผ๋ณด๊ณ , FastAPI๋ Docker ๋ด๋ถ ์๋น์ค๋ก ์จ๊ธธ ์ ์๋ค.
X-Internal-Key
๐ X-Internal-Key: ์๋ฒ๋ผ๋ฆฌ๋ง ๊ณต์ ํ๋ ๋ด๋ถ ์ธ์ฆ ํค๋
์ฌ์ฉ์ ์ธ์ฆ์ฉ ๋น๋ฐ๋ฒํธ๊ฐ ์๋๋ผ, ์ด ์์ฒญ์ด ์ธ๋ถ ์ฌ์ฉ์๊ฐ ์๋๋ผ Spring ๊ฐ์ ์ ๋ขฐ๋ ๋ด๋ถ ์๋น์ค์์ ์จ ์์ฒญ์ธ์ง ํ์ธํ๊ธฐ ์ํ ๊ฐ์ด๋ค.
1
Spring โโ X-Internal-Key โโโบ FastAPI
๋ธ๋ผ์ฐ์ ์๋ ์ด ๊ฐ์ ์ ๋ ๋ด๋ ค์ฃผ์ง ์๋๋ค. ๋ธ๋ผ์ฐ์ ๊ฐ ์์์ผ ํ ์ธ์ฆ ์ ๋ณด๋ ์ธ์ ์ฟ ํค๋ฟ์ด๋ค.
1
2
3
4
5
6
๋ธ๋ผ์ฐ์ -> Spring
Cookie: JSESSIONID=...
Spring -> FastAPI
X-Internal-Key: ...
X-Guest-Id: ...
๋ง์ฝ FastAPI๊ฐ ์ธ๋ถ์ ์ด๋ ค ์๊ณ X-Internal-Key ๊ฒ์ฆ์ด ์๋ค๋ฉด, ๋๊ตฐ๊ฐ๊ฐ Spring์ ๊ฑฐ์น์ง ์๊ณ FastAPI๋ฅผ ์ง์ ํธ์ถํ ์ ์๋ค. ๊ทธ๋ฌ๋ฉด Spring์ ์ธ์
๊ฒ์ฆ์ ์ฐํํ ์ ์๋ค. ๊ทธ๋์ FastAPI chat router์๋ Depends(verify_internal_call)์ ๋ถ์ฌ ๋ด๋ถ ํค๋ฅผ ๊ฒ์ฆํ๋ค.
JSESSIONID
๐ JSESSIONID: Spring์ HTTP ์ธ์
์ ์๋ณํ๋ ์ฟ ํค
๋ธ๋ผ์ฐ์ ๊ฐ Spring์ ์์ฒญํ ๋ ์ด ์ฟ ํค๋ฅผ ๋ณด๋ด๋ฉด, Spring์ ์๋ฒ์ ์ ์ฅ๋ ์ธ์
์์ GUEST_ID ๊ฐ์ ๊ฐ์ ๊บผ๋ผ ์ ์๋ค.
1
Cookie: JSESSIONID=abc123
์ด ํ๋ก์ ํธ์ Spring ์ปจํธ๋กค๋ฌ๋ค์ @GuestId๋ฅผ ํตํด ์ธ์
์์ guest id๋ฅผ ๊บผ๋ธ๋ค. ์ค์ํ ์ ์ @GuestId๊ฐ ๋จ์ํ X-Guest-Id ํค๋๋ฅผ ๋ฏฟ๋ ๊ฒ์ด ์๋๋ผ, Spring์ HttpSession์ ๋ณธ๋ค๋ ๊ฒ์ด๋ค.
1
2
HttpSession session = request.getSession(false);
String guestId = session != null ? (String) session.getAttribute("GUEST_ID") : null;
๋ฐ๋ผ์ FastAPI๊ฐ ๋์ค์ Spring์ ๋ค์ ํธ์ถํด์ผ ํ๋ค๋ฉด, FastAPI๋ ์ ํจํ JSESSIONID๋ฅผ ๊ฐ์ง๊ณ ์์ด์ผ ํ๋ค.
Docker ports์ expose
Docker Compose์์ ports์ expose๋ ๋ค๋ฅด๋ค.
1
2
ports:
- "8000:8000"
ports๋ ํธ์คํธ ์ธ๋ถ๋ก ํฌํธ๋ฅผ ๊ณต๊ฐํ๋ค. ์๋ฒ IP์ ํฌํธ๋ฅผ ์๋ฉด ์ธ๋ถ์์๋ ์ ๊ทผํ ์ ์๋ค.
1
2
expose:
- "8000"
expose๋ ๊ฐ์ Docker ๋คํธ์ํฌ ์์ ์ปจํ
์ด๋๋ผ๋ฆฌ๋ง ์ ๊ทผํ ์ ์๊ฒ ํ๋ค. ์ด๋ฒ ๊ตฌ์กฐ์์๋ ํ๋ก ํธ๊ฐ FastAPI๋ฅผ ์ง์ ํธ์ถํ์ง ์์ผ๋ฏ๋ก, FastAPI๋ ports ๋์ expose๋ง ์ฌ์ฉํ๋ ํธ์ด ์์ ํ๋ค.
๋ฌธ์ ์ํฉ
Spring Boot๊ฐ FastAPI ์๋จ ํ๋ก์ ์ญํ ์ ํ๋ ๊ตฌ์กฐ์์, ์ฑ๋ด ๋ฉ์์ง ์ ์ก API(POST /sessions/{id}/messages)๋ฅผ ํธ์ถํ๋ฉด FastAPI๊ฐ Spring์ onboarding ์๋ํฌ์ธํธ๋ฅผ ์ญ์ผ๋ก ํธ์ถํ ๋ 401 ์ด ๋ฐํ๋๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ค.
ํ๋ฆ์ ์ ๋ฆฌํ๋ฉด ๋ค์๊ณผ ๊ฐ๋ค.
1
2
3
4
5
6
7
8
9
10
11
12
๋ธ๋ผ์ฐ์ โโJSESSIONIDโโโบ Spring :8080
โ
โ (JSESSIONID ๋ฏธ์ ๋ฌ)
โผ
FastAPI :8000
โ
onboarding tool ํธ์ถ ์
โ JSESSIONID=""
โผ
Spring /onboarding
โ
โโโบ 401 Unauthorized (์ธ์
์์)
FastAPI์ send_message ์๋ํฌ์ธํธ๋ JSESSIONID ์ฟ ํค๋ฅผ ํ๋ผ๋ฏธํฐ๋ก ๋ฐ์ onboarding tool์ context์ ์ ๋ฌํ๋ค.
1
2
# router.py
jsessionid: str | None = Cookie(None, alias="JSESSIONID")
onboarding tool์ ์ด ๊ฐ์ Spring ์ญํธ์ถ ์ ์ฟ ํค๋ก ์ฌ์ฉํ๋ค.
1
2
# tools/onboarding.py
cookies={"JSESSIONID": context.get("jsessionid", "")}
๋ฌธ์ ๋ Spring ChatProxyController๊ฐ ๋ธ๋ผ์ฐ์ ์์ฒญ์ JSESSIONID๋ฅผ FastAPI๋ก ์ ๋ฌํ์ง ์์๋ค ๋ ๊ฒ์ด๋ค. FastAPI๊ฐ ๋ฐ๋ jsessionid๋ ํญ์ None์ด์๊ณ , context.get("jsessionid", "") ๋ ๋น ๋ฌธ์์ด์ ๋ฐํํ๋ค.
์์ธ ๋ถ์
ChatProxyController์ sendMessage, streamMessage๋ FastAPI๋ก ๋ณด๋ด๋ ํค๋๋ฅผ ์ง์ ๊ตฌ์ฑํ๋ค. ๋น์ ์ฝ๋๋ X-Guest-Id์ X-Internal-Key๋ง ์ค์ ํ๊ณ , ๋ธ๋ผ์ฐ์ ๊ฐ ๋ณด๋ธ JSESSIONID๋ฅผ Cookie ํค๋๋ก ํฌ์๋ฉํ๋ ๋ก์ง์ด ์์๋ค.
1
2
3
4
5
6
7
// ์์ ์ : JSESSIONID ์์
private HttpHeaders buildGuestHeaders(String guestId) {
HttpHeaders headers = new HttpHeaders();
headers.set("X-Guest-Id", guestId);
headers.set("X-Internal-Key", internalApiKey);
return headers;
}
Spring์ ์์ฒด HttpSession์์ guestId๋ฅผ ๊บผ๋ด๋ ๋ฐฉ์์ด๋ผ JSESSIONID๋ฅผ ์ง์ ๋ค๋ฃจ์ง ์์๋ ๋๋ค. ํ์ง๋ง FastAPI๊ฐ ๊ทธ ๊ฐ์ ๋ค์ Spring์ผ๋ก ์ ๋ฌํด์ผ ํ๋ ์ํฉ ์ ๊ณ ๋ คํ์ง ๋ชปํ๋ค.
ํด๊ฒฐ ๋ฐฉ๋ฒ
HttpServletRequest๋ฅผ ์ปจํธ๋กค๋ฌ ํ๋ผ๋ฏธํฐ๋ก ์ฃผ์
๋ฐ์, getRequestedSessionId() ๋ก ๋ธ๋ผ์ฐ์ ์ธ์
ID๋ฅผ ์ถ์ถํ ๋ค FastAPI ์์ฒญ์ Cookie ํค๋์ ์ถ๊ฐํ๋ค.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// ChatProxyController.java
@PostMapping("/sessions/{sessionId}/messages")
public ResponseEntity<String> sendMessage(
@GuestId String guestId,
@PathVariable Long sessionId,
@RequestBody Map<String, Object> body,
HttpServletRequest servletRequest) { // ์ถ๊ฐ
HttpHeaders headers = buildGuestHeaders(guestId);
headers.setContentType(MediaType.APPLICATION_JSON);
forwardSessionCookie(servletRequest, headers); // ์ถ๊ฐ
...
}
private void forwardSessionCookie(HttpServletRequest servletRequest, HttpHeaders headers) {
String jsessionid = servletRequest.getRequestedSessionId();
if (jsessionid != null) {
headers.set(HttpHeaders.COOKIE, "JSESSIONID=" + jsessionid);
}
}
SSE ์คํธ๋ฆผ ์๋ํฌ์ธํธ(streamMessage)๋ RequestCallback ๋๋ค ๋ด๋ถ์์ ํค๋๋ฅผ ์ค์ ํ๋ฏ๋ก, ๋๋ค ์บก์ฒ๋ฅผ ์ํด ๋ก์ปฌ ๋ณ์๋ก ๋จผ์ ์ถ์ถ ํด์ผ ํ๋ค.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@PostMapping(value = "/sessions/{sessionId}/messages/stream", ...)
public ResponseEntity<StreamingResponseBody> streamMessage(...,
HttpServletRequest servletRequest) {
String jsessionid = servletRequest.getRequestedSessionId(); // ๋๋ค ๋ฐ์์ ์บก์ฒ
StreamingResponseBody stream = outputStream -> {
chatRestTemplate.execute(url, HttpMethod.POST,
request -> {
...
if (jsessionid != null) {
request.getHeaders().set(HttpHeaders.COOKIE, "JSESSIONID=" + jsessionid);
}
}, ...);
};
}
ํ ์คํธ ํจ์ : MockMvc์์ getRequestedSessionId()๋ null
ํ
์คํธ์์ .session(authSession)์ผ๋ก ์ธ์
์ ์ฃผ์
ํ๋ฉด getRequestedSessionId()๊ฐ null์ ๋ฐํ ํ๋ค.
MockHttpServletRequest.getRequestedSessionId()๋ requestedSessionId ํ๋๋ฅผ ๊ทธ๋๋ก ๋ฐํํ๋๋ฐ, MockMvc๊ฐ .session()์ ์ฒ๋ฆฌํ ๋ ์ด ํ๋๋ฅผ ์๋์ผ๋ก ์ฑ์ฐ์ง ์๋๋ค. ์ค์ ์๋ธ๋ฆฟ ํ๊ฒฝ์์๋ JSESSIONID ์ฟ ํค๋ฅผ ํ์ฑํด ์ด ๊ฐ์ ์ค์ ํ์ง๋ง, MockMvc๋ ๊ทธ ๊ณผ์ ์ ์๋ตํ๋ค.
ํด๊ฒฐ:
MockHttpSession์ ๊ณ ์ ID๋ก ์์ฑํ๊ณ ,RequestPostProcessor๋กrequestedSessionId๋ฅผ ๋ช ์์ ์ผ๋ก ์ค์ ํ๋ค.
1
2
3
4
5
// ํ
์คํธ setUp
private static final String SESSION_ID = "test-jsessionid-001";
authSession = new MockHttpSession(null, SESSION_ID); // ๊ณ ์ ID ์ง์
authSession.setAttribute("GUEST_ID", GUEST_ID);
1
2
3
4
5
6
// ํ
์คํธ ์์ฒญ
mockMvc.perform(post("/api/v1/chat/sessions/1/messages")
.session(authSession)
.with(req -> { req.setRequestedSessionId(SESSION_ID); return req; }) // ๋ช
์์ ์ค์
.contentType(MediaType.APPLICATION_JSON)
.content("{\"message\":\"์๋
\"}"))
๊ทธ๋ฐ ๋ค์ ํค๋ ์บก์ฒ ํ ๊ฒ์ฆ:
1
assertThat(headers.getFirst(HttpHeaders.COOKIE)).contains("JSESSIONID=" + SESSION_ID);
์ ๋ฆฌ
- Spring์ด ์ง์ ์ธ์ ์ธ์ฆ์ ์ฒ๋ฆฌํ๋๋ผ๋, ํ์ ์๋น์ค๊ฐ ๊ทธ ์ธ์ ์ ์ญ์ผ๋ก ์ฌ์ฉํด์ผ ํ๋ ๊ฒฝ์ฐ ๋ ์ฟ ํค ํฌ์๋ฉ์ ๋ช ์์ ์ผ๋ก ๊ตฌํํด์ผ ํ๋ค.
- MockMvc๋ ์ค์ ์๋ธ๋ฆฟ ์ปจํ
์ด๋์ ์ฟ ํค ํ์ฑ ๊ณผ์ ์ ์๋ตํ๋ค.
getRequestedSessionId()์ฒ๋ผ ์ฟ ํค์์ ํ์๋๋ ๊ฐ์RequestPostProcessor๋ก ์๋ ์ค์ ํด์ผ ํ ์คํธ๊ฐ ๋์ํ๋ค.
