개요
요리는 30초면 나오는데 손님은 2분을 기다리는 식당이 있다고 해보자. 주방이 느린 게 아니라, 주문서 한 장 한 장이 주방까지 가는 데 시간이 다 간다.
ChatGPT가 만든 분석 이미지를 Notion에 올리는 브릿지가 그랬다. 작업이 너무 오래 걸려서 기록을 뜯어 보니, 실제 전송은 중앙값 0.37초였다. 나머지 16~143초는 단계와 단계 사이에서 기다리는 시간이었다.
분석 이미지도 Notion에 남기고 싶었다
앞 글에서는 Notion 원본을 ChatGPT 채팅으로 가져왔다. 이번엔 반대 방향이다.
ChatGPT에 그림 피드백을 받으면 원본 위에 흐름이나 인체 구조를 그린 주석 이미지, 특정 부위 확대, 여러 작품 비교 같은 분석 이미지가 같이 나온다. 이걸 원본과 한 기록으로 묶어 Notion에 남기고 싶었다. 외부 URL을 임베드하는 게 아니라 Notion 자체에 저장된 파일로.
그런데 ChatGPT의 코드 실행 환경(sandbox)은 외부 네트워크가 막혀 있어서, Notion File Upload API로 바이너리를 직접 보내는 게 실패했다. 그래서 다운로드 때처럼 ChatGPT Sites를 파일 전송 브릿지로 두기로 했다.
처음 요청서에는 브릿지가 Notion Integration Token을 들고 인증부터 페이지 수정까지 다 하는 구조를 적었다. 그런데 토큰을 서버 Secret으로 넣는 단계가 오자 이게 왜 필요한지부터 걸렸다.
그냥 노션 MCP 쓰면 안대냐? 알아해라
Notion 인증은 어차피 내 계정에서 자동으로 되는데 왜 MCP 인증이라던가 그런게 필요해? 다른 내 Sites는 그런거 설정 안해도 잘 되는데
Chat에는 이미 내 계정으로 연결된 Notion 도구가 있다. 업로드 주소 발급과 페이지 편집은 그걸 쓰면 되고, 브릿지는 Notion이 못 받는 바이너리만 대신 나르면 된다. 그래서 브릿지에는 토큰 없이 세 가지만 남았다.
-
stage_image: 이미지 bytes를 브릿지 임시 저장소에 둔다. -
send_image_to_notion: Notion 업로드 주소로 bytes를 보낸다. -
verify_notion_image: Notion에 저장된 파일을 다시 받아 SHA-256과 크기를 대조한다.
첫 실전 호출에서 전송은 HTTP 502 OUTCOME_UNKNOWN으로 끝났다. 보냈는지 안 보냈는지 모르는 상태다. 이 구간을 고친 뒤로 실제 그림 분석 대화에서 업로드가 쌓이기 시작했는데, 쓰다 보니 이번엔 느렸다.
업로드 성공과 페이지 첨부는 다른 일이다
느린 원인을 찾기 전에 구조부터 봐야 했다. Notion에 파일을 붙이는 건 한 동작처럼 보여도 실제로는 세 단계다.
- 업로드 객체를 만들고 bytes를 전송한다. 이때
file_uploadID가 생긴다. - 그 ID를 페이지의 이미지 블록에 연결한다.
- 붙은 파일을 다시 받아 원본과 같은지 확인한다.
Notion 공식 문서 기준으로 업로드한 파일은 1시간 안에 첨부해야 하고, 한 번 업로드한 file_upload ID는 여러 블록과 페이지에 다시 붙일 수 있다. 전송이 성공했어도 첨부가 안 된 채 시간이 지나면 그 업로드는 쓸 수 없게 된다.
브릿지가 독립적으로 보장하는 건 bytes 무결성까지다. 그 파일이 정말 해당 페이지에 붙었는지는 Notion 도구로 페이지를 편집한 쪽, 즉 호출자가 확인해 보고하는 구조다. 서버가 페이지 첨부까지 검증했다고 말하면 근거 범위를 넘는다.
중복 방지가 복구를 막고 있었다
업로드 작업은 같은 이미지가 두 번 올라가지 않도록 key로 구분했다. 이전 key는 owner / page / SHA였다. 로컬에서 재현해 보니 두 가지가 걸렸다.
- 같은 페이지에 같은 bytes면, 역할(원본인지 주석인지 확대인지)이나 기록 ID가 달라도 첫 작업으로 합쳐졌다.
- 전송은 성공했는데 첨부 전에 오래된 작업은, 새 업로드 주소를 받아 와도 “이미 있는 작업”이라 다시 보낼 수 없었다.
실제 기록에는 이걸 피해 간 흔적도 있었다. 03_crop_1.png의 PNG 메타데이터를 바꿔 중복 판정을 통과시킨 것이다. 픽셀은 같아도 bytes가 달라지니 해시도 달라진다. 원본을 보존하려는 시스템에서 원본을 고쳐서 재시도한 셈이라, 이 방식으로 복구를 이어 가면 안 됐다.
새 구조는 이렇게 바꿨다.
| 바꾼 것 | 내용 |
|---|---|
| 작업 key |
owner / page / SHA / role / record. 같은 bytes여도 역할·기록이 다르면 별도 작업 |
| 기존 작업 | ID는 계속 조회되고, 캡션·파일명이 충돌하면 조용히 덮어쓰지 않고 알린다 |
| 재시도 | 최근 Notion 확인에서 만료·실패와 첨부 부재가 관찰된 경우만 child 시도를 새로 만든다. 부모 기록과 원본 bytes는 그대로 둔다 |
OUTCOME_UNKNOWN |
새 업로드 주소를 받아도 자동으로 다시 보내지 않는다. 상태 확인이 먼저다 |
| 업로드 ID 재사용 | 같은 해시를 다른 페이지에 다시 올린 사례가 두 쌍 있었다. 검증된 ID를 재사용할 수 있게 했지만, 재사용했다고 새 페이지 첨부가 끝난 건 아니다 |
같은 주문이 두 번 들어오면 거절하는 게 맞다. 그런데 음식이 식어서 다시 해달라는 주문까지 같은 주문이라고 거절하고 있었다. “같은 이미지에 대한 새 시도”를 표현할 방법이 없었던 거다.
느린 건 전송이 아니었다
Site에 남은 작업 기록 24개 중 양 끝 시각이 다 있는 구간만 골라 쟀다.
| 구간 | 기록 |
|---|---|
| 바이너리 전송 (23건) | 중앙값 0.366초 |
| 재다운로드·해시 검증 (23건) | 중앙값 0.214초 |
| 준비 완료 → 전송 시작 (최근 6건) | 16.1~142.6초 |
| 전송 완료 → 검증 시작 (최근 6건) | 55.7~175.5초 |
전송과 검증은 1초도 안 걸린다. 시간은 단계 사이에 있었다. 파일마다 에이전트가 도구를 하나 부르고, 결과를 읽고, 다음 도구를 다시 부르는 사이에 업로드 주소 발급, 페이지 편집, 다른 파일 처리, 모델과 호스트 대기가 끼어든다. 이 기록만으로는 그중 무엇이 얼마나 차지하는지까지는 나눌 수 없었다.
그래서 검증을 줄이는 대신 왕복을 줄였다. 주문서를 한 장씩 들고 가지 않고 묶어서 넘기는 식이다. 전송은 최대 10개, 검증은 최대 25개를 한 번에 처리하는 일괄 도구를 만들었다. 같은 페이지 안의 순서는 지키고, 다른 페이지는 최대 2개까지만 동시에 돌린다. Notion API는 평균 초당 3회 요청 한도가 있어서, 429를 받으면 고정 지연을 추측하지 않고 응답의 Retry-After를 돌려주며 시작 안 한 전송을 멈춘다.
준비된 파일 6개의 전송·검증 두 단계를 mock으로 비교하면 이렇다.
| 항목 | 단건 호출 | 일괄 호출 |
|---|---|---|
| 브릿지 도구 호출 | 12 | 2 |
| 바이너리 POST | 6 | 6 |
| 검증 GET | 6 | 6 |
| SHA 검증 완료 | 6 | 6 |
POST와 GET 수는 그대로고 도구 호출만 줄었다. 이걸 “6배 빨라졌다”로 말할 수는 없다. 실제 여러 장을 올리는 전체 시간은 이 비교에 들어 있지 않다.
하나 더 있었다. “읽기 전용”이라던 작업 목록 조회가, 이미 정리된 작업에도 정리 작업을 매번 다시 하고 있었다. 완료 작업 10개를 한 번 조회할 때마다 DB 쓰기 30회와 저장소 삭제 10회가 났다. 목록과 계획 조회에서 쓰기와 삭제를 빼고, 만료 파일 정리는 따로 부르는 도구로 분리했다.
코드는 둘로, 입구는 하나로
이 시점에 Site가 여러 개였다. 업로드 브릿지, 원본을 가져오는 다운로드 Site, 앞 글의 위젯 실험용 Viewer Lab까지. 감사 결과에는 “업로드와 다운로드는 역할이 다르니 Site를 나눠 두는 게 낫다”는 추천도 있었다. 그런데 내 생각은 달랐다.
sites들을 병합해서 Notion 전용 브릿지 처럼 통합할 순 없어? 여러 개로 운영하는게 유지관리는 좋아보여도 인입점이 달라서 재수정 요청할 때마다 헷갈릴 것 같아
고치고 싶을 때마다 “이건 어느 Site였지?”부터 따져야 하는 게 문제였다. 그래서 코드 안에서는 upload relay와 download module로 책임을 나누되, Site·배포·MCP endpoint와 이름은 Notion Bridge 하나로 합쳤다. 기존 업로드 Site의 DB와 저장소, 작업 ID는 그대로 유지했다. 코드 책임을 나누는 것과 서비스를 여러 개 운영하는 건 따로 고를 수 있는 문제였다.
그다음은 정리였다. 테스트용이거나 더는 안 쓸 플러그인과 Site를 이력으로 추려, 하나씩 의존성을 확인받고 지웠다. Viewer Lab은 플러그인 연결을 끊고 Site를 삭제한 뒤 목록에서 사라진 걸 확인했고, 옛 다운로드 Site는 내가 직접 지웠다.
통합 뒤에는 스킬에 옛 표시 이름이 남아 있는 게 걸렸다. 도구 설명에 적힌 이름보다 실제 namespace와 상태 응답이 연결의 더 직접적인 증거라, 스킬이 notion_bridge namespace와 상태 응답으로 연결을 확인하게 고쳤다. 마지막 확인에서 통합 브릿지의 도구 15개와 workflow 2.2.0 상태 응답, 원본 bytes 수신까지 실제 호출로 나왔다.
소감
업로드가 느리니까 당연히 파일 올리는 게 오래 걸리는 줄 알았는데, 전송은 0.4초도 안 됐다니..! 멍청한 AI 좌식 내가 말을 해야 알아 듣는구나…ㅠ