오늘은 저번에 구현해놓은 AI서버의 채팅 기능과, BE간의 채팅 통신(SSE ; Server-Sent Events)을 테스트했다.
우리 팀의 BE인 rick.lee랑 진행중에 알게 된 사실을 공유한다.
BE(Spring boot)쪽에서 ai요청에 대해 JSON으로 파싱하는 코드를 작성해서 사용중이었음.
그런데 AI쪽에서 SSE로 보내준 데이터가 JSON형식이 아니어서 에러가 난다는 BE 로그를 발견.
AI쪽 코드를 확인해 보니 아래와 같았음.
# kaeul/22-tenten-ai/services/bot_chats_service.py
# ...
stream_data = {
"stream_id": stream_id,
"message": token + " ", # 각 단어 뒤에 공백을 붙여서 전송
"timestamp": datetime.now().strftime('%Y-%m-%dT%H:%M:%S')
}
# stream_data 딕셔너리를 JSON 문자열로 변환하여 전송
await sse_manager.broadcast(f"event: stream\ndata: {json.dumps(stream_data, ensure_ascii=False)}\n\n")
# ...
서버가 클라이언트(BE 서버)로 이벤트를 보낼때, Python dictionary를 json.dumps()를 사용해 JSON String로 변환한 뒤 보내고 있었음.
따라서 클라이언트는 data필드에 포함된 내용을 JSON으로 파싱 해서 사용해야함.
실제로 클라이언트가 받는 raw data의 형식은 아래의 예제와 같음.
event: stream
data: {"stream_id": "some-stream-id", "message": "안녕하세요 ", "timestamp": "2024-07-17T12:34:56"}
event: stream
data: {"stream_id": "some-stream-id", "message": "반갑습니다 ", "timestamp": "2024-07-17T12:34:57"}
event: done
data: {"stream_id": "some-stream-id", "message": null, "timestamp": "2024-07-17T12:34:58"}
딕셔너리(dict)나 JavaScript의 객체(Object)처럼 메모리에 존재하는 실제 데이터 덩어리임.data['user_id']와 같이 특정 키를 이용해 값에 바로 접근 가능# 이것은 'JSON 데이터 구조'에 해당하는 파이썬 딕셔너리입니다.
data_dict = {
"stream_id": "test-123",
"user_id": 1,
"message": "안녕하세요"
}
# 딕셔너리이므로 키로 값에 접근할 수 있습니다.
print(data_dict['user_id']) # 출력: 1
print(type(data_dict)) # 출력: <class 'dict'>
문자열(String)으로 변환한 결과물직렬화(Serialization)이라고 한다.json_string['user_id']와 같이 키로 값에 접근 할 수 없음. 그래서 이 문자열을 다시 데이터 구조로 변환(Parsing)하는 역직렬화해야함.import json
data_dict = {
"stream_id": "test-123",
"user_id": 1,
"message": "안녕하세요"
}
# 딕셔너리(데이터 구조)를 JSON 문자열(텍스트)로 변환 (직렬화)
json_string = json.dumps(data_dict, ensure_ascii=False)
print(json_string) # 출력: '{"stream_id": "test-123", "user_id": 1, "message": "안녕하세요"}'
print(type(json_string)) # 출력: <class 'str'>
# 문자열이므로 이렇게 접근하면 에러가 발생합니다!
# json_string['user_id'] # TypeError 발생
# 사용하려면 다시 딕셔너리로 변환해야 합니다 (역직렬화).
parsed_dict = json.loads(json_string)
print(parsed_dict['user_id']) # 출력: 1
요약
- 네트워크는 메모리에 있는 데이터 구조(객체, 딕셔너리 등)를 직접 실어서 보낼 수 없다.
- 네트워크는 오직 바이트(byte)의 흐름, 즉 텍스트 같은 순차적인 데이터만 전송 할 수 있다
json.dumps()를 통해 JSON String으로 변환.JSON.parse()같이 각각의 언어에 맞는 객체로 재조립함.요약
편리한 도구들이 중간 과정을 자동으로 처리해주기 때문에 마치 JSON 형식이 그대로 가는 것 처럼 보일 뿐. 데이터 구조 자체가 네트워크를 통해 전송되지 않는다.
Content-Type 헤더일반 HTTP통신이 SSE와 다른 점은 "이 데이터는 JSON String이니, 받으면 JSON 객체로 해석해주세요" 라는 Header를 붙여서 보낸다는 것. 이 Header가 Content-Type : application/json header다.
서버에서
{"status":"ok"}라는 응답을 보낸다고 가정.
[서버]
response_data = {"status": "ok"}
json_string = '{"status": "ok"}'
HTTP/1.1 200 OK
Content-Type: application/json <-- "JSON 이 들어있어요!" 라는 표시
Content-Length: 15
{"status": "ok"} <-- 실제 내용(JSON String)
[네트워크]
Postman, 웹브라우저 같은 HTTP 클라이언트 도구들은 위의 응답을 받으면 다음과 같이 행동합니다.
Content-Type 헤더를 확인.Content-Type 헤더 내용을 기반으로, 응답 본문에 있던 {"status": "ok"}라는 순수 String을 자동으로 자바스크립트의 객체나 파이썬의 딕셔너리로 변환(Parsing)해줌. -> 이 자동 변환 과정때문에 개발자는 마치 서버가 처음부터 JSON 객체를 보내준 것처럼 느끼게 됨.
Content-Type 헤더 차이점Content-Type: application/jsonContent-Type: text/event-streamevent : , data : )에 따라 텍스트가 계속 스트리밍 된다. data 필드에 있는 내용이 JSON String인지는 클라이언트 측에서 직접 해석하고 파싱해야한다.스트리밍 기능을 구현하다보니, 생각치도 못했던 부분을 알게되어서 좋다