왜 SSE로 통신할 때 JSON String이 오지?

가을·2025년 7월 16일

💛 KakaoBase

목록 보기
3/3

Introduction

오늘은 저번에 구현해놓은 AI서버의 채팅 기능과, BE간의 채팅 통신(SSE ; Server-Sent Events)을 테스트했다.
우리 팀의 BE인 rick.lee랑 진행중에 알게 된 사실을 공유한다.

Background

BE(Spring boot)쪽에서 ai요청에 대해 JSON으로 파싱하는 코드를 작성해서 사용중이었음.

Problem definition

그런데 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"}

Study

JSON과 JSON String의 차이점

1. JSON

  • 데이터 구조(Structure)나 형식(Format)을 의미함.
    • key-value 쌍으로 이루어져있고, 계층을 가질 수 있음
  • Python의 딕셔너리(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'>

2. JSON String

  • 위에서 설명한 데이터 구조를 전송하거나, 저장하기 위해 하나의 긴 문자열(String)으로 변환한 결과물
  • 네트워크를 통해 데이터를 보내거나, 파일에 데이터를 쓸 때는 메모리에 있는 데이터 구조를 직접 보낼 수 없기 때문에 이런 텍스트 형태로 변환하는 것임.
  • 이 과정을 직렬화(Serialization)이라고 한다.
  • JSON String은 그냥 긴 텍스트이기 때문에, 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

왜 SSE를 사용할 때, JSON String 형식으로 데이터를 전송해야하는가?

요약

  • 네트워크는 메모리에 있는 데이터 구조(객체, 딕셔너리 등)를 직접 실어서 보낼 수 없다.
  • 네트워크는 오직 바이트(byte)의 흐름, 즉 텍스트 같은 순차적인 데이터만 전송 할 수 있다

이유

  1. 전송의 한계 :
    서버의 메모리에 있는 파이썬 딕셔너리 객체는 서버 컴퓨터에서만 의미가 있는 데이터 덩어리다.
    이 데이터 구조를 네트워크 케이블을 통해 클라이언트 컴퓨터로 그대로 '밀어 넣을'방법은 없다.
    네트워크는 이런 복잡한 구조가 아닌, 순차적인 바이트 스트림만 전송할 수 있다.
    2. 표준화의 필요성 :
    설령 보낼 수 있다고 해도, 파이썬의 딕셔너리를 자바스크립트나 자바가 직접 이해할 수 없다. 각 언어는 데이터를 메모리에 저장하는 방식이 다르기 때문. 그래서 모든 언어가 공통적으로 이해할 수 있는 '표준 글자 형식'이 필요한데, 그것이 바로 JSON String이다.
  2. 직렬화 (Serialization) :
    그래서 서버는 전송 전에 자신의 딕셔너리를 json.dumps()를 통해 JSON String으로 변환.
  3. 역직렬화 (Deserialization) :
    클라이언트는 이 JSON String을 받은 뒤, JSON.parse()같이 각각의 언어에 맞는 객체로 재조립함.

그렇다면 왜 HTTP 통신에서는 JSON형식으로 가는 걸까?

요약

편리한 도구들이 중간 과정을 자동으로 처리해주기 때문에 마치 JSON 형식이 그대로 가는 것 처럼 보일 뿐. 데이터 구조 자체가 네트워크를 통해 전송되지 않는다.

왜 그렇게 보일까? : Content-Type 헤더

일반 HTTP통신이 SSE와 다른 점은 "이 데이터는 JSON String이니, 받으면 JSON 객체로 해석해주세요" 라는 Header를 붙여서 보낸다는 것. 이 Header가 Content-Type : application/json header다.

HTTP 통신 과정을 살펴보자

서버에서 {"status":"ok"}라는 응답을 보낸다고 가정.

[서버]

  1. 데이터 준비 : 먼저 파이썬 딕셔너리 형태의 데이터를 만든다.
    response_data = {"status": "ok"}
  1. JSON String으로 변환(직렬화)
    json_string = '{"status": "ok"}'
  1. HTTP 응답 생성 : 이 JSON Stringdmf HTTP 응답 본문(Body)에 싣는다. 그리고 Header를 붙인다.
    HTTP/1.1 200 OK
    Content-Type: application/json  <-- "JSON 이 들어있어요!" 라는 표시
    Content-Length: 15

    {"status": "ok"} <-- 실제 내용(JSON String)

[네트워크]

Postman, 웹브라우저 같은 HTTP 클라이언트 도구들은 위의 응답을 받으면 다음과 같이 행동합니다.

  1. 응답 확인 :
    가장 먼저 Content-Type 헤더를 확인.
  2. 자동 변환 (역직렬화) :
    Content-Type 헤더 내용을 기반으로, 응답 본문에 있던 {"status": "ok"}라는 순수 String을 자동으로 자바스크립트의 객체나 파이썬의 딕셔너리로 변환(Parsing)해줌.
  3. 개발자에게 전달 :
    그리고 최종적으로 변환된 객체를 개발자에게 보여준다.

-> 이 자동 변환 과정때문에 개발자는 마치 서버가 처음부터 JSON 객체를 보내준 것처럼 느끼게 됨.

HTTP와 SSE의 Content-Type 헤더 차이점

  • 일반 HTTP 통신 : Content-Type: application/json
  • SSE 통신 : Content-Type: text/event-stream
    • 정해진 형식(event : , data : )에 따라 텍스트가 계속 스트리밍 된다. data 필드에 있는 내용이 JSON String인지는 클라이언트 측에서 직접 해석하고 파싱해야한다.

스트리밍 기능을 구현하다보니, 생각치도 못했던 부분을 알게되어서 좋다

profile
안녕하세요. 2년차 머신러닝 엔지니어입니다.

0개의 댓글