남의 스트림에 손을 대기로 했다
하루에 같은 열 줄을 다섯 번 고쳤다. 아무것도 잃지 않은 버전은 17분 만에 지운 것이었다.
외부 모델 공급자를 붙일 때 우리가 한 약속이 하나 있다. 어느 공급자를 쓰든 클라이언트가 받는 응답 형태는 같아야 한다는 것.
공급자들의 SSE 페이로드는 제각각이다. 필드 이름이 다르고, 중첩 깊이가 다르고, 종료 신호도 다르다. 약속을 지키려면 스트림이 지나갈 때 각 이벤트를 열어서 우리 형식으로 바꿔야 한다.
그 순간부터 우리는 릴레이가 아니라 파서가 된다. 이 글은 그 대가에 관한 것이다.
열 줄, 24시간, 다섯 버전
2025년 8월 4일 오전 10시 17분부터 다음 날 오전 10시 1분까지, 같은 자리의 코드가 다섯 번 바뀌었다.
| 시각 | 무엇을 했나 |
|---|---|
| 8/4 15:20 | 줄 단위 읽기를 버리고 바이트 + 버퍼로 이벤트를 재조립 |
| 8/4 15:37 | 버퍼 제거. 청크를 그대로 흘려보냄 |
| 8/4 17:21 | 버퍼 복구 + SSE 이벤트 정규식 도입 |
| 8/5 10:01 | 정규식 제거. "\n\n" 로 자르고 data: 있는 것만 통과 |
넣었다 뺀 것이 두 번이다. 버퍼를 넣고 17분 뒤에 뺐고, 정규식을 넣고 다음 날 아침에 뺐다.
커밋 메시지는 전부 "개선"이라고 말한다. 무엇이 어떻게 잘못됐는지는 적혀 있지 않다. 그래서 네 버전을 각각 떼어내서 같은 스트림을 먹여봤다.
네 버전 중 아무것도 잃지 않은 건 하나다. 원시 통과. 그리고 그건 17분 만에 지워졌다.
왜 원시 통과로 돌아가지 않았나
이 글을 쓰면서 가장 궁금했던 게 그거였다. 손실이 없는 구현이 눈앞에 있었는데 왜 다시 파싱으로 갔을까.
답은 지금 코드에 있다. 최종 구현은 이벤트를 꺼내서 게이트웨이 공용 형식으로 다시 매핑하고, 종료 신호를 걸러내고, 특정 이벤트 타입에서는 필드를 비운다.
즉 원시 통과는 아무것도 잃지 않는 대신 아무것도 바꾸지 않는다. 그런데 이 시스템이 존재하는 이유가 "공급자마다 다른 형태를 하나로 맞춘다"였다. 손대지 않고 흘려보내는 것과 형식을 통일하는 것은 동시에 성립하지 않는다.
열 줄이 열 줄이 아닌 이유
이 코드가 왜 하루 만에 다섯 번이나 손을 타야 했는지는 코드가 어디에 있는지를 봐야 이해된다. 이건 서버가 실행하는 코드가 아니라, 서버가 만들어내는 코드였다.
노드에 커서를 올리면 연결된 것만 남는다
여기서 두 가지가 따라온다.
첫째, 템플릿의 버그는 이미 배포된 모든 공급자에게 복사된 상태다. 템플릿을 고치는 것으로는 아무것도 고쳐지지 않는다. 배포된 파일을 전부 다시 찍어내야 한다. 그래서 그 일만 하는 유지보수 엔드포인트가 따로 있다.
둘째, 이 코드는 디버거로 밟기 어렵다. 문자열 안에 있으니 편집기가 문법을 검사해 주지 않고, 자동 완성도 없고, 이스케이프가 한 겹 더 있다. "\n\n" 를 두 글자로 셀지 네 글자로 셀지 헷갈린 15:20 의 버그가 바로 그 한 겹에서 나왔다.
지금은 어떤가
넉 달 뒤 이 로직은 템플릿 문자열 밖으로 빠져나와 공용 함수가 됐다. 생성되는 파일은 이제 그 함수를 부르기만 한다. 문자열 안에서 편집하던 시절은 끝났다.
손실도 하나 복구됐다. 콜론으로 시작하는 주석 프레임은 다시 그대로 흘려보낸다. 하트비트가 살아났다.
남은 것
event: 와 id: 만 있는 프레임은 지금도 버려진다. 최종 구현은 data 가 있는 프레임을 처리하고, 없으면 주석인지만 확인한다. 둘 다 아닌 프레임은 어디에도 도달하지 않는다. 8월 5일 아침에 생긴 이 손실은 그대로 남아 있다. 지금 붙어 있는 공급자들이 그런 프레임을 보내지 않아서 아무도 모르고 있을 뿐이다.
커밋 메시지가 증상을 남기지 않았다. 다섯 개 전부 "개선"이라고 적혀 있다. 무엇이 깨졌는지, 왜 되돌렸는지는 없다. 그래서 이 글을 쓰려고 네 버전을 다시 떼어내 돌려봐야 했다. 넉 달 뒤의 나에게 필요한 건 "개선"이 아니라 "이렇게 하면 두 글자가 잘린다" 였다.
하루에 다섯 번 고쳤다는 건 배포마다 확인할 방법이 없었다는 뜻이다. 스트림 하나를 먹여서 나온 것을 비교하는 스무 줄짜리 검사가 있었다면 15:20 의 버그는 커밋 전에 걸렸다. 그 검사를 이 글을 쓰면서 처음 만들었다. 순서가 반대였다.