[๐งTroubleshooting] Spring โ FastAPI ํ๋ก์์์ JSESSIONID ์ ๋ฌ ๋๋ฝ ๋ฒ๊ทธ
Spring Boot๊ฐ FastAPI ํ๋ก์ ์ญํ ์ ํ ๋ JSESSIONID๋ฅผ ํ์ ์๋น์ค๋ก ์ ๋ฌํ์ง ์์ ๋ฐ์ํ 401 ๋ฒ๊ทธ โ ์์ธ ๋ถ์๋ถํฐ MockMvc ํ ์คํธ ํจ์ ๊น์ง ์ ๋ฆฌํฉ๋๋ค.
Spring Boot๊ฐ FastAPI ์๋จ ํ๋ก์ ์ญํ ์ ํ๋ ๊ตฌ์กฐ์์, FastAPI๊ฐ Spring์ ์ญํธ์ถํ ๋ 401์ด ํฐ์ง๋ ๋ฒ๊ทธ๋ฅผ ๋ง์ฃผ์ณค๋ค. ์์ธ์ ๋จ์ํ์ง๋ง โ JSESSIONID๋ฅผ FastAPI๋ก ์ ๋ฌํ์ง ์์ ๊ฒ โ MockMvc์์ ์ฌํํ๋ ๊ณผ์ ์์ ์์์น ๋ชปํ ํจ์ ์ด ์์๋ค. ๋ถ์๋ถํฐ ํ
์คํธ ํฝ์ค๊น์ง ๊ธฐ๋กํ๋ค.
What this post covers
- ๋ฌธ์ ์ํฉ ๋ฐ ์์ฒญ ํ๋ฆ ์ ๋ฆฌ
JSESSIONID๋ฏธ์ ๋ฌ์ ๊ทผ๋ณธ ์์ธHttpServletRequest๋ก ์ฟ ํค ํฌ์๋ฉ ๊ตฌํ- MockMvc์์
getRequestedSessionId()๊ฐnull์ ๋ฐํํ๋ ์ด์ ์ ํด๊ฒฐ๋ฒ
Problem
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", "") ๋ ๋น ๋ฌธ์์ด์ ๋ฐํํ๋ค.
Root Cause
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์ผ๋ก ์ ๋ฌํด์ผ ํ๋ ์ํฉ ์ ๊ณ ๋ คํ์ง ๋ชปํ๋ค.
Solution
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);
}
}, ...);
};
}
Testing Gotcha: 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);
Takeaway
- Spring์ด ์ง์ ์ธ์ ์ธ์ฆ์ ์ฒ๋ฆฌํ๋๋ผ๋, ํ์ ์๋น์ค๊ฐ ๊ทธ ์ธ์ ์ ์ญ์ผ๋ก ์ฌ์ฉํด์ผ ํ๋ ๊ฒฝ์ฐ ๋ ์ฟ ํค ํฌ์๋ฉ์ ๋ช ์์ ์ผ๋ก ๊ตฌํํด์ผ ํ๋ค.
- MockMvc๋ ์ค์ ์๋ธ๋ฆฟ ์ปจํ
์ด๋์ ์ฟ ํค ํ์ฑ ๊ณผ์ ์ ์๋ตํ๋ค.
getRequestedSessionId()์ฒ๋ผ ์ฟ ํค์์ ํ์๋๋ ๊ฐ์RequestPostProcessor๋ก ์๋ ์ค์ ํด์ผ ํ ์คํธ๊ฐ ๋์ํ๋ค.
