업로드가 성공해도 메시지에는 파일 ID가 남지 않을 수 있다
튜터 대화의 파일 준비는 문서를 외부 제공자에 업로드하고, 반환된 파일 ID를 사용자 메시지의 첨부 메타데이터에 합친다. 그런데 합칠 대상이 아직 저장되지 않았다면 업로드 성공만으로는 메시지를 완성할 수 없다.
AI Chat / Tutor의 구현에서 파일 ID 병합 도우미는 메시지의 attachments를 읽는다. 빈 배열이면 곧바로 반환한다. 당시 첨부 메타데이터 저장은 제공자별 스트림 실행 안에 있었고, 파일 준비 콜백은 그보다 먼저 실행됐다. 병합이 빈 배열을 보고 끝난 뒤 메타데이터만 늦게 저장되므로 이미 얻은 제공자 ID는 남지 않았다.
실패한 함수가 없다는 점도 이 순서 오류를 숨긴다. 빈 배열을 처리하는 도우미의 동작은 정상이고, 뒤의 저장도 성공할 수 있다. 두 함수의 전제와 호출 순서가 서로 맞지 않는 문제다.
콜백의 입력을 콜백보다 먼저 준비한다
수정에서는 사용자·AI 메시지를 만든 뒤, 공통 BaseStream 경로에서 첨부 저장을 await하도록 옮겼다. Observable을 반환하기 전에 이 저장을 마치고, 구독으로 시작하는 파일 준비가 저장된 첨부를 읽게 한다.
- 메시지 생성과 첨부 메타데이터 저장
파일명과 로컬 첨부 정보를 사용자 메시지에 먼저 넣는다.
- 준비 콜백에서 외부 업로드
문서마다 제공자 파일 ID를 받아 파일명→ID 매핑을 만든다.
- 기존 첨부에 ID 병합
파일명이 일치하는 메타데이터에 file_id를 더한다.
- 제공자 요청 실행
준비한 input_file·input_text를 사용한다. 뒤에서 첨부 메타데이터를 다시 덮어쓰지 않는다.
OpenAI와 Gemini의 뒤쪽 저장 호출도 제거했다. 단순히 앞에 저장을 하나 추가하고 뒤의 저장을 남겨두면 ID가 보강된 메타데이터를 원래 형태로 덮을 수 있다. 공통 경로가 첨부의 최초 저장을 소유하도록 책임을 옮긴 것이다.
저장 순서 수정은 외부 업로드를 원자적으로 만들지 않는다
저장 위치를 앞으로 옮기면 실패가 발생하는 경계도 바뀐다. 최초 첨부 저장이 실패하면 Observable을 돌려주기 전에 함수가 실패한다. 실행 중인 스트림 안에서 오류 이벤트를 내보내는 경우와 구분해야 한다.
외부 업로드와 DB 병합은 하나의 트랜잭션이 아니다. 여러 파일의 병렬 업로드 중 하나가 실패해도 이미 성공한 업로드를 자동 삭제하지 않는다. 파일명으로 ID를 연결하므로 같은 이름의 첨부가 겹치는 문제도 남는다. 이 수정은 병합 전에 필요한 데이터를 준비하며, 업로드의 중복 실행이나 부분 실패 정리까지 보장하지 않는다.
회귀 테스트 코드는 Observable을 구독하고 첨부 저장 호출과 준비 콜백의 순서를 배열에 기록한다. 저장이 먼저 호출됐는지와 전달한 인자를 검사한다. 파일 준비 테스트는 업로드·파일명 매핑·요청 본문·문서 없는 경우를 다룬다. 실제 DB와 외부 파일 서비스까지 연결한 검증은 아니지만, 개별 함수의 결과 검사만으로 놓치던 “병합 대상이 먼저 존재해야 한다”는 조건을 직접 표현한다.